From ca0d73716ad9fb1b0dfd1b97ed989579445ca88b Mon Sep 17 00:00:00 2001 From: tehkillerbee Date: Thu, 11 Dec 2025 21:45:31 +0100 Subject: [PATCH 1/9] Version bump --- HISTORY.rst | 6 ++++++ docs/conf.py | 2 +- pyproject.toml | 2 +- tidalapi/__init__.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index de2ef3e8..536db06f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ History ======= +v0.8.10 +-------- +* Tests: Updated misc. tests, added pagination tests - tehkillerbee_ +* Bugfix: Final chunk count must not exceed maximum number of playlists (Fixes #389) - tehkillerbee_ +* Bugfix: Fix track version if empty (Fixes #388) - tehkillerbee_ + v0.8.9 -------- * Bugfix: Return correct Exception, depending on status_code (404, 429). Add missing Raise, comments. Fixes #385 - tehkillerbee_ diff --git a/docs/conf.py b/docs/conf.py index f050bc1f..cf471276 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ author = "The tidalapi Developers" # The full version, including alpha/beta/rc tags -release = "0.8.9" +release = "0.8.10" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 97fcd5e1..024a7af9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tidalapi" -version = "0.8.9" +version = "0.8.10" description = "Unofficial API for TIDAL music streaming service." authors = ["Thomas Amland "] maintainers = ["tehkillerbee "] diff --git a/tidalapi/__init__.py b/tidalapi/__init__.py index c2cbb696..eac49a07 100644 --- a/tidalapi/__init__.py +++ b/tidalapi/__init__.py @@ -17,4 +17,4 @@ User, ) -__version__ = "0.8.9" +__version__ = "0.8.10" From cb6768d22ab991f5728aaa0eaf894e8bb7c97eb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:06:22 +0000 Subject: [PATCH 2/9] Bump virtualenv from 20.35.3 to 20.36.1 Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.35.3 to 20.36.1. - [Release notes](https://github.com/pypa/virtualenv/releases) - [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/virtualenv/compare/20.35.3...20.36.1) --- updated-dependencies: - dependency-name: virtualenv dependency-version: 20.36.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 166 +++++++++++++++++++--------------------------------- 1 file changed, 60 insertions(+), 106 deletions(-) diff --git a/poetry.lock b/poetry.lock index f23773c2..f01830a4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "black" @@ -7,7 +7,7 @@ description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, @@ -55,7 +55,7 @@ description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -103,7 +103,7 @@ description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, @@ -116,7 +116,7 @@ description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701"}, {file = "cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201"}, @@ -482,7 +482,7 @@ description = "cryptography is a package which provides cryptographic recipes an optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "(python_full_version >= \"3.9.0\" or platform_python_implementation == \"PyPy\") and sys_platform == \"linux\"" +markers = "(python_full_version >= \"3.9.0\" or platform_python_implementation == \"PyPy\") and sys_platform == \"linux\" and python_version < \"3.10\"" files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, @@ -533,7 +533,7 @@ description = "cryptography is a package which provides cryptographic recipes an optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" and platform_python_implementation != \"PyPy\" and sys_platform == \"linux\"" +markers = "python_full_version == \"3.8.*\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\" and sys_platform == \"linux\"" files = [ {file = "cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02"}, @@ -592,7 +592,10 @@ files = [ ] [package.dependencies] -cffi = {version = ">=1.14", markers = "python_full_version == \"3.8.*\" and platform_python_implementation != \"PyPy\""} +cffi = [ + {version = ">=1.14", markers = "python_full_version == \"3.8.*\" and platform_python_implementation != \"PyPy\""}, + {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}, +] typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} [package.extras] @@ -624,7 +627,7 @@ description = "Formats docstrings to follow PEP 257" optional = false python-versions = "<4.0,>=3.7" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "docformatter-1.7.6-py3-none-any.whl", hash = "sha256:3259698c29b411fc53ac363c5f90b5e1c0ccb7224b166df33224b85d98f2e540"}, {file = "docformatter-1.7.6.tar.gz", hash = "sha256:911f03dd302d0045739dd4daf6a007ce26256db693494b19a4ccdc9136a59dbb"}, @@ -645,7 +648,7 @@ description = "Formats docstrings to follow PEP 257" optional = false python-versions = "<4.0,>=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "docformatter-1.7.7-py3-none-any.whl", hash = "sha256:7af49f8a46346a77858f6651f431b882c503c2f4442c8b4524b920c863277834"}, {file = "docformatter-1.7.7.tar.gz", hash = "sha256:ea0e1e8867e5af468dfc3f9e947b92230a55be9ec17cd1609556387bffac7978"}, @@ -666,7 +669,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version <= \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -703,7 +706,7 @@ description = "A platform independent file lock." optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -721,12 +724,25 @@ description = "A platform independent file lock." optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\" and python_version < \"3.10\"" files = [ {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, ] +[[package]] +name = "filelock" +version = "3.20.3" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, + {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, +] + [[package]] name = "future" version = "1.0.0" @@ -761,7 +777,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.12\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, @@ -786,7 +802,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\" and python_version < \"3.12\"" +markers = "python_version >= \"3.9\" and (platform_python_implementation != \"PyPy\" or python_version >= \"3.10\") and python_version < \"3.12\"" files = [ {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, @@ -935,7 +951,7 @@ description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, @@ -948,7 +964,7 @@ description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b"}, {file = "more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd"}, @@ -973,7 +989,7 @@ description = "Optional static typing for Python" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, @@ -1034,7 +1050,7 @@ description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, @@ -1230,7 +1246,7 @@ description = "A small Python package for determining appropriate platform-speci optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1248,7 +1264,7 @@ description = "A small Python package for determining appropriate platform-speci optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, @@ -1266,7 +1282,7 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1283,7 +1299,7 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -1311,7 +1327,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "platform_python_implementation != \"PyPy\" and sys_platform == \"linux\" and (python_full_version == \"3.8.*\" or implementation_name != \"PyPy\")" +markers = "platform_python_implementation != \"PyPy\" and sys_platform == \"linux\" and (implementation_name != \"PyPy\" or python_full_version == \"3.8.*\")" files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, @@ -1324,7 +1340,7 @@ description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, @@ -1345,7 +1361,7 @@ description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948"}, {file = "pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335"}, @@ -1389,7 +1405,7 @@ description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, @@ -1408,7 +1424,7 @@ description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, @@ -1466,7 +1482,6 @@ description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" files = [ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, @@ -1482,29 +1497,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - [[package]] name = "ruff" version = "0.0.277" @@ -1568,7 +1560,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version <= \"3.10\"" files = [ {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, @@ -1621,7 +1613,6 @@ description = "tox is a generic virtualenv management and test command line tool optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" files = [ {file = "tox-4.25.0-py3-none-any.whl", hash = "sha256:4dfdc7ba2cc6fdc6688dde1b21e7b46ff6c41795fb54586c91a3533317b5255c"}, {file = "tox-4.25.0.tar.gz", hash = "sha256:dd67f030317b80722cf52b246ff42aafd3ed27ddf331c415612d084304cf5e52"}, @@ -1643,32 +1634,6 @@ virtualenv = ">=20.29.1" [package.extras] test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.4)", "pytest-mock (>=3.14)"] -[[package]] -name = "tox" -version = "4.30.3" -description = "tox is a generic virtualenv management and test command line tool" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" -files = [ - {file = "tox-4.30.3-py3-none-any.whl", hash = "sha256:a9f17b4b2d0f74fe0d76207236925a119095011e5c2e661a133115a8061178c9"}, - {file = "tox-4.30.3.tar.gz", hash = "sha256:f3dd0735f1cd4e8fbea5a3661b77f517456b5f0031a6256432533900e34b90bf"}, -] - -[package.dependencies] -cachetools = ">=6.1" -chardet = ">=5.2" -colorama = ">=0.4.6" -filelock = ">=3.18" -packaging = ">=25" -platformdirs = ">=4.3.8" -pluggy = ">=1.6" -pyproject-api = ">=1.9.1" -tomli = {version = ">=2.2.1", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.14.1", markers = "python_version < \"3.11\""} -virtualenv = ">=20.31.2" - [[package]] name = "types-python-dateutil" version = "2.9.0.20241206" @@ -1676,7 +1641,7 @@ description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, @@ -1689,7 +1654,7 @@ description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "types_python_dateutil-2.9.0.20251008-py3-none-any.whl", hash = "sha256:b9a5232c8921cf7661b29c163ccc56055c418ab2c6eabe8f917cbcc73a4c4157"}, {file = "types_python_dateutil-2.9.0.20251008.tar.gz", hash = "sha256:c3826289c170c93ebd8360c3485311187df740166dbab9dd3b792e69f2bc1f9c"}, @@ -1702,7 +1667,7 @@ description = "Typing stubs for requests" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, @@ -1718,7 +1683,7 @@ description = "Typing stubs for requests" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1"}, {file = "types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d"}, @@ -1746,25 +1711,11 @@ description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main", "dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" files = [ {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - [[package]] name = "untokenize" version = "0.1.1" @@ -1783,7 +1734,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.8" groups = ["main", "dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, @@ -1802,7 +1753,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.9" groups = ["main", "dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, @@ -1816,19 +1767,22 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.35.3" +version = "20.36.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a"}, - {file = "virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44"}, + {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"}, + {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"}, ] [package.dependencies] distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" +filelock = [ + {version = ">=3.16.1,<4", markers = "python_version < \"3.10\""}, + {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""}, +] platformdirs = ">=3.9.1,<5" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} @@ -1843,7 +1797,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.12\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, @@ -1864,7 +1818,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\" and python_version < \"3.12\"" +markers = "python_version >= \"3.9\" and (platform_python_implementation != \"PyPy\" or python_version >= \"3.10\") and python_version < \"3.12\"" files = [ {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, From 542b372c07db121f0cd4a3f8754bfd9d330bec94 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Thu, 15 Jan 2026 20:35:02 +0100 Subject: [PATCH 3/9] chore(media): Don't suppress original exception in ManifestDecodeError The purpose of this commit is to improve error handling when decoding the manifest in the tidalapi library. Instead of suppressing the original exception when a manifest decoding error occurs, it now chains the original exception to the ManifestDecodeError. Reference: #396 --- tidalapi/media.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tidalapi/media.py b/tidalapi/media.py index 8cb4d2cb..e04d382a 100644 --- a/tidalapi/media.py +++ b/tidalapi/media.py @@ -609,8 +609,8 @@ def get_manifest_data(self) -> str: try: # Stream Manifest is base64 encoded. return base64.b64decode(self.manifest).decode("utf-8") - except: - raise ManifestDecodeError + except Exception as e: + raise ManifestDecodeError from e @property def is_mpd(self) -> bool: @@ -761,15 +761,15 @@ def from_stream(stream) -> "DashInfo": try: if stream.is_mpd and not stream.is_encrypted: return DashInfo(stream.get_manifest_data()) - except: - raise ManifestDecodeError + except Exception as e: + raise ManifestDecodeError from e @staticmethod def from_mpd(mpd_manifest) -> "DashInfo": try: return DashInfo(mpd_manifest) - except: - raise ManifestDecodeError + except Exception as e: + raise ManifestDecodeError from e def __init__(self, mpd_xml): mpd = MPEGDASHParser.parse( From 4957634bf4a0f430b2eab03cc31a80e73ba2ab6d Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 19 Jan 2026 01:51:13 +0100 Subject: [PATCH 4/9] chore(deps): Bump mpegdash version to 0.4.1 This fixes the issue with non-compliant DASH manifests being sent by TIDAL as of 2026-01-15. Context: https://github.com/sangwonl/python-mpegdash/pull/64 Closes: https://github.com/EbbLabs/python-tidal/issues/396 --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index f01830a4..fa575b35 100644 --- a/poetry.lock +++ b/poetry.lock @@ -972,14 +972,14 @@ files = [ [[package]] name = "mpegdash" -version = "0.4.0" +version = "0.4.1" description = "MPEG-DASH MPD(Media Presentation Description) Parser" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "mpegdash-0.4.0-py3-none-any.whl", hash = "sha256:d07f6e1f2a67ddce1be501e3ad7abc29a2d6a7b1830b4da974b49c2ebe99cf2a"}, - {file = "mpegdash-0.4.0.tar.gz", hash = "sha256:65368c7a367c6875eb8c456a08644eb0708981a745044da0c9e942a3bc2b6389"}, + {file = "mpegdash-0.4.1-py3-none-any.whl", hash = "sha256:788e559b73acbf9175422548c58a4c441486c0ce0633fa10f77097bfffeaad4d"}, + {file = "mpegdash-0.4.1.tar.gz", hash = "sha256:e31d81e1f0184cc456553ef2a4a1de1c662972b8ce04b687c1854180912759e6"}, ] [[package]] @@ -1835,4 +1835,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.8" -content-hash = "5ca113aeb551a76c25cea8919954156c5bce4a154a93c6331048af188ee954a6" +content-hash = "eb37fb08af4e8d1f657b546398964ff33431f7f22898065fd31b9d6a70997064" diff --git a/pyproject.toml b/pyproject.toml index 024a7af9..87c89bb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ python-dateutil = "^2.8.2" typing-extensions = "^4.12.2" ratelimit = "^2.2.1" isodate = "^0.7.2" -mpegdash = "^0.4.0" +mpegdash = "^0.4.1" pyaes = "^1.6.1" [tool.poetry.group.dev.dependencies] From 05b6ebeb7b2541ea6a409b7402641bc5623e5eaa Mon Sep 17 00:00:00 2001 From: tehkillerbee Date: Mon, 19 Jan 2026 20:10:55 +0100 Subject: [PATCH 5/9] Version bump --- HISTORY.rst | 4 ++++ docs/conf.py | 2 +- pyproject.toml | 2 +- tidalapi/__init__.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 536db06f..ce853582 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,10 @@ History ======= +v0.8.11 +-------- +* Bump mpegdash version to 0.4.1 (Fixes bug caused by non-compliant DASH manifests) + v0.8.10 -------- * Tests: Updated misc. tests, added pagination tests - tehkillerbee_ diff --git a/docs/conf.py b/docs/conf.py index cf471276..84697941 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ author = "The tidalapi Developers" # The full version, including alpha/beta/rc tags -release = "0.8.10" +release = "0.8.11" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 87c89bb0..4bc81bce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tidalapi" -version = "0.8.10" +version = "0.8.11" description = "Unofficial API for TIDAL music streaming service." authors = ["Thomas Amland "] maintainers = ["tehkillerbee "] diff --git a/tidalapi/__init__.py b/tidalapi/__init__.py index eac49a07..5f4f7be1 100644 --- a/tidalapi/__init__.py +++ b/tidalapi/__init__.py @@ -17,4 +17,4 @@ User, ) -__version__ = "0.8.10" +__version__ = "0.8.11" From 623c3a7fda42bcdd0a048f6be9422b14182faa09 Mon Sep 17 00:00:00 2001 From: "Bradon Kanyid (rattboi)" Date: Mon, 13 Apr 2026 05:17:03 +0000 Subject: [PATCH 6/9] feat: add nix flake for development Adds nix flake w/ devShell for simpler development Similar to mopidy-tidal, but only supporting devShell for now --- .envrc | 1 + .gitignore | 5 ++++- flake.lock | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 41 ++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 3111ee5a..52732e41 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,7 @@ tidal*.json *.mpd # Local devtools -devtools \ No newline at end of file +devtools + +# Nix / direnv +.direnv/ \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..d6ae36a9 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1775710090, + "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "4c1018dae018162ec878d42fec712642d214fdfa", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1774748309, + "narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "333c4e0545a6da976206c74db8773a1645b5870a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..bcd8c375 --- /dev/null +++ b/flake.nix @@ -0,0 +1,41 @@ +{ + description = "Unofficial API for TIDAL music streaming service."; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + }; + + outputs = inputs @ { + nixpkgs, + flake-parts, + ... + }: + flake-parts.lib.mkFlake {inherit inputs;} { + systems = nixpkgs.lib.systems.flakeExposed; + perSystem = { + pkgs, + ... + }: let + python = pkgs.python3; + buildInputs = + [ + python + ] + ++ (with pkgs; [ + # build / dev + poetry + ruff + ]); + in { + devShells.default = pkgs.mkShell { + packages = buildInputs; + shellHook = '' + [ ! -d .venv ] && python -m venv .venv + source .venv/bin/activate + poetry install --quiet 2>/dev/null + ''; + }; + }; + }; +} From 1d2c280964b9c3b79cc1a5397d1d4cee70287663 Mon Sep 17 00:00:00 2001 From: "Bradon Kanyid (rattboi)" Date: Mon, 13 Apr 2026 05:28:19 +0000 Subject: [PATCH 7/9] fix: opt-in cursor-based pagination for playlists and folders The v2 my-collection/playlists/folders endpoint uses cursor-based pagination, but playlists() and playlist_folders() were using offset-based requests via map_request(). This caused all pages to return the same first 50 results, since the offset parameter is silently ignored by the API. Note: I believe this means some of the existing tests that try to use offset-based calls to playlist() are inaccurately passing in sitations they shouldn't. I left those tests as-is for now, but added one specifically to test cursor-based pagination. I added a new defaulted parameter at the end of the function arguments in order to try to not break any API interfaces. Unfortunately, that meant hiding a little state in the class to remember the previous cursor from pagination call to call. Switched to raw request + map_json to preserve the cursor from the JSON response. Add an optional cursor parameter to playlists() and playlist_folders(). Rewrite playlists_paginated() to loop using the cursor instead of parallel offset-based fetching via get_items(). Unfortunately because each call returns the next cursor, I don't believe parallel fetch is possible anymore. --- tests/test_user.py | 8 +++++ tidalapi/user.py | 76 ++++++++++++++++++++++++++++------------------ 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/tests/test_user.py b/tests/test_user.py index 3733d3cc..c38a3daf 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -65,6 +65,14 @@ def test_get_user_playlists(session): assert playlist_ids | favourite_ids == both_ids +def test_get_user_playlists_paginated(session): + expected_count = session.user.favorites.get_playlists_count() + all_playlists = session.user.favorites.playlists_paginated() + assert len(all_playlists) == expected_count + unique_ids = set(x.id for x in all_playlists) + assert len(unique_ids) == expected_count + + def test_get_playlist_folders(session): folder = session.user.create_folder(title="testfolder") assert folder diff --git a/tidalapi/user.py b/tidalapi/user.py index 1e534e8a..2a073356 100644 --- a/tidalapi/user.py +++ b/tidalapi/user.py @@ -671,16 +671,31 @@ def playlists_paginated( order: Optional[PlaylistOrder] = None, order_direction: Optional[OrderDirection] = None, ) -> List["Playlist"]: - """Get the users favorite playlists, using pagination. + """Get the users favorite playlists, using cursor-based pagination. + + The v2 my-collection/playlists/folders endpoint uses cursor-based + pagination. Each response includes a ``cursor`` field that must be + passed to the next request to retrieve the following page. :param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" :return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite playlists. """ - count = self.session.user.favorites.get_playlists_count() - return get_items( - self.session.user.favorites.playlists, count, order, order_direction - ) + playlists: List["Playlist"] = [] + cursor: Optional[str] = None + + while True: + items = self.playlists( + cursor=cursor, + order=order, + order_direction=order_direction, + ) + playlists.extend(items) + cursor = self._last_playlists_cursor + if not cursor or not items: + break + + return playlists def playlists( self, @@ -688,14 +703,18 @@ def playlists( offset: int = 0, order: Optional[PlaylistOrder] = None, order_direction: Optional[OrderDirection] = None, + cursor: Optional[str] = None, ) -> List["Playlist"]: - """Get the users favorite playlists (v2 endpoint), relative to the root folder + """Get the users favorite playlists (v2 endpoint), relative to the root folder. This function is limited to 50 by TIDAL, requiring pagination. :param limit: The number of playlists you want returned (Note: Cannot exceed 50) - :param offset: The index of the first playlist to fetch + :param offset: The index of the first playlist to fetch. Note: this parameter is + ignored by the TIDAL API for this endpoint. Use ``cursor`` for pagination. :param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" + :param cursor: Cursor for fetching the next page of results. Obtained from + :attr:`_last_playlists_cursor` after a previous call to this method. :return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite playlists. """ params = { @@ -704,6 +723,8 @@ def playlists( "limit": limit, "includeOnly": "PLAYLIST", # Include only PLAYLIST types, FOLDER will be ignored } + if cursor: + params["cursor"] = cursor if order: params["order"] = order.value else: @@ -714,17 +735,13 @@ def playlists( params["orderDirection"] = OrderDirection.Descending.value endpoint = "my-collection/playlists/folders" - return cast( - List["Playlist"], - self.session.request.map_request( - url=urljoin( - self.session.config.api_v2_location, - endpoint, - ), - params=params, - parse=self.session.parse_playlist, - ), + url = urljoin(self.session.config.api_v2_location, endpoint) + json_obj = self.session.request.request("GET", url, params).json() + items = self.session.request.map_json( + json_obj, parse=self.session.parse_playlist ) + self._last_playlists_cursor = json_obj.get("cursor") + return cast(List["Playlist"], items) def playlist_folders( self, @@ -733,14 +750,19 @@ def playlist_folders( order: Optional[PlaylistOrder] = None, order_direction: Optional[OrderDirection] = None, parent_folder_id: str = "root", + cursor: Optional[str] = None, ) -> List["Folder"]: """Get a list of folders created by the user. :param limit: The number of playlists you want returned (Note: Cannot exceed 50) - :param offset: The index of the first playlist folder to fetch + :param offset: The index of the first playlist folder to fetch. Note: this + parameter is ignored by the TIDAL API for this endpoint. Use ``cursor`` + for pagination. :param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" :param parent_folder_id: Parent folder ID. Default: 'root' playlist folder + :param cursor: Cursor for fetching the next page of results. Obtained from + :attr:`_last_folders_cursor` after a previous call to this method. :return: Returns a list of :class:`~tidalapi.playlist.Folder` objects containing the Folders. """ params = { @@ -750,23 +772,19 @@ def playlist_folders( "order": "NAME", "includeOnly": "FOLDER", } + if cursor: + params["cursor"] = cursor if order: params["order"] = order.value if order_direction: params["orderDirection"] = order_direction.value endpoint = "my-collection/playlists/folders" - return cast( - List["Folder"], - self.session.request.map_request( - url=urljoin( - self.session.config.api_v2_location, - endpoint, - ), - params=params, - parse=self.session.parse_folder, - ), - ) + url = urljoin(self.session.config.api_v2_location, endpoint) + json_obj = self.session.request.request("GET", url, params).json() + items = self.session.request.map_json(json_obj, parse=self.session.parse_folder) + self._last_folders_cursor = json_obj.get("cursor") + return cast(List["Folder"], items) def get_playlists_count(self) -> int: """Get the total number of playlists in the user's root collection. From e2962a3d22774a09b4323f5178cd01e233576d25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 21:23:19 +0000 Subject: [PATCH 8/9] Bump idna from 3.11 to 3.15 Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md) - [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15) --- updated-dependencies: - dependency-name: idna dependency-version: '3.15' dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index fa575b35..c5688753 100644 --- a/poetry.lock +++ b/poetry.lock @@ -757,18 +757,18 @@ files = [ [[package]] name = "idna" -version = "3.11" +version = "3.15" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, + {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, + {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, ] [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "importlib-metadata" From 65cbbedbd71c72cb3f00a973aa659d8e481a81e2 Mon Sep 17 00:00:00 2001 From: Martin Rys Date: Wed, 5 Aug 2026 22:50:53 +0200 Subject: [PATCH 9/9] fix: Remove ratelimit dependency --- poetry.lock | 15 ++------------- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index c5688753..471fa451 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "black" @@ -1464,17 +1464,6 @@ files = [ {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] -[[package]] -name = "ratelimit" -version = "2.2.1" -description = "API rate limit decorator" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "ratelimit-2.2.1.tar.gz", hash = "sha256:af8a9b64b821529aca09ebaf6d8d279100d766f19e90b5059ac6a718ca6dee42"}, -] - [[package]] name = "requests" version = "2.32.4" @@ -1835,4 +1824,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.8" -content-hash = "eb37fb08af4e8d1f657b546398964ff33431f7f22898065fd31b9d6a70997064" +content-hash = "da901a06a54df8072dbd88d433aaec24f99be41ab67972bc45a3155ce6cc2c84" diff --git a/pyproject.toml b/pyproject.toml index 4bc81bce..f0284388 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,6 @@ python = "^3.8" requests = "^2.32.3" python-dateutil = "^2.8.2" typing-extensions = "^4.12.2" -ratelimit = "^2.2.1" isodate = "^0.7.2" mpegdash = "^0.4.1" pyaes = "^1.6.1"