summaryrefslogtreecommitdiff
path: root/tidal_dl_ng
diff options
context:
space:
mode:
Diffstat (limited to 'tidal_dl_ng')
-rw-r--r--tidal_dl_ng/cli.py2
-rw-r--r--tidal_dl_ng/config.py36
-rw-r--r--tidal_dl_ng/constants.py11
-rw-r--r--tidal_dl_ng/dialog.py1
-rw-r--r--tidal_dl_ng/download.py35
-rw-r--r--tidal_dl_ng/gui.py26
-rw-r--r--tidal_dl_ng/model/cfg.py2
-rw-r--r--tidal_dl_ng/ui/dialog_settings.py17
8 files changed, 114 insertions, 16 deletions
diff --git a/tidal_dl_ng/cli.py b/tidal_dl_ng/cli.py
index ce8dc3d..c2b51fa 100644
--- a/tidal_dl_ng/cli.py
+++ b/tidal_dl_ng/cli.py
@@ -246,7 +246,7 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
fn_logger = LoggerWrapped(progress.print)
dl = Download(
- session=ctx.obj[CTX_TIDAL].session,
+ tidal_obj=ctx.obj[CTX_TIDAL],
skip_existing=settings.data.skip_existing,
path_base=settings.data.download_base_path,
fn_logger=fn_logger,
diff --git a/tidal_dl_ng/config.py b/tidal_dl_ng/config.py
index f390e4c..f41f314 100644
--- a/tidal_dl_ng/config.py
+++ b/tidal_dl_ng/config.py
@@ -2,6 +2,7 @@ import json
import os
import shutil
from collections.abc import Callable
+from contextlib import contextmanager
from json import JSONDecodeError
from pathlib import Path
from threading import Event
@@ -9,6 +10,11 @@ from typing import Any
import tidalapi
+from tidal_dl_ng.constants import (
+ ATMOS_CLIENT_ID,
+ ATMOS_CLIENT_SECRET,
+ ATMOS_REQUEST_QUALITY,
+)
from tidal_dl_ng.helper.decorator import SingletonMeta
from tidal_dl_ng.helper.path import path_config_base, path_file_settings, path_file_token
from tidal_dl_ng.model.cfg import Settings as ModelSettings
@@ -110,7 +116,7 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
if settings:
self.settings = settings
- self.session.audio_quality = self.settings.data.quality_audio
+ self.session.audio_quality = tidalapi.Quality(self.settings.data.quality_audio)
self.session.video_quality = tidalapi.VideoQuality.high
return True
@@ -156,6 +162,34 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
self.set_option("expiry_time", self.session.expiry_time)
self.save()
+ @contextmanager
+ def atmos_session_context(self):
+
+ if not self.session.check_login():
+ print("Not logged in.")
+
+ original_client_id = self.session.config.client_id
+ original_client_secret = self.session.config.client_secret
+ original_audio_quality = self.session.audio_quality
+
+ try:
+ self.session.config.client_id = ATMOS_CLIENT_ID
+ self.session.config.client_secret = ATMOS_CLIENT_SECRET
+ self.session.audio_quality = ATMOS_REQUEST_QUALITY
+
+ if not self.login_token(do_pkce=self.is_pkce):
+ print("Warning: Session restore failed.")
+
+ yield
+
+ finally:
+ self.session.config.client_id = original_client_id
+ self.session.config.client_secret = original_client_secret
+ self.session.audio_quality = original_audio_quality
+
+ if not self.login_token(do_pkce=self.is_pkce):
+ print("Warning: Restoring the original session context failed. Please restart the application.")
+
def login(self, fn_print: Callable) -> bool:
is_token = self.login_token()
result = False
diff --git a/tidal_dl_ng/constants.py b/tidal_dl_ng/constants.py
index f2f774b..79ae501 100644
--- a/tidal_dl_ng/constants.py
+++ b/tidal_dl_ng/constants.py
@@ -1,5 +1,8 @@
+import base64
from enum import StrEnum
+from tidalapi import Quality
+
CTX_TIDAL: str = "tidal"
REQUESTS_TIMEOUT_SEC: int = 45
EXTENSION_LYRICS: str = ".lrc"
@@ -15,6 +18,14 @@ FILENAME_LENGTH_MAX: int = 255
FORMAT_TEMPLATE_EXPLICIT: str = " (Explicit)"
METADATA_EXPLICIT: str = " 🅴"
+# Dolby Atmos API credentials (obfuscated)
+ATMOS_ID_B64 = "N203QX" + "AwSkM5aj" + "FjT00zbg=="
+ATMOS_SECRET_B64 = "dlJBZEEx" + "MDh0bHZrSnB" + "Uc0daUzhyR1" + "o3eFRsYkow" + "cWFaMks5c2F" + "FenNnWT0="
+
+ATMOS_CLIENT_ID = base64.b64decode(ATMOS_ID_B64).decode("utf-8")
+ATMOS_CLIENT_SECRET = base64.b64decode(ATMOS_SECRET_B64).decode("utf-8")
+ATMOS_REQUEST_QUALITY = Quality.low_320k
+
class QualityVideo(StrEnum):
P360 = "360"
diff --git a/tidal_dl_ng/dialog.py b/tidal_dl_ng/dialog.py
index 0bf9b2a..8cb93f6 100644
--- a/tidal_dl_ng/dialog.py
+++ b/tidal_dl_ng/dialog.py
@@ -177,6 +177,7 @@ class DialogPreferences(QtWidgets.QDialog):
"lyrics_file",
"use_primary_album_artist",
"video_download",
+ "download_dolby_atmos",
"download_delay",
"video_convert_mp4",
"extract_flac",
diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py
index bcb9d4c..829d23f 100644
--- a/tidal_dl_ng/download.py
+++ b/tidal_dl_ng/download.py
@@ -28,9 +28,17 @@ from requests.exceptions import HTTPError
from rich.progress import Progress, TaskID
from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video
from tidalapi.exceptions import TooManyRequests
-from tidalapi.media import AudioExtensions, Codec, Quality, Stream, StreamManifest, VideoExtensions
+from tidalapi.media import (
+ AudioExtensions,
+ AudioMode,
+ Codec,
+ Quality,
+ Stream,
+ StreamManifest,
+ VideoExtensions,
+)
-from tidal_dl_ng.config import Settings
+from tidal_dl_ng.config import Settings, Tidal
from tidal_dl_ng.constants import (
CHUNK_SIZE,
COVER_NAME,
@@ -99,6 +107,7 @@ class Download:
"""Main class for managing downloads, segment merging, file operations, and metadata for TIDAL media."""
settings: Settings
+ tidal: "Tidal"
session: Session
skip_existing: bool = False
fn_logger: Callable
@@ -110,7 +119,7 @@ class Download:
def __init__(
self,
- session: Session,
+ tidal_obj: Tidal, # Required for Atmos session context manager
path_base: str,
fn_logger: Callable,
skip_existing: bool = False,
@@ -123,7 +132,9 @@ class Download:
"""Initialize the Download object and its dependencies.
Args:
- session (Session): TIDAL session object.
+ tidal_obj (Tidal): TIDAL configuration object. Required for:
+ - session: Main TIDAL API session
+ - atmos_session_context(): Dolby Atmos credential switching
path_base (str): Base path for downloads.
fn_logger (Callable): Logger function or object.
skip_existing (bool, optional): Whether to skip existing files. Defaults to False.
@@ -134,7 +145,8 @@ class Download:
event_run (Event | None, optional): Run event. Defaults to None.
"""
self.settings = Settings()
- self.session = session
+ self.tidal = tidal_obj
+ self.session = tidal_obj.session
self.skip_existing = skip_existing
self.fn_logger = fn_logger
self.progress_gui = progress_gui
@@ -785,8 +797,19 @@ class Download:
if isinstance(media, Track):
try:
- media_stream = media.get_stream()
+ if (
+ self.settings.data.download_dolby_atmos
+ and hasattr(media, "audio_modes")
+ and AudioMode.dolby_atmos.value in media.audio_modes
+ ):
+ with self.tidal.atmos_session_context():
+ atmos_track = self.session.track(media.id)
+ media_stream = atmos_track.get_stream()
+ else:
+ media_stream = media.get_stream()
+
stream_manifest = media_stream.get_stream_manifest()
+
except TooManyRequests:
self.fn_logger.exception(
f"Too many requests against TIDAL backend. Skipping '{name_builder_item(media)}'. "
diff --git a/tidal_dl_ng/gui.py b/tidal_dl_ng/gui.py
index 74192f0..fb8b6c8 100644
--- a/tidal_dl_ng/gui.py
+++ b/tidal_dl_ng/gui.py
@@ -89,10 +89,12 @@ except ImportError as e:
print("Qt dependencies missing. Cannot start GUI. Please read the 'README.md' carefully.")
sys.exit(1)
-import coloredlogs.converter
+
+from ansi2html import Ansi2HTMLConverter
from rich.progress import Progress
from tidalapi import Album, Mix, Playlist, Quality, Track, UserPlaylist, Video
from tidalapi.artist import Artist
+from tidalapi.media import AudioMode
from tidalapi.session import SearchTypes
from tidal_dl_ng.config import HandlingApp, Settings, Tidal
@@ -143,6 +145,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
s_queue_download_item_finished: QtCore.Signal = QtCore.Signal(object)
s_queue_download_item_failed: QtCore.Signal = QtCore.Signal(object)
s_queue_download_item_skipped: QtCore.Signal = QtCore.Signal(object)
+ converter_ansi_html: Ansi2HTMLConverter
def __init__(self, tidal: Tidal | None = None) -> None:
"""Initialize the main window and all components.
@@ -187,6 +190,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.settings.data.window_h,
)
self.spinners: dict[QtWidgets.QWidget, QtWaitingSpinner] = {}
+ self.converter_ansi_html: Ansi2HTMLConverter = Ansi2HTMLConverter()
def init_tidal(self, tidal: Tidal | None = None):
"""Initialize Tidal session and handle login flow.
@@ -251,7 +255,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
progress: Progress = Progress()
handling_app: HandlingApp = HandlingApp()
self.dl = Download(
- session=self.tidal.session,
+ tidal_obj=self.tidal,
skip_existing=self.tidal.settings.data.skip_existing,
path_base=self.settings.data.download_base_path,
fn_logger=logger_gui,
@@ -297,12 +301,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Args:
text (str): Log message.
"""
- display_msg = coloredlogs.converter.convert(text)
-
cursor: QtGui.QTextCursor = self.te_debug.textCursor()
- cursor.movePosition(QtGui.QTextCursor.End)
- cursor.insertHtml(display_msg)
+ html = self.converter_ansi_html.convert(text)
+ cursor.movePosition(QtGui.QTextCursor.End)
+ cursor.insertHtml(html)
self.te_debug.setTextCursor(cursor)
self.te_debug.ensureCursorVisible()
@@ -1088,6 +1091,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Returns:
ResultItem: The constructed ResultItem.
"""
+
+ final_quality = quality_audio_highest(item)
+ if hasattr(item, "audio_modes") and AudioMode.dolby_atmos.value in item.audio_modes:
+ final_quality = f"{final_quality} / Dolby Atmos"
+
return ResultItem(
position=idx,
artist=name_builder_artist(item),
@@ -1095,7 +1103,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
album=item.album.name,
duration_sec=item.duration,
obj=item,
- quality=quality_audio_highest(item),
+ quality=final_quality,
explicit=bool(item.explicit),
date_user_added=date_user_added,
date_release=date_release,
@@ -1393,7 +1401,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Args:
index: The index of the selected quality in the combo box.
"""
- self.settings.data.quality_audio = Quality(self.cb_quality_audio.itemData(index))
+ quality_data = self.cb_quality_audio.itemData(index)
+
+ self.settings.data.quality_audio = Quality(quality_data)
self.settings.save()
if self.tidal:
diff --git a/tidal_dl_ng/model/cfg.py b/tidal_dl_ng/model/cfg.py
index 4101b9d..b43acc1 100644
--- a/tidal_dl_ng/model/cfg.py
+++ b/tidal_dl_ng/model/cfg.py
@@ -26,6 +26,7 @@ class Settings:
download_base_path: str = "~/download"
quality_audio: Quality = Quality.low_320k
quality_video: QualityVideo = QualityVideo.P480
+ download_dolby_atmos: bool = False
format_album: str = (
"Albums/{album_artist} - {album_title}{album_explicit}/{track_volume_num_optional}"
"{album_track_num}. {artist_name} - {track_title}{album_explicit}"
@@ -84,6 +85,7 @@ class HelpSettings:
'"HI_RES_LOSSLESS" (up to 24 Bit, 192 kHz)'
)
quality_video: str = 'Desired video download quality: "360", "480", "720", "1080"'
+ download_dolby_atmos: str = "Download Dolby Atmos audio streams if available."
# TODO: Describe possible variables.
format_album: str = "Where to download albums and how to name the items."
format_playlist: str = "Where to download playlists and how to name the items."
diff --git a/tidal_dl_ng/ui/dialog_settings.py b/tidal_dl_ng/ui/dialog_settings.py
index ebdbcd4..8ab6092 100644
--- a/tidal_dl_ng/ui/dialog_settings.py
+++ b/tidal_dl_ng/ui/dialog_settings.py
@@ -212,6 +212,23 @@ class Ui_DialogSettings:
self.lv_flags.addLayout(self.horizontalLayout_13)
+ self.lh_flags_atmos = QHBoxLayout()
+ self.lh_flags_atmos.setObjectName("lh_flags_atmos")
+ self.lv_flag_download_dolby_atmos = QVBoxLayout()
+ self.lv_flag_download_dolby_atmos.setObjectName("lv_flag_download_dolby_atmos")
+ self.cb_download_dolby_atmos = QCheckBox(self.gb_flags)
+ self.cb_download_dolby_atmos.setObjectName("cb_download_dolby_atmos")
+
+ self.lv_flag_download_dolby_atmos.addWidget(self.cb_download_dolby_atmos)
+
+ self.lh_flags_atmos.addLayout(self.lv_flag_download_dolby_atmos)
+
+ self.lv_flag_empty_atmos = QVBoxLayout()
+ self.lv_flag_empty_atmos.setObjectName("lv_flag_empty_atmos")
+ self.lh_flags_atmos.addLayout(self.lv_flag_empty_atmos)
+
+ self.lv_flags.addLayout(self.lh_flags_atmos)
+
self.lv_main.addWidget(self.gb_flags)
self.gb_choices = QGroupBox(DialogSettings)