diff options
| -rw-r--r-- | .github/workflows/master.yml | 2 | ||||
| -rw-r--r-- | poetry.lock | 115 | ||||
| -rw-r--r-- | pyproject.toml | 12 | ||||
| -rw-r--r-- | tidal_dl_ng/cli.py | 2 | ||||
| -rw-r--r-- | tidal_dl_ng/config.py | 36 | ||||
| -rw-r--r-- | tidal_dl_ng/constants.py | 11 | ||||
| -rw-r--r-- | tidal_dl_ng/dialog.py | 1 | ||||
| -rw-r--r-- | tidal_dl_ng/download.py | 35 | ||||
| -rw-r--r-- | tidal_dl_ng/gui.py | 26 | ||||
| -rw-r--r-- | tidal_dl_ng/model/cfg.py | 2 | ||||
| -rw-r--r-- | tidal_dl_ng/ui/dialog_settings.py | 17 | ||||
| -rw-r--r-- | tox.ini | 3 |
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) @@ -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 |
