✨ 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:
Robert Honz
2024-04-21 15:58:31 +02:00
parent 1224e42689
commit f9862e0646
6 changed files with 73 additions and 16 deletions
+8 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+15
View File
@@ -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
): ):
+3 -3
View File
@@ -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:
+3 -1
View File
@@ -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