summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/master.yml2
-rw-r--r--poetry.lock115
-rw-r--r--pyproject.toml12
-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
-rw-r--r--tox.ini3
12 files changed, 192 insertions, 70 deletions
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 44e02e9..c86f452 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
- python-version: ["3.12"]
+ python-version: ["3.12", "3.13"]
fail-fast: false
steps:
- name: Check out
diff --git a/poetry.lock b/poetry.lock
index 7972e3e..4d608b3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,6 +1,22 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
+name = "ansi2html"
+version = "1.9.2"
+description = "Convert text with ANSI color codes to HTML or to LaTeX"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "ansi2html-1.9.2-py3-none-any.whl", hash = "sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5"},
+ {file = "ansi2html-1.9.2.tar.gz", hash = "sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5"},
+]
+
+[package.extras]
+docs = ["mkdocs", "mkdocs-material", "mkdocs-material-extensions", "mkdocstrings", "mkdocstrings-python", "pymdown-extensions"]
+test = ["pytest", "pytest-cov"]
+
+[[package]]
name = "astroid"
version = "4.0.1"
description = "An abstract syntax tree for Python with inference support."
@@ -633,14 +649,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2
[[package]]
name = "iniconfig"
-version = "2.1.0"
+version = "2.3.0"
description = "brain-dead simple config-ini parsing"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
groups = ["dev"]
files = [
- {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
- {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
+ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
+ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
[[package]]
@@ -1160,15 +1176,14 @@ files = [
]
[[package]]
-name = "nuitka"
-version = "2.8.1"
+name = "Nuitka"
+version = "2.8.4"
description = "Python compiler with full language support and CPython compatibility"
optional = false
python-versions = "*"
groups = ["dev"]
-files = [
- {file = "Nuitka-2.8.1.tar.gz", hash = "sha256:456fd55c25292252f2dfb26da2c1ee0c0dbeb00106669979b8a2a1f3ae653cb0"},
-]
+files = []
+develop = false
[package.dependencies]
ordered-set = ">=4.1.0"
@@ -1180,6 +1195,12 @@ build-wheel = ["setuptools (>=42)", "toml", "wheel"]
icon-conversion = ["imageio"]
onefile = ["zstandard (>=0.15)"]
+[package.source]
+type = "git"
+url = "https://github.com/Nuitka/Nuitka.git"
+reference = "2.8.4"
+resolved_reference = "fa3c20ac628e9652f58cff9558ae066c7a500ca0"
+
[[package]]
name = "ordered-set"
version = "4.1.0"
@@ -1528,14 +1549,14 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pylint"
-version = "4.0.1"
+version = "4.0.2"
description = "python code static checker"
optional = false
python-versions = ">=3.10.0"
groups = ["dev"]
files = [
- {file = "pylint-4.0.1-py3-none-any.whl", hash = "sha256:6077ac21d01b7361eae6ed0f38d9024c02732fdc635d9e154d4fe6063af8ac56"},
- {file = "pylint-4.0.1.tar.gz", hash = "sha256:06db6a1fda3cedbd7aee58f09d241e40e5f14b382fd035ed97be320f11728a84"},
+ {file = "pylint-4.0.2-py3-none-any.whl", hash = "sha256:9627ccd129893fb8ee8e8010261cb13485daca83e61a6f854a85528ee579502d"},
+ {file = "pylint-4.0.2.tar.gz", hash = "sha256:9c22dfa52781d3b79ce86ab2463940f874921a3e5707bcfc98dd0c019945014e"},
]
[package.dependencies]
@@ -1623,63 +1644,63 @@ dev = ["build", "flake8", "mypy", "pytest", "twine"]
[[package]]
name = "pyside6"
-version = "6.9.0"
+version = "6.10.0"
description = "Python bindings for the Qt cross-platform application and UI framework"
optional = true
python-versions = "<3.14,>=3.9"
groups = ["main"]
markers = "extra == \"gui\""
files = [
- {file = "PySide6-6.9.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:b8f286a1bd143f3b2bdf08367b9362b13f469d26986c25700af9c4c68f79213e"},
- {file = "PySide6-6.9.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:09239d1b808f18efccd3803db874d683917efcdebfdf0e8dec449cf50e74e7aa"},
- {file = "PySide6-6.9.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:1a176409dd0dd12b72d2c78b776e5051f569071ec52b7aaadd0a5b3333493c24"},
- {file = "PySide6-6.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:0103e5d161696db40d75bfbf4e4b7d4f3372903c1b400c4e3379377b62c50290"},
- {file = "PySide6-6.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:846fbccf0b3501eb31cf0791a46e137615efba6ce540da2b426d79fa3e7762c4"},
+ {file = "pyside6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0"},
+ {file = "pyside6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01"},
+ {file = "pyside6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820"},
+ {file = "pyside6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f"},
+ {file = "pyside6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339"},
]
[package.dependencies]
-PySide6-Addons = "6.9.0"
-PySide6-Essentials = "6.9.0"
-shiboken6 = "6.9.0"
+PySide6_Addons = "6.10.0"
+PySide6_Essentials = "6.10.0"
+shiboken6 = "6.10.0"
[[package]]
name = "pyside6-addons"
-version = "6.9.0"
+version = "6.10.0"
description = "Python bindings for the Qt cross-platform application and UI framework (Addons)"
optional = true
python-versions = "<3.14,>=3.9"
groups = ["main"]
markers = "extra == \"gui\""
files = [
- {file = "PySide6_Addons-6.9.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:98f9ad4b65820736e12d49c18db2e570eac63727407fbb59a62ac753e89dc201"},
- {file = "PySide6_Addons-6.9.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fc9dcd63a0ce7565f238cb11c44494435a50eb6cb72b8dbce3b709618989c3dc"},
- {file = "PySide6_Addons-6.9.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:d8a650644e0b9d1e7a092f6bcd11f25a63706d12f77d442b6ace75d346ab5d30"},
- {file = "PySide6_Addons-6.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:8cf54065b3d1b4698448fad825378a25c10ef52017d9dff48cead03200636d8d"},
- {file = "PySide6_Addons-6.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:260a56da59539f476c1635a3ff13591e10f1b04d92155c0617129bc53ca8b5f8"},
+ {file = "pyside6_addons-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c"},
+ {file = "pyside6_addons-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464"},
+ {file = "pyside6_addons-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0"},
+ {file = "pyside6_addons-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859"},
+ {file = "pyside6_addons-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1"},
]
[package.dependencies]
-PySide6-Essentials = "6.9.0"
-shiboken6 = "6.9.0"
+PySide6_Essentials = "6.10.0"
+shiboken6 = "6.10.0"
[[package]]
name = "pyside6-essentials"
-version = "6.9.0"
+version = "6.10.0"
description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)"
optional = true
python-versions = "<3.14,>=3.9"
groups = ["main"]
markers = "extra == \"gui\""
files = [
- {file = "PySide6_Essentials-6.9.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:b18e3e01b507e8a57481fe19792eb373d5f10a23a50702ce540da1435e722f39"},
- {file = "PySide6_Essentials-6.9.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:45eaf7f17688d1991f39680dbfd3c41674f3cbb78f278aa10fe0b5f2f31c1989"},
- {file = "PySide6_Essentials-6.9.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:69aedfad77119c5bec0005ca31d5620e9bac8ba5ae66c7389160530cfd698ed8"},
- {file = "PySide6_Essentials-6.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:94a0096d6bb1d3e5cef29ca4a5366d0f229d42480fbb17aa25ad85d72b1b7947"},
- {file = "PySide6_Essentials-6.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:d2dc45536f2269ad111991042e81257124f1cd1c9ed5ea778d7224fd65dc9e2b"},
+ {file = "pyside6_essentials-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a"},
+ {file = "pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8"},
+ {file = "pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998"},
+ {file = "pyside6_essentials-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe"},
+ {file = "pyside6_essentials-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1"},
]
[package.dependencies]
-shiboken6 = "6.9.0"
+shiboken6 = "6.10.0"
[[package]]
name = "pytest"
@@ -1963,18 +1984,18 @@ files = [
[[package]]
name = "shiboken6"
-version = "6.9.0"
+version = "6.10.0"
description = "Python/C++ bindings helper module"
optional = true
python-versions = "<3.14,>=3.9"
groups = ["main"]
markers = "extra == \"gui\""
files = [
- {file = "shiboken6-6.9.0-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:c4d8e3a5907154ac4789e52c77957db95bcf584238c244d7743cb39e9b66dd26"},
- {file = "shiboken6-6.9.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3f585caae5b814a7e23308db0a077355a7dc20c34d58ca4c339ff7625e9a1936"},
- {file = "shiboken6-6.9.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b61579b90bf9c53ecc174085a69429166dfe57a0b8b894f933d1281af9df6568"},
- {file = "shiboken6-6.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:121ea290ed1afa5ad6abf690b377612693436292b69c61b0f8e10b1f0850f935"},
- {file = "shiboken6-6.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:24f53857458881b54798d7e35704611d07f6b6885bcdf80f13a4c8bb485b8df2"},
+ {file = "shiboken6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543"},
+ {file = "shiboken6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234"},
+ {file = "shiboken6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148"},
+ {file = "shiboken6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717"},
+ {file = "shiboken6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61"},
]
[[package]]
@@ -2095,14 +2116,14 @@ virtualenv = ">=20.34"
[[package]]
name = "typer"
-version = "0.19.2"
+version = "0.20.0"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9"},
- {file = "typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca"},
+ {file = "typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a"},
+ {file = "typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37"},
]
[package.dependencies]
@@ -2338,5 +2359,5 @@ gui = ["pyqtdarktheme-fork", "pyside6"]
[metadata]
lock-version = "2.1"
-python-versions = ">=3.12,<3.13"
-content-hash = "b4016a2fe1c82a9257429d437ae13bbd4fafd97ac6f120152e1a03209d0e70b0"
+python-versions = ">=3.12,<3.14"
+content-hash = "73a9195e1d46d0176c01c731c1431d4b43085376485622d33e0efd53c42e2fb1"
diff --git a/pyproject.toml b/pyproject.toml
index 0a5edae..37434af 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,8 +11,9 @@ classifiers = [
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
]
-requires-python = ">=3.12,<3.13"
+requires-python = ">=3.12,<3.14"
dynamic = ["dependencies"]
[project.urls]
@@ -35,14 +36,15 @@ dataclasses-json = "^0.6.7"
pathvalidate = "^3.3.1"
m3u8 = "^6.0.0"
coloredlogs = "^15.0.1"
-pyside6 = { version = "6.9.0", optional = true }
+pyside6 = { version = "6.10.0", optional = true }
pyqtdarktheme-fork = { version = "^2.3.4", optional = true }
rich = "^14.2.0"
toml = "^0.10.2"
-typer = "^0.19.2"
+typer = "^0.20.0"
tidalapi = "^0.8.8"
python-ffmpeg = "^2.0.12"
pycryptodome = "^3.23.0"
+ansi2html = "^1.9.2"
[project.optional-dependencies]
gui = ["pyside6", "pyqtdarktheme-fork"]
@@ -66,7 +68,7 @@ pylint = "^4.0.1"
pillow = "^12.0.0"
pytest-cov = "^7.0.0"
black = "^25.9.0"
-nuitka = "^2.7.11"
+nuitka = { git = "https://github.com/Nuitka/Nuitka.git", tag = "2.8.4" }
[tool.poetry.group.docs]
optional = true
@@ -140,7 +142,7 @@ ignore = [
[tool.black]
# https://github.com/psf/black
-target-version = ["py312"]
+target-version = ["py312", "py313"]
line-length = 120
color = true
preview = true
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)
diff --git a/tox.ini b/tox.ini
index 7dffa3f..e156596 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,11 @@
[tox]
skipsdist = true
-envlist = py312
+envlist = py312,py313
[gh-actions]
python =
3.12: py312
+ 3.13: py313
[testenv]
passenv = PYTHON_VERSION