Merge pull request #573 from Rikrdoga/Dolby_Atmos

Implement Dolby Atmos Support & API Key Selection
This commit is contained in:
exislow
2025-10-22 21:34:02 +02:00
committed by GitHub
8 changed files with 107 additions and 11 deletions
+1 -1
View File
@@ -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,
+35 -1
View File
@@ -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
+11
View File
@@ -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"
+1
View File
@@ -176,6 +176,7 @@ class DialogPreferences(QtWidgets.QDialog):
"lyrics_embed",
"lyrics_file",
"video_download",
"download_dolby_atmos",
"download_delay",
"video_convert_mp4",
"extract_flac",
+28 -5
View File
@@ -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:
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)}'. "
+11 -3
View File
@@ -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:
+2
View File
@@ -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."
+17
View File
@@ -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)