summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorexislow <exislow@users.noreply.github.com>2025-10-22 21:34:02 +0200
committerGitHub <noreply@github.com>2025-10-22 21:34:02 +0200
commit0cf3516fb054c312c504d45ca5fe60caf34f9a21 (patch)
tree9ff8e25ce03a5799a706e0f5c92050fdb1e6af93
parenta71bc18ca61c4d8c4e2570c4696ec78f38930ba4 (diff)
parente266149684baf6e49462df6d758c6a5714dbfc05 (diff)
downloadtidal-dl-ng-0cf3516fb054c312c504d45ca5fe60caf34f9a21.tar.gz
Merge pull request #573 from Rikrdoga/Dolby_Atmos
Implement Dolby Atmos Support & API Key Selection
-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.py14
-rw-r--r--tidal_dl_ng/model/cfg.py2
-rw-r--r--tidal_dl_ng/ui/dialog_settings.py17
8 files changed, 107 insertions, 11 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 72b9f93..28ea58b 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 dcedf2e..0c55e95 100644
--- a/tidal_dl_ng/dialog.py
+++ b/tidal_dl_ng/dialog.py
@@ -176,6 +176,7 @@ class DialogPreferences(QtWidgets.QDialog):
"lyrics_embed",
"lyrics_file",
"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 1735793..7d855db 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
@@ -783,8 +795,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 9ffc444..9c3feb9 100644
--- a/tidal_dl_ng/gui.py
+++ b/tidal_dl_ng/gui.py
@@ -94,6 +94,7 @@ 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
@@ -254,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,
@@ -887,6 +888,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),
@@ -894,7 +900,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,
@@ -1191,7 +1197,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 44db92f..a376f2f 100644
--- a/tidal_dl_ng/model/cfg.py
+++ b/tidal_dl_ng/model/cfg.py
@@ -23,6 +23,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}"
@@ -77,6 +78,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 9ec2738..f05adef 100644
--- a/tidal_dl_ng/ui/dialog_settings.py
+++ b/tidal_dl_ng/ui/dialog_settings.py
@@ -198,6 +198,23 @@ class Ui_DialogSettings:
self.lv_flags.addLayout(self.horizontalLayout_12)
+ 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)