Merge pull request #573 from Rikrdoga/Dolby_Atmos
Implement Dolby Atmos Support & API Key Selection
This commit is contained in:
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user