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) fn_logger = LoggerWrapped(progress.print)
dl = Download( dl = Download(
session=ctx.obj[CTX_TIDAL].session, tidal_obj=ctx.obj[CTX_TIDAL],
skip_existing=settings.data.skip_existing, skip_existing=settings.data.skip_existing,
path_base=settings.data.download_base_path, path_base=settings.data.download_base_path,
fn_logger=fn_logger, fn_logger=fn_logger,
+35 -1
View File
@@ -2,6 +2,7 @@ import json
import os import os
import shutil import shutil
from collections.abc import Callable from collections.abc import Callable
from contextlib import contextmanager
from json import JSONDecodeError from json import JSONDecodeError
from pathlib import Path from pathlib import Path
from threading import Event from threading import Event
@@ -9,6 +10,11 @@ from typing import Any
import tidalapi 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.decorator import SingletonMeta
from tidal_dl_ng.helper.path import path_config_base, path_file_settings, path_file_token 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 from tidal_dl_ng.model.cfg import Settings as ModelSettings
@@ -110,7 +116,7 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
if settings: if settings:
self.settings = 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 self.session.video_quality = tidalapi.VideoQuality.high
return True return True
@@ -156,6 +162,34 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
self.set_option("expiry_time", self.session.expiry_time) self.set_option("expiry_time", self.session.expiry_time)
self.save() 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: def login(self, fn_print: Callable) -> bool:
is_token = self.login_token() is_token = self.login_token()
result = False result = False
+11
View File
@@ -1,5 +1,8 @@
import base64
from enum import StrEnum from enum import StrEnum
from tidalapi import Quality
CTX_TIDAL: str = "tidal" CTX_TIDAL: str = "tidal"
REQUESTS_TIMEOUT_SEC: int = 45 REQUESTS_TIMEOUT_SEC: int = 45
EXTENSION_LYRICS: str = ".lrc" EXTENSION_LYRICS: str = ".lrc"
@@ -15,6 +18,14 @@ FILENAME_LENGTH_MAX: int = 255
FORMAT_TEMPLATE_EXPLICIT: str = " (Explicit)" FORMAT_TEMPLATE_EXPLICIT: str = " (Explicit)"
METADATA_EXPLICIT: str = " 🅴" 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): class QualityVideo(StrEnum):
P360 = "360" P360 = "360"
+1
View File
@@ -176,6 +176,7 @@ class DialogPreferences(QtWidgets.QDialog):
"lyrics_embed", "lyrics_embed",
"lyrics_file", "lyrics_file",
"video_download", "video_download",
"download_dolby_atmos",
"download_delay", "download_delay",
"video_convert_mp4", "video_convert_mp4",
"extract_flac", "extract_flac",
+28 -5
View File
@@ -28,9 +28,17 @@ 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.exceptions import TooManyRequests 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 ( from tidal_dl_ng.constants import (
CHUNK_SIZE, CHUNK_SIZE,
COVER_NAME, COVER_NAME,
@@ -99,6 +107,7 @@ class Download:
"""Main class for managing downloads, segment merging, file operations, and metadata for TIDAL media.""" """Main class for managing downloads, segment merging, file operations, and metadata for TIDAL media."""
settings: Settings settings: Settings
tidal: "Tidal"
session: Session session: Session
skip_existing: bool = False skip_existing: bool = False
fn_logger: Callable fn_logger: Callable
@@ -110,7 +119,7 @@ class Download:
def __init__( def __init__(
self, self,
session: Session, tidal_obj: Tidal, # Required for Atmos session context manager
path_base: str, path_base: str,
fn_logger: Callable, fn_logger: Callable,
skip_existing: bool = False, skip_existing: bool = False,
@@ -123,7 +132,9 @@ class Download:
"""Initialize the Download object and its dependencies. """Initialize the Download object and its dependencies.
Args: 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. path_base (str): Base path for downloads.
fn_logger (Callable): Logger function or object. fn_logger (Callable): Logger function or object.
skip_existing (bool, optional): Whether to skip existing files. Defaults to False. 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. event_run (Event | None, optional): Run event. Defaults to None.
""" """
self.settings = Settings() self.settings = Settings()
self.session = session self.tidal = tidal_obj
self.session = tidal_obj.session
self.skip_existing = skip_existing self.skip_existing = skip_existing
self.fn_logger = fn_logger self.fn_logger = fn_logger
self.progress_gui = progress_gui self.progress_gui = progress_gui
@@ -783,8 +795,19 @@ class Download:
if isinstance(media, Track): if isinstance(media, Track):
try: 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() media_stream = media.get_stream()
stream_manifest = media_stream.get_stream_manifest() stream_manifest = media_stream.get_stream_manifest()
except TooManyRequests: except TooManyRequests:
self.fn_logger.exception( self.fn_logger.exception(
f"Too many requests against TIDAL backend. Skipping '{name_builder_item(media)}'. " 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 rich.progress import Progress
from tidalapi import Album, Mix, Playlist, Quality, Track, UserPlaylist, Video from tidalapi import Album, Mix, Playlist, Quality, Track, UserPlaylist, Video
from tidalapi.artist import Artist from tidalapi.artist import Artist
from tidalapi.media import AudioMode
from tidalapi.session import SearchTypes from tidalapi.session import SearchTypes
from tidal_dl_ng.config import HandlingApp, Settings, Tidal from tidal_dl_ng.config import HandlingApp, Settings, Tidal
@@ -254,7 +255,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
progress: Progress = Progress() progress: Progress = Progress()
handling_app: HandlingApp = HandlingApp() handling_app: HandlingApp = HandlingApp()
self.dl = Download( self.dl = Download(
session=self.tidal.session, tidal_obj=self.tidal,
skip_existing=self.tidal.settings.data.skip_existing, skip_existing=self.tidal.settings.data.skip_existing,
path_base=self.settings.data.download_base_path, path_base=self.settings.data.download_base_path,
fn_logger=logger_gui, fn_logger=logger_gui,
@@ -887,6 +888,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Returns: Returns:
ResultItem: The constructed ResultItem. 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( return ResultItem(
position=idx, position=idx,
artist=name_builder_artist(item), artist=name_builder_artist(item),
@@ -894,7 +900,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
album=item.album.name, album=item.album.name,
duration_sec=item.duration, duration_sec=item.duration,
obj=item, obj=item,
quality=quality_audio_highest(item), quality=final_quality,
explicit=bool(item.explicit), explicit=bool(item.explicit),
date_user_added=date_user_added, date_user_added=date_user_added,
date_release=date_release, date_release=date_release,
@@ -1191,7 +1197,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Args: Args:
index: The index of the selected quality in the combo box. 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() self.settings.save()
if self.tidal: if self.tidal:
+2
View File
@@ -23,6 +23,7 @@ class Settings:
download_base_path: str = "~/download" download_base_path: str = "~/download"
quality_audio: Quality = Quality.low_320k quality_audio: Quality = Quality.low_320k
quality_video: QualityVideo = QualityVideo.P480 quality_video: QualityVideo = QualityVideo.P480
download_dolby_atmos: bool = False
format_album: str = ( format_album: str = (
"Albums/{album_artist} - {album_title}{album_explicit}/{track_volume_num_optional}" "Albums/{album_artist} - {album_title}{album_explicit}/{track_volume_num_optional}"
"{album_track_num}. {artist_name} - {track_title}{album_explicit}" "{album_track_num}. {artist_name} - {track_title}{album_explicit}"
@@ -77,6 +78,7 @@ class HelpSettings:
'"HI_RES_LOSSLESS" (up to 24 Bit, 192 kHz)' '"HI_RES_LOSSLESS" (up to 24 Bit, 192 kHz)'
) )
quality_video: str = 'Desired video download quality: "360", "480", "720", "1080"' 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. # TODO: Describe possible variables.
format_album: str = "Where to download albums and how to name the items." 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." 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.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.lv_main.addWidget(self.gb_flags)
self.gb_choices = QGroupBox(DialogSettings) self.gb_choices = QGroupBox(DialogSettings)