✨ PKCE / OAuth login will be chosen on the fly, depending on the desired audio quality of the track to speed up non HIRES_LOSSLESS downloads. Quality for tracks in download queue can be chosen individually. Fixes #73
This commit is contained in:
@@ -86,6 +86,7 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
|
|||||||
session: tidalapi.Session
|
session: tidalapi.Session
|
||||||
token_from_storage: bool = False
|
token_from_storage: bool = False
|
||||||
settings: Settings
|
settings: Settings
|
||||||
|
is_pkce: bool
|
||||||
|
|
||||||
def __init__(self, settings: Settings = None):
|
def __init__(self, settings: Settings = None):
|
||||||
self.cls_model = ModelToken
|
self.cls_model = ModelToken
|
||||||
@@ -110,9 +111,13 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def login_token(self) -> bool:
|
def login_token(self, do_pkce: bool = True) -> bool:
|
||||||
result = False
|
result = False
|
||||||
|
|
||||||
|
# Do not login via PKCE, if HiRes LOSSLESS is not set as global audio quality.
|
||||||
|
if self.session.audio_quality != tidalapi.media.Quality.hi_res_lossless:
|
||||||
|
do_pkce = False
|
||||||
|
|
||||||
if self.token_from_storage:
|
if self.token_from_storage:
|
||||||
try:
|
try:
|
||||||
result = self.session.load_oauth_session(
|
result = self.session.load_oauth_session(
|
||||||
@@ -120,8 +125,9 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
|
|||||||
self.data.access_token,
|
self.data.access_token,
|
||||||
self.data.refresh_token,
|
self.data.refresh_token,
|
||||||
self.data.expiry_time,
|
self.data.expiry_time,
|
||||||
is_pkce=True,
|
is_pkce=do_pkce,
|
||||||
)
|
)
|
||||||
|
self.is_pkce = do_pkce
|
||||||
except HTTPError:
|
except HTTPError:
|
||||||
result = False
|
result = False
|
||||||
|
|
||||||
|
|||||||
+27
-4
@@ -12,9 +12,9 @@ import requests
|
|||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
from rich.progress import Progress, TaskID
|
from rich.progress import Progress, TaskID
|
||||||
from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video
|
from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video
|
||||||
from tidalapi.media import AudioExtensions, StreamManifest, VideoExtensions
|
from tidalapi.media import AudioExtensions, Quality, StreamManifest, VideoExtensions
|
||||||
|
|
||||||
from tidal_dl_ng.config import Settings
|
from tidal_dl_ng.config import Settings, Tidal
|
||||||
from tidal_dl_ng.constants import EXTENSION_LYRICS, REQUESTS_TIMEOUT_SEC, MediaType, SkipExisting
|
from tidal_dl_ng.constants import EXTENSION_LYRICS, REQUESTS_TIMEOUT_SEC, MediaType, SkipExisting
|
||||||
from tidal_dl_ng.helper.decryption import decrypt_file, decrypt_security_token
|
from tidal_dl_ng.helper.decryption import decrypt_file, decrypt_security_token
|
||||||
from tidal_dl_ng.helper.exceptions import MediaMissing
|
from tidal_dl_ng.helper.exceptions import MediaMissing
|
||||||
@@ -174,6 +174,7 @@ class Download:
|
|||||||
media_type: MediaType = None,
|
media_type: MediaType = None,
|
||||||
video_download: bool = True,
|
video_download: bool = True,
|
||||||
download_delay: bool = False,
|
download_delay: bool = False,
|
||||||
|
quality: Quality | None = None,
|
||||||
) -> (bool, str):
|
) -> (bool, str):
|
||||||
try:
|
try:
|
||||||
if media_id and media_type:
|
if media_id and media_type:
|
||||||
@@ -202,6 +203,10 @@ class Download:
|
|||||||
|
|
||||||
return False, ""
|
return False, ""
|
||||||
|
|
||||||
|
# If a quality is explicitly set, change it.
|
||||||
|
if quality:
|
||||||
|
quality_old: Quality = self.quality_adjust(quality)
|
||||||
|
|
||||||
# Get extension.
|
# Get extension.
|
||||||
file_extension: str
|
file_extension: str
|
||||||
|
|
||||||
@@ -251,6 +256,10 @@ class Download:
|
|||||||
|
|
||||||
status_download: bool = not file_exists
|
status_download: bool = not file_exists
|
||||||
|
|
||||||
|
if quality:
|
||||||
|
# Set quality back to the global user value
|
||||||
|
self.quality_adjust(quality_old)
|
||||||
|
|
||||||
# Whether a file was downloaded or skipped and the download delay is enabled, wait until the next download.
|
# Whether a file was downloaded or skipped and the download delay is enabled, wait until the next download.
|
||||||
# Only use this, if you have a list of several Track items.
|
# Only use this, if you have a list of several Track items.
|
||||||
if download_delay:
|
if download_delay:
|
||||||
@@ -261,6 +270,20 @@ class Download:
|
|||||||
|
|
||||||
return status_download, path_file
|
return status_download, path_file
|
||||||
|
|
||||||
|
def quality_adjust(self, quality) -> Quality:
|
||||||
|
# Save original quality settings
|
||||||
|
quality_old: Quality = self.session.audio_quality
|
||||||
|
self.session.audio_quality = quality
|
||||||
|
tidal: Tidal = Tidal()
|
||||||
|
|
||||||
|
# If track is not requested as hires_lossless do not use PKCE, because BTS downloads are faster than MPD.
|
||||||
|
if quality == Quality.hi_res_lossless and not tidal.is_pkce:
|
||||||
|
tidal.login_token(do_pkce=True)
|
||||||
|
elif quality != Quality.hi_res_lossless and tidal.is_pkce:
|
||||||
|
tidal.login_token(do_pkce=False)
|
||||||
|
|
||||||
|
return quality_old
|
||||||
|
|
||||||
def _move_lyrics(self, file_media_dst: str, file_media_src: str):
|
def _move_lyrics(self, file_media_dst: str, file_media_src: str):
|
||||||
# Build tmp lyrics filename
|
# Build tmp lyrics filename
|
||||||
tmp_lyrics_file_path: str = file_media_src + EXTENSION_LYRICS
|
tmp_lyrics_file_path: str = file_media_src + EXTENSION_LYRICS
|
||||||
@@ -340,6 +363,7 @@ class Download:
|
|||||||
media_type: MediaType = None,
|
media_type: MediaType = None,
|
||||||
video_download: bool = False,
|
video_download: bool = False,
|
||||||
download_delay: bool = True,
|
download_delay: bool = True,
|
||||||
|
quality: Quality | None = None,
|
||||||
):
|
):
|
||||||
# If no media instance is provided, we need to create the media instance.
|
# If no media instance is provided, we need to create the media instance.
|
||||||
if media_id and media_type:
|
if media_id and media_type:
|
||||||
@@ -381,8 +405,7 @@ class Download:
|
|||||||
for media in items:
|
for media in items:
|
||||||
# Download the item.
|
# Download the item.
|
||||||
status_download, result_path_file = self.item(
|
status_download, result_path_file = self.item(
|
||||||
media=media,
|
media=media, file_template=file_name_relative, quality=quality
|
||||||
file_template=file_name_relative,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Advance progress bar.
|
# Advance progress bar.
|
||||||
|
|||||||
+17
-6
@@ -9,6 +9,7 @@ from tidal_dl_ng import __version__, update_available
|
|||||||
from tidal_dl_ng.dialog import DialogLogin, DialogPreferences, DialogVersion
|
from tidal_dl_ng.dialog import DialogLogin, DialogPreferences, DialogVersion
|
||||||
from tidal_dl_ng.helper.gui import (
|
from tidal_dl_ng.helper.gui import (
|
||||||
get_queue_download_media,
|
get_queue_download_media,
|
||||||
|
get_queue_download_quality,
|
||||||
get_results_media_item,
|
get_results_media_item,
|
||||||
get_user_list_media_item,
|
get_user_list_media_item,
|
||||||
set_queue_download_media,
|
set_queue_download_media,
|
||||||
@@ -542,7 +543,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
) -> QueueDownloadItem | bool:
|
) -> QueueDownloadItem | bool:
|
||||||
result: QueueDownloadItem | False
|
result: QueueDownloadItem | False
|
||||||
name: str = ""
|
name: str = ""
|
||||||
quality: str = ""
|
quality: Quality
|
||||||
explicit: str = ""
|
explicit: str = ""
|
||||||
|
|
||||||
# Check if item is available on TIDAL.
|
# Check if item is available on TIDAL.
|
||||||
@@ -798,10 +799,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
result: QueueDownloadStatus
|
result: QueueDownloadStatus
|
||||||
item: QtWidgets.QTreeWidgetItem = items[0]
|
item: QtWidgets.QTreeWidgetItem = items[0]
|
||||||
media: Track | Album | Playlist | Video | Mix | Artist = get_queue_download_media(item)
|
media: Track | Album | Playlist | Video | Mix | Artist = get_queue_download_media(item)
|
||||||
|
quality: Quality = get_queue_download_quality(item)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.s_queue_download_item_downloading.emit(item)
|
self.s_queue_download_item_downloading.emit(item)
|
||||||
result = self.on_queue_download(media)
|
result = self.on_queue_download(media, quality=quality)
|
||||||
|
|
||||||
if result == QueueDownloadStatus.Finished:
|
if result == QueueDownloadStatus.Finished:
|
||||||
self.s_queue_download_item_finished.emit(item)
|
self.s_queue_download_item_finished.emit(item)
|
||||||
@@ -827,7 +829,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
def queue_download_item_status(self, item: QtWidgets.QTreeWidgetItem, status: str) -> None:
|
def queue_download_item_status(self, item: QtWidgets.QTreeWidgetItem, status: str) -> None:
|
||||||
item.setText(0, status)
|
item.setText(0, status)
|
||||||
|
|
||||||
def on_queue_download(self, media: Track | Album | Playlist | Video | Mix | Artist) -> QueueDownloadStatus:
|
def on_queue_download(
|
||||||
|
self, media: Track | Album | Playlist | Video | Mix | Artist, quality: Quality | None = None
|
||||||
|
) -> QueueDownloadStatus:
|
||||||
result: QueueDownloadStatus
|
result: QueueDownloadStatus
|
||||||
items_media: [Track | Album | Playlist | Video | Mix | Artist]
|
items_media: [Track | Album | Playlist | Video | Mix | Artist]
|
||||||
|
|
||||||
@@ -839,12 +843,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
download_delay: bool = bool(isinstance(media, Track | Video) and self.settings.data.download_delay)
|
download_delay: bool = bool(isinstance(media, Track | Video) and self.settings.data.download_delay)
|
||||||
|
|
||||||
for item_media in items_media:
|
for item_media in items_media:
|
||||||
result = self.download(item_media, self.dl, delay_track=download_delay)
|
result = self.download(item_media, self.dl, delay_track=download_delay, quality=quality)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def download(
|
def download(
|
||||||
self, media: Track | Album | Playlist | Video | Mix | Artist, dl: Download, delay_track: bool = False
|
self,
|
||||||
|
media: Track | Album | Playlist | Video | Mix | Artist,
|
||||||
|
dl: Download,
|
||||||
|
delay_track: bool = False,
|
||||||
|
quality: Quality | None = None,
|
||||||
) -> QueueDownloadStatus:
|
) -> QueueDownloadStatus:
|
||||||
result_dl: bool
|
result_dl: bool
|
||||||
path_file: str
|
path_file: str
|
||||||
@@ -855,13 +863,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
file_template = get_format_template(media, self.settings)
|
file_template = get_format_template(media, self.settings)
|
||||||
|
|
||||||
if isinstance(media, Track | Video):
|
if isinstance(media, Track | Video):
|
||||||
result_dl, path_file = dl.item(media=media, file_template=file_template, download_delay=delay_track)
|
result_dl, path_file = dl.item(
|
||||||
|
media=media, file_template=file_template, download_delay=delay_track, quality=quality
|
||||||
|
)
|
||||||
elif isinstance(media, Album | Playlist | Mix):
|
elif isinstance(media, Album | Playlist | Mix):
|
||||||
dl.items(
|
dl.items(
|
||||||
media=media,
|
media=media,
|
||||||
file_template=file_template,
|
file_template=file_template,
|
||||||
video_download=self.settings.data.video_download,
|
video_download=self.settings.data.video_download,
|
||||||
download_delay=self.settings.data.download_delay,
|
download_delay=self.settings.data.download_delay,
|
||||||
|
quality=quality,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Dummy values
|
# Dummy values
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from PySide6 import QtCore, QtWidgets
|
from PySide6 import QtCore, QtWidgets
|
||||||
from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video
|
from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video
|
||||||
from tidalapi.artist import Artist
|
from tidalapi.artist import Artist
|
||||||
|
from tidalapi.media import Quality
|
||||||
|
|
||||||
|
|
||||||
def get_table_data(
|
def get_table_data(
|
||||||
@@ -11,6 +12,12 @@ def get_table_data(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_table_text(item: QtWidgets.QTreeWidgetItem, column: int) -> str:
|
||||||
|
result: str = item.text(column)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_results_media_item(item: QtWidgets.QTreeWidgetItem) -> Track | Video | Album | Artist | Playlist | Mix:
|
def get_results_media_item(item: QtWidgets.QTreeWidgetItem) -> Track | Video | Album | Artist | Playlist | Mix:
|
||||||
result: Track | Video | Album | Artist | Playlist | Mix = get_table_data(item, 1)
|
result: Track | Video | Album | Artist | Playlist | Mix = get_table_data(item, 1)
|
||||||
|
|
||||||
@@ -31,6 +38,14 @@ def get_queue_download_media(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_queue_download_quality(
|
||||||
|
item: QtWidgets.QTreeWidgetItem,
|
||||||
|
) -> Quality:
|
||||||
|
result: Quality = get_table_text(item, 4)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def set_table_data(
|
def set_table_data(
|
||||||
item: QtWidgets.QTreeWidgetItem, data: Track | Video | Album | Artist | Mix | Playlist | UserPlaylist, column: int
|
item: QtWidgets.QTreeWidgetItem, data: Track | Video | Album | Artist | Mix | Playlist | UserPlaylist, column: int
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -154,13 +154,13 @@ def instantiate_media(
|
|||||||
return media
|
return media
|
||||||
|
|
||||||
|
|
||||||
def quality_audio_highest(media: Track | Album) -> str:
|
def quality_audio_highest(media: Track | Album) -> Quality:
|
||||||
quality: str
|
quality: Quality
|
||||||
|
|
||||||
if MediaMetadataTags.hires_lossless in media.media_metadata_tags:
|
if MediaMetadataTags.hires_lossless in media.media_metadata_tags:
|
||||||
quality = Quality.hi_res_lossless
|
quality = Quality.hi_res_lossless
|
||||||
elif MediaMetadataTags.mqa in media.media_metadata_tags:
|
elif MediaMetadataTags.mqa in media.media_metadata_tags:
|
||||||
quality = f"{Quality.hi_res}"
|
quality = Quality.hi_res
|
||||||
elif MediaMetadataTags.lossless in media.media_metadata_tags:
|
elif MediaMetadataTags.lossless in media.media_metadata_tags:
|
||||||
quality = Quality.high_lossless
|
quality = Quality.high_lossless
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from tidalapi.media import Quality
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PySide6 import QtCore
|
from PySide6 import QtCore
|
||||||
|
|
||||||
@@ -39,5 +41,5 @@ class QueueDownloadItem:
|
|||||||
status: str
|
status: str
|
||||||
name: str
|
name: str
|
||||||
type_media: str
|
type_media: str
|
||||||
quality: str
|
quality: Quality
|
||||||
obj: object
|
obj: object
|
||||||
|
|||||||
Reference in New Issue
Block a user