Merge pull request #478 from exislow/ai-refactoring

This commit is contained in:
exislow
2025-07-03 09:57:26 +02:00
committed by GitHub
10 changed files with 2329 additions and 847 deletions
+36
View File
@@ -0,0 +1,36 @@
---
applyTo: "**/*.py"
---
# Project general coding standards
## Naming Conventions
- Use snake_case for variable and function names.
- Use CamelCase for class names.
- Follow PEP 8 style guidelines.
- Prefix private class members with underscore (\_).
- Use ALL_CAPS for constants.
## Error Handling
- Use try/except blocks for async operations.
- Always log errors with contextual information.
## Coding Guidelines
- Use type annotations / hints for function and method parameters, return types and variables. Follow PEP 484.
- Use the modern built-in generics from the `typing` module, such as `list`, `dict`, and `set`, instead of the older `List`, `Dict`, and `Set` from `typing`.
- Use newest coding style which is supported by the used Python version.
- Use more blank lines to achieve better code organization and readability.
- Follow PEP 492 – Coroutines with async and await syntax
- Follow PEP 498 – Literal String Interpolation
- Follow PEP 572 – Assignment Expressions
## Code Documentation
- Always write doc strings for all modules, classes, functions, and methods using google docstring style.
- Use typing in doc strings.
- Use line comments to explain complex logic.
- If refactoring code, ensure to update or add doc strings accordingly.
- If refactoring code to not remove existing line comments, but to update them to reflect the new code logic.
+1 -1
View File
@@ -191,4 +191,4 @@ Special thanks goes out to [@orbittwz](https://github.com/orbittwz) for all his
This project is based on:
- <https://fpgmaas.github.io/cookiecutter-poetry/>
- [cookiecutter-poetry](https://fpgmaas.github.io/cookiecutter-poetry/)
Generated
+120 -115
View File
@@ -978,14 +978,14 @@ pyyaml = ">=5.1"
[[package]]
name = "mkdocs-material"
version = "9.6.14"
version = "9.6.15"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
groups = ["docs"]
files = [
{file = "mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b"},
{file = "mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754"},
{file = "mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a"},
{file = "mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5"},
]
[package.dependencies]
@@ -1272,101 +1272,106 @@ setuptools = "*"
[[package]]
name = "pillow"
version = "11.2.1"
version = "11.3.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"},
{file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"},
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"},
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"},
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"},
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"},
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"},
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"},
{file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"},
{file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"},
{file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"},
{file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"},
{file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"},
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"},
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"},
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"},
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"},
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"},
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"},
{file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"},
{file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"},
{file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"},
{file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"},
{file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"},
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"},
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"},
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"},
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"},
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"},
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"},
{file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"},
{file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"},
{file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"},
{file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"},
{file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"},
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"},
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"},
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"},
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"},
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"},
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"},
{file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"},
{file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"},
{file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"},
{file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"},
{file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"},
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"},
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"},
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"},
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"},
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"},
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"},
{file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"},
{file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"},
{file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"},
{file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"},
{file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"},
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"},
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"},
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"},
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"},
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"},
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"},
{file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"},
{file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"},
{file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"},
{file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"},
{file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"},
{file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"},
{file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"},
{file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"},
{file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"},
{file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"},
{file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"},
{file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"},
{file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"},
{file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"},
{file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"},
{file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"},
{file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"},
{file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"},
{file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"},
{file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"},
{file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"},
{file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"},
{file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"},
{file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"},
{file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"},
{file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"},
{file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"},
{file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"},
{file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"},
{file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"},
{file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"},
{file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"},
{file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"},
{file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"},
{file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"},
{file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"},
{file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"},
{file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"},
{file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"},
{file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"},
{file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"},
{file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"},
{file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"},
{file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"},
{file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"},
{file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"},
{file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"},
{file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"},
{file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"},
{file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"},
{file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"},
{file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"},
{file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"},
{file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"},
{file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"},
{file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"},
{file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"},
{file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"},
{file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"},
{file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"},
{file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"},
{file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"},
{file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"},
{file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"},
{file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"},
{file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"},
{file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"},
{file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"},
{file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"},
{file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"},
{file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"},
{file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"},
{file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"},
{file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"},
{file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"},
{file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"},
{file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"},
{file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"},
{file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"},
{file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"},
{file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"},
{file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"},
{file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"},
{file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"},
{file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"},
{file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"},
{file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"},
{file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"},
{file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"},
{file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
test-arrow = ["pyarrow"]
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"]
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
typing = ["typing-extensions ; python_version < \"3.10\""]
xmp = ["defusedxml"]
@@ -1634,60 +1639,60 @@ dev = ["build", "flake8", "mypy", "pytest", "twine"]
[[package]]
name = "pyside6"
version = "6.8.3"
version = "6.8.1"
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.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:31f390c961b54067ae41360e5ea3b340ce0e0e5feadea2236c28226d3b37edcc"},
{file = "PySide6-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8e53e2357bfbdee1fa86c48312bf637460a2c26d49e7af0b3fae2e179ccc7052"},
{file = "PySide6-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:5bf5153cab9484629315f57c56a9ad4b7d075b4dd275f828f7549abf712c590b"},
{file = "PySide6-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:722dc0061d8ef6dbb8c0b99864f21e83a5b49ece1ecb2d0b890840d969e1e461"},
{file = "PySide6-6.8.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:6d1fd95651cdbdea741af21e155350986eca31ff015fc4c721ce01c2a110a4cc"},
{file = "PySide6-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d6adc5d53313249bbe02edb673877c1d437e215d71e88da78412520653f5c9f"},
{file = "PySide6-6.8.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ddeeaeca8ebd0ddb1ded30dd33e9240a40f330cc91832de346ba6c9d0cd1253e"},
{file = "PySide6-6.8.1-cp39-abi3-win_amd64.whl", hash = "sha256:866eeaca3ffead6b9d30fa3ed395d5624da0246d7586c8b8207e77ac65d82458"},
]
[package.dependencies]
PySide6-Addons = "6.8.3"
PySide6-Essentials = "6.8.3"
shiboken6 = "6.8.3"
PySide6-Addons = "6.8.1"
PySide6-Essentials = "6.8.1"
shiboken6 = "6.8.1"
[[package]]
name = "pyside6-addons"
version = "6.8.3"
version = "6.8.1"
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.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:ea46649e40b9e6ab11a0da2da054d3914bff5607a5882885e9c3bc2eef200036"},
{file = "PySide6_Addons-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6983d3b01fad53637bad5360930d5923509c744cc39704f9c1190eb9934e33da"},
{file = "PySide6_Addons-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7949a844a40ee10998eb2734e2c06c4c7182dfcd4c21cc4108a6b96655ebe59f"},
{file = "PySide6_Addons-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:67548f6db11f4e1b7e4b6efd9c3fc2e8d275188a7b2feac388961128572a6955"},
{file = "PySide6_Addons-6.8.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:879c12346b4b76f5d5ee6499d8ca53b5666c0c998b8fdf8780f08f69ea95d6f9"},
{file = "PySide6_Addons-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f80cc03c1ac54132c6f800aa461dced64acd7d1646898db164ccb56fe3c23dd4"},
{file = "PySide6_Addons-6.8.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:570a25016d80046274f454ed0bb06734f478ce6c21be5dec62b624773fc7504e"},
{file = "PySide6_Addons-6.8.1-cp39-abi3-win_amd64.whl", hash = "sha256:d7c8c1e89ee0db84631d5b8fdb9129d9d2a0ffb3b4cb2f5192dc8367dd980db4"},
]
[package.dependencies]
PySide6-Essentials = "6.8.3"
shiboken6 = "6.8.3"
PySide6-Essentials = "6.8.1"
shiboken6 = "6.8.1"
[[package]]
name = "pyside6-essentials"
version = "6.8.3"
version = "6.8.1"
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.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:aa56c135db924ecfaf50088baf32f737d28027419ca5fee67c0c7141b29184e3"},
{file = "PySide6_Essentials-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fd57fa0c886ef99b3844173322c0023ec77cc946a0c9a0cdfbc2ac5c511053c1"},
{file = "PySide6_Essentials-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b4f4823f870b5bed477d6f7b6a3041839b859f70abfd703cf53208c73c2fe4cd"},
{file = "PySide6_Essentials-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:3c0fae5550aff69f2166f46476c36e0ef56ce73d84829eac4559770b0c034b07"},
{file = "PySide6_Essentials-6.8.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:bd05155245e3cd1572e68d72772e78fadfd713575bbfdd2c5e060d5278e390e9"},
{file = "PySide6_Essentials-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f600b149e65b57acd6a444edb17615adc42cc2491548ae443ccb574036d86b1"},
{file = "PySide6_Essentials-6.8.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:bf8a3c9ee0b997eb18fb00cb09aacaa28b8a51ce3c295a252cc594c5530aba56"},
{file = "PySide6_Essentials-6.8.1-cp39-abi3-win_amd64.whl", hash = "sha256:d5ed4ddb149f36d65bc49ae4260b2d213ee88b2d9a309012ae27f38158c2d1b6"},
]
[package.dependencies]
shiboken6 = "6.8.3"
shiboken6 = "6.8.1"
[[package]]
name = "pytest"
@@ -1957,17 +1962,17 @@ files = [
[[package]]
name = "shiboken6"
version = "6.8.3"
version = "6.8.1"
description = "Python/C++ bindings helper module"
optional = true
python-versions = "<3.14,>=3.9"
groups = ["main"]
markers = "extra == \"gui\""
files = [
{file = "shiboken6-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:483efc7dd53c69147b8a8ade71f7619c79ffc683efcb1dc4f4cb6c40bb23d29b"},
{file = "shiboken6-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:295a003466ca2cccf6660e2f2ceb5e6cef4af192a48a196a32d46b6f0c9ec5cb"},
{file = "shiboken6-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:2b1a41348102952d2a5fbf3630bddd4d44112e18058b5e4cf505e51f2812429d"},
{file = "shiboken6-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:bca3a94513ce9242f7d4bbdca902072a1631888e0aa3a8711a52cc5dbe93588f"},
{file = "shiboken6-6.8.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:9a2f51d1ddd3b6d193a0f0fdc09f8d41f2092bc664723c9b9efc1056660d0608"},
{file = "shiboken6-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1dc4c1976809b0e68872bb98474cccd590455bdcd015f0e0639907e94af27b6a"},
{file = "shiboken6-6.8.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ab5b60602ca6227103138aae89c4f5df3b1b8e249cbc8ec9e6e2a57f20ad9a91"},
{file = "shiboken6-6.8.1-cp39-abi3-win_amd64.whl", hash = "sha256:3ea127fd72be113b73cacd70e06687ad6f83c1c888047833c7dcdd5cf8e7f586"},
]
[[package]]
@@ -2337,5 +2342,5 @@ gui = ["pyqtdarktheme-fork", "pyside6"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.12,<3.14"
content-hash = "ad08ac2eff30520097a409a3461facd636bbee0fc4228fa2131c53f4684bb5c6"
python-versions = ">=3.12,<3.13"
content-hash = "04e7d96d0b9a2e4777c9c2ad4305b81704acfec50ab7dfceca91666e985a57d7"
+3 -5
View File
@@ -10,9 +10,8 @@ classifiers = [
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
requires-python = ">=3.12,<3.14"
requires-python = ">=3.12,<3.13"
dynamic = ["dependencies"]
[project.urls]
@@ -35,9 +34,8 @@ dataclasses-json = "^0.6.7"
pathvalidate = "^3.3.1"
m3u8 = "^6.0.0"
coloredlogs = "^15.0.1"
pyside6 = { version = "6.8.3", optional = true }
pyside6 = { version = "6.8.1", optional = true }
pyqtdarktheme-fork = { version = "^2.3.4", optional = true }
mpegdash = "^0.4.0"
rich = "^14.0.0"
toml = "^0.10.2"
typer = "^0.16.0"
@@ -141,7 +139,7 @@ ignore = [
[tool.black]
# https://github.com/psf/black
target-version = ["py312", "py313"]
target-version = ["py312"]
line-length = 120
color = true
preview = true
+232 -131
View File
@@ -22,12 +22,6 @@ from tidal_dl_ng.constants import CTX_TIDAL, MediaType
from tidal_dl_ng.download import Download
from tidal_dl_ng.helper.path import get_format_template, path_file_settings
from tidal_dl_ng.helper.tidal import (
Album,
Artist,
Mix,
Playlist,
Track,
Video,
all_artist_album_ids,
get_tidal_media_id,
get_tidal_media_type,
@@ -47,30 +41,167 @@ app.add_typer(dl_fav_group, name="dl_fav")
def version_callback(value: bool):
"""Callback to print version and exit if version flag is set.
Args:
value (bool): If True, prints version and exits.
"""
if value:
print(f"{__version__}")
raise typer.Exit()
def _handle_track_or_video(
dl: Download, ctx: typer.Context, item: str, media: object, file_template: str, idx: int, urls_pos_last: int
) -> None:
"""Handle downloading a track or video item.
Args:
dl (Download): The Download instance.
ctx (typer.Context): Typer context object.
item (str): The URL or identifier of the item.
media: The media object to download.
file_template (str): The file template for saving the media.
idx (int): The index of the item in the list.
urls_pos_last (int): The last index in the URLs list.
"""
settings = ctx.obj[CTX_TIDAL].settings
download_delay: bool = bool(settings.data.download_delay and idx < urls_pos_last)
dl.item(
media=media,
file_template=file_template,
download_delay=download_delay,
quality_audio=settings.data.quality_audio,
quality_video=settings.data.quality_video,
)
def _handle_album_playlist_mix_artist(
ctx: typer.Context,
dl: Download,
handling_app: HandlingApp,
media_type: MediaType,
media: object,
item_id: str,
file_template: str,
) -> bool:
"""Handle downloading albums, playlists, mixes, or artist collections.
Args:
ctx (typer.Context): Typer context object.
dl (Download): The Download instance.
handling_app (HandlingApp): The HandlingApp instance.
media_type (MediaType): The type of media (album, playlist, mix, or artist).
media: The media object to download.
item_id (str): The ID of the media item.
file_template (str): The file template for saving the media.
Returns:
bool: False if aborted, True otherwise.
"""
item_ids: list[str] = []
settings = ctx.obj[CTX_TIDAL].settings
if media_type == MediaType.ARTIST:
media_type = MediaType.ALBUM
item_ids += all_artist_album_ids(media)
else:
item_ids.append(item_id)
for _item_id in item_ids:
if handling_app.event_abort.is_set():
return False
dl.items(
media_id=_item_id,
media_type=media_type,
file_template=file_template,
video_download=settings.data.video_download,
download_delay=settings.data.download_delay,
quality_audio=settings.data.quality_audio,
quality_video=settings.data.quality_video,
)
return True
def _process_url(
dl: Download,
ctx: typer.Context,
handling_app: HandlingApp,
item: str,
idx: int,
urls_pos_last: int,
) -> bool:
"""Process a single URL or ID for download.
Args:
dl (Download): The Download instance.
ctx (typer.Context): Typer context object.
handling_app (HandlingApp): The HandlingApp instance.
item (str): The URL or identifier to process.
idx (int): The index of the item in the list.
urls_pos_last (int): The last index in the URLs list.
Returns:
bool: False if aborted, True otherwise.
"""
settings = ctx.obj[CTX_TIDAL].settings
if handling_app.event_abort.is_set():
return False
if "http" not in item:
print(f"It seems like that you have supplied an invalid URL: {item}")
return True
media_type = get_tidal_media_type(item)
if not isinstance(media_type, MediaType):
print(f"Could not determine media type for: {item}")
return True
item_id = get_tidal_media_id(item)
if not isinstance(item_id, str):
print(f"Could not determine media id for: {item}")
return True
file_template = get_format_template(media_type, settings)
if not isinstance(file_template, str):
print(f"Could not determine file template for: {item}")
return True
try:
media = instantiate_media(ctx.obj[CTX_TIDAL].session, media_type, item_id)
except Exception:
print(f"Media not found (ID: {item_id}). Maybe it is not available anymore.")
return True
if media_type in [MediaType.TRACK, MediaType.VIDEO]:
_handle_track_or_video(dl, ctx, item, media, file_template, idx, urls_pos_last)
elif media_type in [MediaType.ALBUM, MediaType.PLAYLIST, MediaType.MIX, MediaType.ARTIST]:
return _handle_album_playlist_mix_artist(ctx, dl, handling_app, media_type, media, item_id, file_template)
return True
def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bool:
"""Invokes download function and tracks progress.
:param ctx: The typer context object.
:type ctx: typer.Context
:param urls: The list of URLs to download.
:type urls: list[str]
:param try_login: If true, attempts to login to TIDAL.
:type try_login: bool
:return: True if ran successfully.
:rtype: bool
Args:
ctx (typer.Context): The typer context object.
urls (list[str]): The list of URLs to download.
try_login (bool, optional): If true, attempts to login to TIDAL. Defaults to True.
Returns:
bool: True if ran successfully.
"""
if try_login:
# Call login method to validate the token.
ctx.invoke(login, ctx)
# Create initial objects.
settings: Settings = Settings()
settings: Settings = ctx.obj[CTX_TIDAL].settings
handling_app: HandlingApp = HandlingApp()
progress: Progress = Progress(
TextColumn("[progress.description]{task.description}"),
SpinnerColumn(),
@@ -79,8 +210,9 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
refresh_per_second=20,
auto_refresh=True,
expand=True,
transient=False, # Prevent progress from disappearing
transient=False,
)
progress_overall = Progress(
TextColumn("[progress.description]{task.description}"),
SpinnerColumn(),
@@ -89,12 +221,14 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
refresh_per_second=20,
auto_refresh=True,
expand=True,
transient=False, # Prevent progress from disappearing
transient=False,
)
fn_logger = LoggerWrapped(progress.print)
dl = Download(
session=ctx.obj[CTX_TIDAL].session,
skip_existing=ctx.obj[CTX_TIDAL].settings.data.skip_existing,
skip_existing=settings.data.skip_existing,
path_base=settings.data.download_base_path,
fn_logger=fn_logger,
progress=progress,
@@ -102,80 +236,20 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
event_abort=handling_app.event_abort,
event_run=handling_app.event_run,
)
progress_table = Table.grid()
# Style Progress display.
progress_table = Table.grid()
progress_table.add_row(progress)
progress_table.add_row(progress_overall)
progress_group = Group(
progress_table,
)
progress_group = Group(progress_table)
urls_pos_last = len(urls) - 1
# Use a single Live display for both progress and table
with Live(progress_group, refresh_per_second=20, vertical_overflow="visible"):
try:
for item in urls:
media_type: MediaType | bool = False
# Exit loop if abort signal is set.
if handling_app.event_abort.is_set():
for idx, item in enumerate(urls):
if _process_url(dl, ctx, handling_app, item, idx, urls_pos_last) is False:
return False
# Extract media name and id from link.
if "http" in item:
media_type = get_tidal_media_type(item)
item_id = get_tidal_media_id(item)
file_template = get_format_template(media_type, settings)
else:
print(f"It seems like that you have supplied an invalid URL: {item}")
continue
try:
media: Track | Video | Album | Playlist | Mix | Artist = instantiate_media(
ctx.obj[CTX_TIDAL].session, media_type, item_id
)
except Exception:
print(f"Media not found (ID: {item_id}). Maybe it is not available anymore.")
continue
# Download media.
if media_type in [MediaType.TRACK, MediaType.VIDEO]:
download_delay: bool = bool(settings.data.download_delay and urls.index(item) < urls_pos_last)
dl.item(
media=media,
file_template=file_template,
download_delay=download_delay,
quality_audio=settings.data.quality_audio,
quality_video=settings.data.quality_video,
)
elif media_type in [MediaType.ALBUM, MediaType.PLAYLIST, MediaType.MIX, MediaType.ARTIST]:
item_ids: [int] = []
if media_type == MediaType.ARTIST:
media_type = MediaType.ALBUM
item_ids = item_ids + all_artist_album_ids(media)
else:
item_ids.append(item_id)
for item_id in item_ids:
# Exit loop if abort signal is set.
if handling_app.event_abort.is_set():
return False
dl.items(
media_id=item_id,
media_type=media_type,
file_template=file_template,
video_download=ctx.obj[CTX_TIDAL].settings.data.video_download,
download_delay=settings.data.download_delay,
)
finally:
# Clear and stop progress display
progress.refresh()
progress.stop()
@@ -187,6 +261,12 @@ def callback_app(
ctx: typer.Context,
version: Annotated[bool | None, typer.Option("--version", "-v", callback=version_callback, is_eager=True)] = None,
):
"""App callback to initialize context and handle version option.
Args:
ctx (typer.Context): Typer context object.
version (bool | None, optional): Version flag. Defaults to None.
"""
ctx.obj = {"tidal": None}
@@ -196,16 +276,12 @@ def settings_management(
editor: Annotated[
bool, typer.Option("--editor", "-e", help="Open the settings file in your default editor.")
] = False,
):
"""
Print or set an option.
If no arguments are given, all options will be listed.
If only one argument is given, the value will be printed for this option.
To set a value for an option simply pass the value as the second argument
) -> None:
"""Print or set an option, or open the settings file in an editor.
:param editor: If set, your favorite system editor will be opened.
:param names: (Optional) None (list all options), one (list the value only for this option) or two arguments
(set the value for the option).
Args:
names (list[str] | None, optional): None (list all options), one (list the value only for this option) or two arguments (set the value for the option). Defaults to None.
editor (bool, optional): If set, your favorite system editor will be opened. Defaults to False.
"""
if editor:
config_path: Path = Path(path_file_settings())
@@ -223,12 +299,11 @@ def settings_management(
if names:
if names[0] not in d_settings:
print(f'Option "{names[0]}" is not valid!')
else:
if len(names) == 1:
print(f'{names[0]}: "{d_settings[names[0]]}"')
elif len(names) > 1:
settings.set_option(names[0], names[1])
settings.save()
elif len(names) == 1:
print(f'{names[0]}: "{d_settings[names[0]]}"')
elif len(names) > 1:
settings.set_option(names[0], names[1])
settings.save()
else:
help_settings: dict = HelpSettings().to_dict()
table = Table(title=f"Config: {path_file_settings()}")
@@ -236,7 +311,6 @@ def settings_management(
table.add_column("Value", style="magenta")
table.add_column("Description", style="green")
# Iterate over the attributes of the dataclass
for key, value in sorted(d_settings.items()):
table.add_row(key, str(value), help_settings[key])
@@ -246,6 +320,14 @@ def settings_management(
@app.command(name="login")
def login(ctx: typer.Context) -> bool:
"""Login to TIDAL and update context object.
Args:
ctx (typer.Context): Typer context object.
Returns:
bool: True if login was successful, False otherwise.
"""
print("Let us check, if you are already logged in... ", end="")
settings = Settings()
@@ -258,6 +340,11 @@ def login(ctx: typer.Context) -> bool:
@app.command(name="logout")
def logout() -> bool:
"""Logout from TIDAL.
Returns:
bool: True if logout was successful, False otherwise.
"""
settings = Settings()
tidal = Tidal(settings)
result = tidal.logout()
@@ -287,6 +374,16 @@ def download(
),
] = None,
) -> bool:
"""Download media from provided URLs or a file containing URLs.
Args:
ctx (typer.Context): Typer context object.
urls (list[str] | None, optional): List of URLs to download. Defaults to None.
file_urls (Path | None, optional): Path to file containing URLs. Defaults to None.
Returns:
bool: True if download was successful, False otherwise.
"""
if not urls:
# Read the text file provided.
if file_urls:
@@ -307,10 +404,11 @@ def download(
def download_fav_tracks(ctx: typer.Context) -> bool:
"""Download your favorite track collection.
:param ctx: Typer context object.
:type ctx: typer.Context
:return: Download result.
:rtype: bool
Args:
ctx (typer.Context): Typer context object.
Returns:
bool: Download result.
"""
# Method name
func_name_favorites: str = "tracks"
@@ -325,10 +423,11 @@ def download_fav_tracks(ctx: typer.Context) -> bool:
def download_fav_artists(ctx: typer.Context) -> bool:
"""Download your favorite artist collection.
:param ctx: Typer context object.
:type ctx: typer.Context
:return: Download result.
:rtype: bool
Args:
ctx (typer.Context): Typer context object.
Returns:
bool: Download result.
"""
# Method name
func_name_favorites: str = "artists"
@@ -343,10 +442,11 @@ def download_fav_artists(ctx: typer.Context) -> bool:
def download_fav_albums(ctx: typer.Context) -> bool:
"""Download your favorite album collection.
:param ctx: Typer context object.
:type ctx: typer.Context
:return: Download result.
:rtype: bool
Args:
ctx (typer.Context): Typer context object.
Returns:
bool: Download result.
"""
# Method name
func_name_favorites: str = "albums"
@@ -361,10 +461,11 @@ def download_fav_albums(ctx: typer.Context) -> bool:
def download_fav_videos(ctx: typer.Context) -> bool:
"""Download your favorite video collection.
:param ctx: Typer context object.
:type ctx: typer.Context
:return: Download result.
:rtype: bool
Args:
ctx (typer.Context): Typer context object.
Returns:
bool: Download result.
"""
# Method name
func_name_favorites: str = "videos"
@@ -375,26 +476,26 @@ def download_fav_videos(ctx: typer.Context) -> bool:
def _download_fav_factory(ctx: typer.Context, func_name_favorites: str) -> bool:
"""Factory which helps to download items from the favorites collections.
:param ctx: Typer context object.
:type ctx: typer.Context
:param func_name_favorites: Method name to call from `tidalapi` favorites object.
:type func_name_favorites: str
:return: Download result.
:rtype: bool
Args:
ctx (typer.Context): Typer context object.
func_name_favorites (str): Method name to call from `tidalapi` favorites object.
Returns:
bool: Download result.
"""
# Call login method to validate the token.
ctx.invoke(login, ctx)
# Get the method from the module
func_favorites: Callable = getattr(ctx.obj[CTX_TIDAL].session.user.favorites, func_name_favorites)
# Get favorite videos
media_urls: [str] = [media.share_url for media in func_favorites()]
media_urls: list[str] = [media.share_url for media in func_favorites()]
return _download(ctx, media_urls, try_login=False)
@app.command()
def gui(ctx: typer.Context):
"""Launch the GUI for the application.
Args:
ctx (typer.Context): Typer context object.
"""
from tidal_dl_ng.gui import gui_activate
ctx.invoke(login, ctx)
@@ -404,9 +505,9 @@ def gui(ctx: typer.Context):
def handle_sigint_term(signum, frame):
"""Set app abort event, so threads can check it and shutdown.
:param signum:
:param frame:
:return:
Args:
signum: Signal number.
frame: Current stack frame.
"""
handling_app: HandlingApp = HandlingApp()
+896 -212
View File
File diff suppressed because it is too large Load Diff
+636 -188
View File
File diff suppressed because it is too large Load Diff
+10 -2
View File
@@ -7,9 +7,17 @@ from Crypto.Util import Counter
def decrypt_security_token(security_token: str) -> (str, str):
"""
Decrypts security token into key and nonce pair
The `decrypt_security_token` function decrypts a security token into a key and nonce pair using AES
encryption.
security_token should match the securityToken value from the web response
Args:
security_token (str): The `security_token` parameter in the `decrypt_security_token` function is a
string that represents an encrypted security token. This function decrypts the security token into a
key and nonce pair using AES encryption. security_token should match the securityToken value from the web response.
Returns:
The `decrypt_security_token` function returns a tuple containing the key and nonce extracted from
the decrypted security token.
"""
# Do not change this
+394 -191
View File
@@ -24,6 +24,11 @@ from tidal_dl_ng.helper.tidal import name_builder_album_artist, name_builder_art
def path_home() -> str:
"""Get the home directory path.
Returns:
str: The home directory path.
"""
if "XDG_CONFIG_HOME" in os.environ:
return os.environ["XDG_CONFIG_HOME"]
elif "HOME" in os.environ:
@@ -35,6 +40,11 @@ def path_home() -> str:
def path_config_base() -> str:
"""Get the base configuration path.
Returns:
str: The base configuration path.
"""
# https://wiki.archlinux.org/title/XDG_Base_Directory
# X11 workaround: If user specified config path is set, do not point to "~/.config"
path_user_custom: str = os.environ.get("XDG_CONFIG_HOME", "")
@@ -45,14 +55,29 @@ def path_config_base() -> str:
def path_file_log() -> str:
"""Get the path to the log file.
Returns:
str: The log file path.
"""
return os.path.join(path_config_base(), "app.log")
def path_file_token() -> str:
"""Get the path to the token file.
Returns:
str: The token file path.
"""
return os.path.join(path_config_base(), "token.json")
def path_file_settings() -> str:
"""Get the path to the settings file.
Returns:
str: The settings file path.
"""
return os.path.join(path_config_base(), "settings.json")
@@ -91,130 +116,300 @@ def format_str_media(
list_pos: int = 0,
list_total: int = 0,
) -> str:
result: str = name
"""Format a string based on media attributes.
Args:
name (str): The format template name.
media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object.
album_track_num_pad_min (int): Minimum padding for track numbers. Defaults to 0.
list_pos (int): Position in a list. Defaults to 0.
list_total (int): Total items in a list. Defaults to 0.
Returns:
str: The formatted string.
"""
try:
match name:
case "artist_name":
if isinstance(media, Track | Video):
if hasattr(media, "artists"):
result = name_builder_artist(media)
elif hasattr(media, "artist"):
result = media.artist.name
case "album_artist":
result = name_builder_album_artist(media, first_only=True)
case "album_artists":
result = name_builder_album_artist(media)
case "track_title":
if isinstance(media, Track | Video):
result = name_builder_title(media)
case "mix_name":
if isinstance(media, Mix):
result = media.title
case "playlist_name":
if isinstance(media, Playlist | UserPlaylist):
result = media.name
case "album_title":
if isinstance(media, Album):
result = media.name
elif isinstance(media, Track):
result = media.album.name
case "album_track_num":
if isinstance(media, Track | Video):
result = calculate_number_padding(
album_track_num_pad_min,
media.track_num,
media.album.num_tracks if hasattr(media, "album") else 1,
)
case "album_num_tracks":
if isinstance(media, Track | Video):
result = str(media.album.num_tracks if hasattr(media, "album") else 1)
case "track_id":
if isinstance(media, Track | Video):
result = str(media.id)
case "playlist_id":
if isinstance(media, Playlist):
result = str(media.id)
case "album_id":
if isinstance(media, Album):
result = str(media.id)
elif isinstance(media, Track):
result = str(media.album.id)
case "track_duration_seconds":
if isinstance(media, Track | Video):
result = str(media.duration)
case "track_duration_minutes":
if isinstance(media, Track | Video):
m, s = divmod(media.duration, 60)
result = f"{m:01d}:{s:02d}"
case "album_duration_seconds":
if isinstance(media, Album):
result = str(media.duration)
case "album_duration_minutes":
if isinstance(media, Album):
m, s = divmod(media.duration, 60)
result = f"{m:01d}:{s:02d}"
case "playlist_duration_seconds":
if isinstance(media, Album):
result = str(media.duration)
case "playlist_duration_minutes":
if isinstance(media, Album):
m, s = divmod(media.duration, 60)
result = f"{m:01d}:{s:02d}"
case "album_year":
if isinstance(media, Album):
result = str(media.year)
elif isinstance(media, Track):
result = str(media.album.year)
case "video_quality":
if isinstance(media, Video):
result = media.video_quality
case "track_quality":
if isinstance(media, Track):
result = ", ".join(tag for tag in media.media_metadata_tags)
case "track_explicit":
if isinstance(media, Track | Video):
result = FORMAT_TEMPLATE_EXPLICIT if media.explicit else ""
case "album_explicit":
if isinstance(media, Album):
result = FORMAT_TEMPLATE_EXPLICIT if media.explicit else ""
case "album_num_volumes":
if isinstance(media, Album):
result = str(media.num_volumes)
case "track_volume_num":
if isinstance(media, Track | Video):
result = str(media.volume_num)
case "track_volume_num_optional":
if isinstance(media, Track | Video):
num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1
result = "" if num_volumes == 1 else str(media.volume_num)
case "track_volume_num_optional_CD":
if isinstance(media, Track | Video):
num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1
result = "" if num_volumes == 1 else f"CD{media.volume_num!s}"
case "isrc":
if isinstance(media, Track):
result = media.isrc
case "list_pos":
if isinstance(media, Track | Video):
# TODO: Rename `album_track_num_pad_min` globally.
result = calculate_number_padding(album_track_num_pad_min, list_pos, list_total)
# Try each formatter function in sequence
for formatter in (
_format_names,
_format_numbers,
_format_ids,
_format_durations,
_format_metadata,
_format_volumes,
):
result = formatter(name, media, album_track_num_pad_min, list_pos, list_total)
if result is not None:
return result
except Exception as e:
# TODO: Implement better exception logging.
print(e)
pass
return name
return result
def _format_artist_names(
name: str,
media: Track | Album | Playlist | UserPlaylist | Video | Mix,
*_args,
) -> str | None:
"""Handle artist name-related format strings.
Args:
name (str): The format string name to check.
media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract artist information from.
*_args (Any): Additional arguments (not used).
Returns:
str | None: The formatted artist name or None if the format string is not artist-related.
"""
if name == "artist_name" and isinstance(media, Track | Video):
if hasattr(media, "artists"):
return name_builder_artist(media)
elif hasattr(media, "artist"):
return media.artist.name
elif name == "album_artist":
return name_builder_album_artist(media, first_only=True)
elif name == "album_artists":
return name_builder_album_artist(media)
return None
def _format_titles(
name: str,
media: Track | Album | Playlist | UserPlaylist | Video | Mix,
*_args,
) -> str | None:
"""Handle title-related format strings.
Args:
name (str): The format string name to check.
media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract title information from.
*_args (Any): Additional arguments (not used).
Returns:
str | None: The formatted title or None if the format string is not title-related.
"""
if name == "track_title" and isinstance(media, Track | Video):
return name_builder_title(media)
elif name == "mix_name" and isinstance(media, Mix):
return media.title
elif name == "playlist_name" and isinstance(media, Playlist | UserPlaylist):
return media.name
elif name == "album_title":
if isinstance(media, Album):
return media.name
elif isinstance(media, Track):
return media.album.name
return None
def _format_names(
name: str,
media: Track | Album | Playlist | UserPlaylist | Video | Mix,
*_args,
) -> str | None:
"""Handle name-related format strings.
Args:
name (str): The format string name to check.
media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract name information from.
*_args (Any): Additional arguments (not used).
Returns:
str | None: The formatted name or None if the format string is not name-related.
"""
# First try artist name formats
result = _format_artist_names(name, media)
if result is not None:
return result
# Then try title formats
return _format_titles(name, media)
def _format_numbers(
name: str,
media: Track | Album | Playlist | UserPlaylist | Video | Mix,
album_track_num_pad_min: int,
list_pos: int,
list_total: int,
*_args,
) -> str | None:
"""Handle number-related format strings.
Args:
name (str): The format string name to check.
media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract number information from.
album_track_num_pad_min (int): Minimum padding for track numbers.
list_pos (int): Position in a list.
list_total (int): Total items in a list.
*_args (Any): Additional arguments (not used).
Returns:
str | None: The formatted number or None if the format string is not number-related.
"""
if name == "album_track_num" and isinstance(media, Track | Video):
return calculate_number_padding(
album_track_num_pad_min,
media.track_num,
media.album.num_tracks if hasattr(media, "album") else 1,
)
elif name == "album_num_tracks" and isinstance(media, Track | Video):
return str(media.album.num_tracks if hasattr(media, "album") else 1)
elif name == "list_pos" and isinstance(media, Track | Video):
# TODO: Rename `album_track_num_pad_min` globally.
return calculate_number_padding(album_track_num_pad_min, list_pos, list_total)
return None
def _format_ids(
name: str,
media: Track | Album | Playlist | UserPlaylist | Video | Mix,
*_args,
) -> str | None:
"""Handle ID-related format strings.
Args:
name (str): The format string name to check.
media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract ID information from.
*_args (Any): Additional arguments (not used).
Returns:
str | None: The formatted ID or None if the format string is not ID-related.
"""
# Handle track and playlist IDs
if (name == "track_id" and isinstance(media, Track | Video)) or (
name == "playlist_id" and isinstance(media, Playlist)
):
return str(media.id)
# Handle album IDs
elif name == "album_id":
if isinstance(media, Album):
return str(media.id)
elif isinstance(media, Track):
return str(media.album.id)
# Handle ISRC
elif name == "isrc" and isinstance(media, Track):
return media.isrc
return None
def _format_durations(
name: str,
media: Track | Album | Playlist | UserPlaylist | Video | Mix,
*_args,
) -> str | None:
"""Handle duration-related format strings.
Args:
name (str): The format string name to check.
media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract duration information from.
*_args (Any): Additional arguments (not used).
Returns:
str | None: The formatted duration or None if the format string is not duration-related.
"""
# Format track durations
if name == "track_duration_seconds" and isinstance(media, Track | Video):
return str(media.duration)
elif name == "track_duration_minutes" and isinstance(media, Track | Video):
m, s = divmod(media.duration, 60)
return f"{m:01d}:{s:02d}"
# Format album durations
elif name == "album_duration_seconds" and isinstance(media, Album):
return str(media.duration)
elif name == "album_duration_minutes" and isinstance(media, Album):
m, s = divmod(media.duration, 60)
return f"{m:01d}:{s:02d}"
# Format playlist durations
elif name == "playlist_duration_seconds" and isinstance(media, Album):
return str(media.duration)
elif name == "playlist_duration_minutes" and isinstance(media, Album):
m, s = divmod(media.duration, 60)
return f"{m:01d}:{s:02d}"
# Format year
elif name == "album_year":
if isinstance(media, Album):
return str(media.year)
elif isinstance(media, Track):
return str(media.album.year)
return None
def _format_metadata(
name: str,
media: Track | Album | Playlist | UserPlaylist | Video | Mix,
*_args,
) -> str | None:
"""Handle metadata-related format strings.
Args:
name (str): The format string name to check.
media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract metadata information from.
*_args (Any): Additional arguments (not used).
Returns:
str | None: The formatted metadata or None if the format string is not metadata-related.
"""
if name == "video_quality" and isinstance(media, Video):
return media.video_quality
elif name == "track_quality" and isinstance(media, Track):
return ", ".join(tag for tag in media.media_metadata_tags if tag is not None)
elif (name == "track_explicit" and isinstance(media, Track | Video)) or (
name == "album_explicit" and isinstance(media, Album)
):
return FORMAT_TEMPLATE_EXPLICIT if media.explicit else ""
return None
def _format_volumes(
name: str,
media: Track | Album | Playlist | UserPlaylist | Video | Mix,
*_args,
) -> str | None:
"""Handle volume-related format strings.
Args:
name (str): The format string name to check.
media (Track | Album | Playlist | UserPlaylist | Video | Mix): The media object to extract volume information from.
*_args (Any): Additional arguments (not used).
Returns:
str | None: The formatted volume information or None if the format string is not volume-related.
"""
if name == "album_num_volumes" and isinstance(media, Album):
return str(media.num_volumes)
elif name == "track_volume_num" and isinstance(media, Track | Video):
return str(media.volume_num)
elif name == "track_volume_num_optional" and isinstance(media, Track | Video):
num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1
return "" if num_volumes == 1 else str(media.volume_num)
elif name == "track_volume_num_optional_CD" and isinstance(media, Track | Video):
num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1
return "" if num_volumes == 1 else f"CD{media.volume_num!s}"
return None
def calculate_number_padding(padding_minimum: int, item_position: int, items_max: int) -> str:
"""Calculate the padded number string for an item.
Args:
padding_minimum (int): Minimum number of digits for padding.
item_position (int): The position of the item.
items_max (int): The maximum number of items.
Returns:
str: The padded number string.
"""
result: str
if items_max > 0:
count_digits: int = int(math.log10(items_max)) + 1
count_digits_computed: int = count_digits if count_digits > padding_minimum else padding_minimum
result = str(item_position).zfill(count_digits_computed)
count_digits = max(int(math.log10(items_max)) + 1, padding_minimum)
result = str(item_position).zfill(count_digits)
else:
result = str(item_position)
@@ -224,6 +419,15 @@ def calculate_number_padding(padding_minimum: int, item_position: int, items_max
def get_format_template(
media: Track | Album | Playlist | UserPlaylist | Video | Mix | MediaType, settings
) -> str | bool:
"""Get the format template for a given media type.
Args:
media (Track | Album | Playlist | UserPlaylist | Video | Mix | MediaType): The media object or type.
settings: The settings object containing format templates.
Returns:
str | bool: The format template string or False if not found.
"""
result = False
if isinstance(media, Track) or media == MediaType.TRACK:
@@ -241,83 +445,59 @@ def get_format_template(
def path_file_sanitize(path_file: pathlib.Path, adapt: bool = False, uniquify: bool = False) -> pathlib.Path:
sanitized_filename: str = path_file.name
sanitized_path: pathlib.Path = path_file.parent
result: pathlib.Path
"""Sanitize a file path to ensure it is valid and optionally make it unique.
# Sanitize filename and make sure it does not exceed FILENAME_LENGTH_MAX
try:
# sanitize_filename can shorten the file name actually
sanitized_filename = sanitize_filename(
sanitized_filename, replacement_text="_", validate_after_sanitize=True, platform="auto"
Args:
path_file (pathlib.Path): The file path to sanitize.
adapt (bool, optional): Whether to adapt the path in case of errors. Defaults to False.
uniquify (bool, optional): Whether to make the file name unique. Defaults to False.
Returns:
pathlib.Path: The sanitized file path.
"""
sanitized_filename = sanitize_filename(
path_file.name, replacement_text="_", validate_after_sanitize=True, platform="auto"
)
if not sanitized_filename.endswith(path_file.suffix):
sanitized_filename = (
sanitized_filename[: -len(path_file.suffix)] + FILENAME_SANITIZE_PLACEHOLDER + path_file.suffix
)
# Check if the file extension was removed by shortening the filename length
if not sanitized_filename.endswith(path_file.suffix):
# Add the original file extension
file_suffix: str = FILENAME_SANITIZE_PLACEHOLDER + path_file.suffix
sanitized_filename = sanitized_filename[: -len(file_suffix)] + file_suffix
except ValidationError as e:
if adapt:
# TODO: Implement proper exception handling and logging.
# Hacky stuff, since the sanitizing function does not shorten the filename (filename too long)
if str(e).startswith("[PV1101]"):
byte_ct: int = len(sanitized_filename.encode(sys.getfilesystemencoding())) - FILENAME_LENGTH_MAX
sanitized_filename = (
sanitized_filename[: -byte_ct - len(FILENAME_SANITIZE_PLACEHOLDER) - len(path_file.suffix)]
+ FILENAME_SANITIZE_PLACEHOLDER
+ path_file.suffix
)
else:
raise
else:
raise
# Sanitize the path.
# First sanitize sanitize each part of the path. Each part of the path is not allowed to be longer then 'PC_NAME_MAX'.
sanitized_parts = []
for part in sanitized_path.parts:
if part in sanitized_path.anchor:
sanitized_parts.append(part)
else:
sanitized_parts.append(
sanitized_path = pathlib.Path(
*[
(
sanitize_filename(part, replacement_text="_", validate_after_sanitize=True, platform="auto")
if part not in path_file.anchor
else part
)
for part in path_file.parent.parts
]
)
sanitized_path = pathlib.Path(*sanitized_parts)
# Then sanitize the whole path itself. The whole path is not allowed to be longer than 'PC_NAME_MAX'.
try:
sanitized_path: pathlib.Path = sanitize_filepath(
sanitized_path = sanitize_filepath(
sanitized_path, replacement_text="_", validate_after_sanitize=True, platform="auto"
)
except ValidationError as e:
# If adaption of path is allowed in case of an error set path to HOME.
if adapt:
if str(e).startswith("[PV1101]"):
sanitized_path = pathlib.Path.home()
else:
raise
if adapt and str(e).startswith("[PV1101]"):
sanitized_path = pathlib.Path.home()
else:
raise
result = sanitized_path / sanitized_filename
# Uniquify
if uniquify:
result = path_file_uniquify(result)
return result
return path_file_uniquify(result) if uniquify else result
def path_file_uniquify(path_file: pathlib.Path) -> pathlib.Path:
"""Checks whether the file exists, if so it tries to return an unique name suffix.
"""Ensure a file path is unique by appending a suffix if necessary.
:param path_file: Path to file name which shall be unique.
:type path_file: pathlib.Path
:return: Unique file name with path for given input.
:rtype: pathlib.Path
Args:
path_file (pathlib.Path): The file path to uniquify.
Returns:
pathlib.Path: The unique file path.
"""
unique_suffix: str = file_unique_suffix(path_file)
@@ -333,7 +513,16 @@ def path_file_uniquify(path_file: pathlib.Path) -> pathlib.Path:
return path_file
def file_unique_suffix(path_file: pathlib.Path, seperator: str = "_") -> str:
def file_unique_suffix(path_file: pathlib.Path, separator: str = "_") -> str:
"""Generate a unique suffix for a file path.
Args:
path_file (pathlib.Path): The file path to check for uniqueness.
separator (str, optional): The separator to use for the suffix. Defaults to "_".
Returns:
str: The unique suffix, or an empty string if not needed.
"""
threshold_zfill: int = len(str(UNIQUIFY_THRESHOLD))
count: int = 0
path_file_tmp: pathlib.Path = deepcopy(path_file)
@@ -341,46 +530,60 @@ def file_unique_suffix(path_file: pathlib.Path, seperator: str = "_") -> str:
while check_file_exists(path_file_tmp) and count < UNIQUIFY_THRESHOLD:
count += 1
unique_suffix = seperator + str(count).zfill(threshold_zfill)
unique_suffix = separator + str(count).zfill(threshold_zfill)
path_file_tmp = path_file.parent / (path_file.stem + unique_suffix + path_file.suffix)
return unique_suffix
def check_file_exists(path_file: pathlib.Path, extension_ignore: bool = False) -> bool:
"""Check if a file exists.
Args:
path_file (pathlib.Path): The file path to check.
extension_ignore (bool, optional): Whether to ignore the file extension. Defaults to False.
Returns:
bool: True if the file exists, False otherwise.
"""
if extension_ignore:
path_file_stem: str = pathlib.Path(path_file).stem
path_parent: pathlib.Path = pathlib.Path(path_file).parent
path_files: [str] = []
path_files: list[str] = []
for extension in AudioExtensions:
path_files.append(str(path_parent.joinpath(path_file_stem + extension)))
path_files.extend(str(path_parent.joinpath(path_file_stem + extension)) for extension in AudioExtensions)
else:
path_files: [str] = [path_file]
path_files: list[str] = [str(path_file)]
result = bool(sum([[True] if os.path.isfile(_file) else [] for _file in path_files], []))
return result
return any(os.path.isfile(_file) for _file in path_files)
def resource_path(relative_path):
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
def resource_path(relative_path: str) -> str:
"""Get the absolute path to a resource.
Args:
relative_path: The relative path to the resource.
Returns:
str: The absolute path to the resource.
"""
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
return os.path.join(base_path, relative_path)
def url_to_filename(url: str) -> str:
"""Return basename corresponding to url.
>>> print(url_to_filename('http://example.com/path/to/file%C3%80?opt=1'))
fileÀ
>>> print(url_to_filename('http://example.com/slash%2fname')) # '/' in name
Taken from https://gist.github.com/zed/c2168b9c52b032b5fb7d
Traceback (most recent call last):
...
ValueError
"""Convert a URL to a valid filename.
Args:
url (str): The URL to convert.
Returns:
str: The corresponding filename.
Raises:
ValueError: If the URL contains invalid characters for a filename.
"""
urlpath: str = urlsplit(url).path
basename: str = posixpath.basename(unquote(urlpath))
+1 -2
View File
@@ -1,11 +1,10 @@
[tox]
skipsdist = true
envlist = py312,py313
envlist = py312
[gh-actions]
python =
3.12: py312
3.13: py313
[testenv]
passenv = PYTHON_VERSION