Merge pull request #478 from exislow/ai-refactoring
This commit is contained in:
@@ -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.
|
||||||
@@ -191,4 +191,4 @@ Special thanks goes out to [@orbittwz](https://github.com/orbittwz) for all his
|
|||||||
|
|
||||||
This project is based on:
|
This project is based on:
|
||||||
|
|
||||||
- <https://fpgmaas.github.io/cookiecutter-poetry/>
|
- [cookiecutter-poetry](https://fpgmaas.github.io/cookiecutter-poetry/)
|
||||||
|
|||||||
Generated
+120
-115
@@ -978,14 +978,14 @@ pyyaml = ">=5.1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mkdocs-material"
|
name = "mkdocs-material"
|
||||||
version = "9.6.14"
|
version = "9.6.15"
|
||||||
description = "Documentation that simply works"
|
description = "Documentation that simply works"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["docs"]
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b"},
|
{file = "mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a"},
|
||||||
{file = "mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754"},
|
{file = "mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1272,101 +1272,106 @@ setuptools = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "11.2.1"
|
version = "11.3.0"
|
||||||
description = "Python Imaging Library (Fork)"
|
description = "Python Imaging Library (Fork)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"},
|
{file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"},
|
{file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"},
|
{file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"},
|
{file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"},
|
{file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"},
|
{file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"},
|
{file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"},
|
{file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"},
|
{file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"},
|
{file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"},
|
||||||
{file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"},
|
{file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"},
|
{file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"},
|
{file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"},
|
{file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"},
|
{file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"},
|
{file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"},
|
{file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"},
|
{file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"},
|
{file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"},
|
{file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"},
|
{file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"},
|
||||||
{file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"},
|
{file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"},
|
{file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"},
|
{file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"},
|
{file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"},
|
{file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"},
|
{file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"},
|
{file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"},
|
{file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"},
|
{file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"},
|
{file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"},
|
{file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"},
|
||||||
{file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"},
|
{file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"},
|
{file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"},
|
{file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"},
|
{file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"},
|
{file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"},
|
{file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"},
|
{file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"},
|
{file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"},
|
{file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"},
|
{file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"},
|
{file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"},
|
{file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"},
|
{file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"},
|
{file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"},
|
{file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"},
|
{file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"},
|
{file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"},
|
{file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"},
|
{file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"},
|
{file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"},
|
{file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"},
|
{file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"},
|
||||||
{file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"},
|
{file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"},
|
{file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"},
|
{file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"},
|
{file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"},
|
{file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"},
|
{file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"},
|
{file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"},
|
{file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"},
|
{file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"},
|
{file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"},
|
{file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"},
|
||||||
{file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"},
|
{file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"},
|
||||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"},
|
{file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"},
|
||||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"},
|
{file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"},
|
||||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"},
|
{file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"},
|
||||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"},
|
{file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"},
|
||||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"},
|
{file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"},
|
||||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"},
|
{file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"},
|
||||||
{file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"},
|
{file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"},
|
||||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"},
|
{file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"},
|
||||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"},
|
{file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"},
|
||||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"},
|
{file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"},
|
||||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"},
|
{file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"},
|
||||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"},
|
{file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"},
|
||||||
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"},
|
{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.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"},
|
{file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"},
|
||||||
{file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"},
|
{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]
|
[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"]
|
fpx = ["olefile"]
|
||||||
mic = ["olefile"]
|
mic = ["olefile"]
|
||||||
test-arrow = ["pyarrow"]
|
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\""]
|
typing = ["typing-extensions ; python_version < \"3.10\""]
|
||||||
xmp = ["defusedxml"]
|
xmp = ["defusedxml"]
|
||||||
|
|
||||||
@@ -1634,60 +1639,60 @@ dev = ["build", "flake8", "mypy", "pytest", "twine"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyside6"
|
name = "pyside6"
|
||||||
version = "6.8.3"
|
version = "6.8.1"
|
||||||
description = "Python bindings for the Qt cross-platform application and UI framework"
|
description = "Python bindings for the Qt cross-platform application and UI framework"
|
||||||
optional = true
|
optional = true
|
||||||
python-versions = "<3.14,>=3.9"
|
python-versions = "<3.14,>=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "extra == \"gui\""
|
markers = "extra == \"gui\""
|
||||||
files = [
|
files = [
|
||||||
{file = "PySide6-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:31f390c961b54067ae41360e5ea3b340ce0e0e5feadea2236c28226d3b37edcc"},
|
{file = "PySide6-6.8.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:6d1fd95651cdbdea741af21e155350986eca31ff015fc4c721ce01c2a110a4cc"},
|
||||||
{file = "PySide6-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8e53e2357bfbdee1fa86c48312bf637460a2c26d49e7af0b3fae2e179ccc7052"},
|
{file = "PySide6-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d6adc5d53313249bbe02edb673877c1d437e215d71e88da78412520653f5c9f"},
|
||||||
{file = "PySide6-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:5bf5153cab9484629315f57c56a9ad4b7d075b4dd275f828f7549abf712c590b"},
|
{file = "PySide6-6.8.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ddeeaeca8ebd0ddb1ded30dd33e9240a40f330cc91832de346ba6c9d0cd1253e"},
|
||||||
{file = "PySide6-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:722dc0061d8ef6dbb8c0b99864f21e83a5b49ece1ecb2d0b890840d969e1e461"},
|
{file = "PySide6-6.8.1-cp39-abi3-win_amd64.whl", hash = "sha256:866eeaca3ffead6b9d30fa3ed395d5624da0246d7586c8b8207e77ac65d82458"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
PySide6-Addons = "6.8.3"
|
PySide6-Addons = "6.8.1"
|
||||||
PySide6-Essentials = "6.8.3"
|
PySide6-Essentials = "6.8.1"
|
||||||
shiboken6 = "6.8.3"
|
shiboken6 = "6.8.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyside6-addons"
|
name = "pyside6-addons"
|
||||||
version = "6.8.3"
|
version = "6.8.1"
|
||||||
description = "Python bindings for the Qt cross-platform application and UI framework (Addons)"
|
description = "Python bindings for the Qt cross-platform application and UI framework (Addons)"
|
||||||
optional = true
|
optional = true
|
||||||
python-versions = "<3.14,>=3.9"
|
python-versions = "<3.14,>=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "extra == \"gui\""
|
markers = "extra == \"gui\""
|
||||||
files = [
|
files = [
|
||||||
{file = "PySide6_Addons-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:ea46649e40b9e6ab11a0da2da054d3914bff5607a5882885e9c3bc2eef200036"},
|
{file = "PySide6_Addons-6.8.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:879c12346b4b76f5d5ee6499d8ca53b5666c0c998b8fdf8780f08f69ea95d6f9"},
|
||||||
{file = "PySide6_Addons-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6983d3b01fad53637bad5360930d5923509c744cc39704f9c1190eb9934e33da"},
|
{file = "PySide6_Addons-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f80cc03c1ac54132c6f800aa461dced64acd7d1646898db164ccb56fe3c23dd4"},
|
||||||
{file = "PySide6_Addons-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7949a844a40ee10998eb2734e2c06c4c7182dfcd4c21cc4108a6b96655ebe59f"},
|
{file = "PySide6_Addons-6.8.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:570a25016d80046274f454ed0bb06734f478ce6c21be5dec62b624773fc7504e"},
|
||||||
{file = "PySide6_Addons-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:67548f6db11f4e1b7e4b6efd9c3fc2e8d275188a7b2feac388961128572a6955"},
|
{file = "PySide6_Addons-6.8.1-cp39-abi3-win_amd64.whl", hash = "sha256:d7c8c1e89ee0db84631d5b8fdb9129d9d2a0ffb3b4cb2f5192dc8367dd980db4"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
PySide6-Essentials = "6.8.3"
|
PySide6-Essentials = "6.8.1"
|
||||||
shiboken6 = "6.8.3"
|
shiboken6 = "6.8.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyside6-essentials"
|
name = "pyside6-essentials"
|
||||||
version = "6.8.3"
|
version = "6.8.1"
|
||||||
description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)"
|
description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)"
|
||||||
optional = true
|
optional = true
|
||||||
python-versions = "<3.14,>=3.9"
|
python-versions = "<3.14,>=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "extra == \"gui\""
|
markers = "extra == \"gui\""
|
||||||
files = [
|
files = [
|
||||||
{file = "PySide6_Essentials-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:aa56c135db924ecfaf50088baf32f737d28027419ca5fee67c0c7141b29184e3"},
|
{file = "PySide6_Essentials-6.8.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:bd05155245e3cd1572e68d72772e78fadfd713575bbfdd2c5e060d5278e390e9"},
|
||||||
{file = "PySide6_Essentials-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fd57fa0c886ef99b3844173322c0023ec77cc946a0c9a0cdfbc2ac5c511053c1"},
|
{file = "PySide6_Essentials-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f600b149e65b57acd6a444edb17615adc42cc2491548ae443ccb574036d86b1"},
|
||||||
{file = "PySide6_Essentials-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b4f4823f870b5bed477d6f7b6a3041839b859f70abfd703cf53208c73c2fe4cd"},
|
{file = "PySide6_Essentials-6.8.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:bf8a3c9ee0b997eb18fb00cb09aacaa28b8a51ce3c295a252cc594c5530aba56"},
|
||||||
{file = "PySide6_Essentials-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:3c0fae5550aff69f2166f46476c36e0ef56ce73d84829eac4559770b0c034b07"},
|
{file = "PySide6_Essentials-6.8.1-cp39-abi3-win_amd64.whl", hash = "sha256:d5ed4ddb149f36d65bc49ae4260b2d213ee88b2d9a309012ae27f38158c2d1b6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
shiboken6 = "6.8.3"
|
shiboken6 = "6.8.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
@@ -1957,17 +1962,17 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shiboken6"
|
name = "shiboken6"
|
||||||
version = "6.8.3"
|
version = "6.8.1"
|
||||||
description = "Python/C++ bindings helper module"
|
description = "Python/C++ bindings helper module"
|
||||||
optional = true
|
optional = true
|
||||||
python-versions = "<3.14,>=3.9"
|
python-versions = "<3.14,>=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "extra == \"gui\""
|
markers = "extra == \"gui\""
|
||||||
files = [
|
files = [
|
||||||
{file = "shiboken6-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:483efc7dd53c69147b8a8ade71f7619c79ffc683efcb1dc4f4cb6c40bb23d29b"},
|
{file = "shiboken6-6.8.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:9a2f51d1ddd3b6d193a0f0fdc09f8d41f2092bc664723c9b9efc1056660d0608"},
|
||||||
{file = "shiboken6-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:295a003466ca2cccf6660e2f2ceb5e6cef4af192a48a196a32d46b6f0c9ec5cb"},
|
{file = "shiboken6-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1dc4c1976809b0e68872bb98474cccd590455bdcd015f0e0639907e94af27b6a"},
|
||||||
{file = "shiboken6-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:2b1a41348102952d2a5fbf3630bddd4d44112e18058b5e4cf505e51f2812429d"},
|
{file = "shiboken6-6.8.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ab5b60602ca6227103138aae89c4f5df3b1b8e249cbc8ec9e6e2a57f20ad9a91"},
|
||||||
{file = "shiboken6-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:bca3a94513ce9242f7d4bbdca902072a1631888e0aa3a8711a52cc5dbe93588f"},
|
{file = "shiboken6-6.8.1-cp39-abi3-win_amd64.whl", hash = "sha256:3ea127fd72be113b73cacd70e06687ad6f83c1c888047833c7dcdd5cf8e7f586"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2337,5 +2342,5 @@ gui = ["pyqtdarktheme-fork", "pyside6"]
|
|||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.12,<3.14"
|
python-versions = ">=3.12,<3.13"
|
||||||
content-hash = "ad08ac2eff30520097a409a3461facd636bbee0fc4228fa2131c53f4684bb5c6"
|
content-hash = "04e7d96d0b9a2e4777c9c2ad4305b81704acfec50ab7dfceca91666e985a57d7"
|
||||||
|
|||||||
+3
-5
@@ -10,9 +10,8 @@ classifiers = [
|
|||||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
]
|
]
|
||||||
requires-python = ">=3.12,<3.14"
|
requires-python = ">=3.12,<3.13"
|
||||||
dynamic = ["dependencies"]
|
dynamic = ["dependencies"]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -35,9 +34,8 @@ dataclasses-json = "^0.6.7"
|
|||||||
pathvalidate = "^3.3.1"
|
pathvalidate = "^3.3.1"
|
||||||
m3u8 = "^6.0.0"
|
m3u8 = "^6.0.0"
|
||||||
coloredlogs = "^15.0.1"
|
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 }
|
pyqtdarktheme-fork = { version = "^2.3.4", optional = true }
|
||||||
mpegdash = "^0.4.0"
|
|
||||||
rich = "^14.0.0"
|
rich = "^14.0.0"
|
||||||
toml = "^0.10.2"
|
toml = "^0.10.2"
|
||||||
typer = "^0.16.0"
|
typer = "^0.16.0"
|
||||||
@@ -141,7 +139,7 @@ ignore = [
|
|||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
# https://github.com/psf/black
|
# https://github.com/psf/black
|
||||||
target-version = ["py312", "py313"]
|
target-version = ["py312"]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
color = true
|
color = true
|
||||||
preview = true
|
preview = true
|
||||||
|
|||||||
+249
-148
@@ -22,12 +22,6 @@ from tidal_dl_ng.constants import CTX_TIDAL, MediaType
|
|||||||
from tidal_dl_ng.download import Download
|
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.path import get_format_template, path_file_settings
|
||||||
from tidal_dl_ng.helper.tidal import (
|
from tidal_dl_ng.helper.tidal import (
|
||||||
Album,
|
|
||||||
Artist,
|
|
||||||
Mix,
|
|
||||||
Playlist,
|
|
||||||
Track,
|
|
||||||
Video,
|
|
||||||
all_artist_album_ids,
|
all_artist_album_ids,
|
||||||
get_tidal_media_id,
|
get_tidal_media_id,
|
||||||
get_tidal_media_type,
|
get_tidal_media_type,
|
||||||
@@ -47,104 +41,33 @@ app.add_typer(dl_fav_group, name="dl_fav")
|
|||||||
|
|
||||||
|
|
||||||
def version_callback(value: bool):
|
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:
|
if value:
|
||||||
print(f"{__version__}")
|
print(f"{__version__}")
|
||||||
|
|
||||||
raise typer.Exit()
|
raise typer.Exit()
|
||||||
|
|
||||||
|
|
||||||
def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bool:
|
def _handle_track_or_video(
|
||||||
"""Invokes download function and tracks progress.
|
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.
|
||||||
|
|
||||||
:param ctx: The typer context object.
|
Args:
|
||||||
:type ctx: typer.Context
|
dl (Download): The Download instance.
|
||||||
:param urls: The list of URLs to download.
|
ctx (typer.Context): Typer context object.
|
||||||
:type urls: list[str]
|
item (str): The URL or identifier of the item.
|
||||||
:param try_login: If true, attempts to login to TIDAL.
|
media: The media object to download.
|
||||||
:type try_login: bool
|
file_template (str): The file template for saving the media.
|
||||||
:return: True if ran successfully.
|
idx (int): The index of the item in the list.
|
||||||
:rtype: bool
|
urls_pos_last (int): The last index in the URLs list.
|
||||||
"""
|
"""
|
||||||
if try_login:
|
settings = ctx.obj[CTX_TIDAL].settings
|
||||||
# Call login method to validate the token.
|
download_delay: bool = bool(settings.data.download_delay and idx < urls_pos_last)
|
||||||
ctx.invoke(login, ctx)
|
|
||||||
|
|
||||||
# Create initial objects.
|
|
||||||
settings: Settings = Settings()
|
|
||||||
handling_app: HandlingApp = HandlingApp()
|
|
||||||
progress: Progress = Progress(
|
|
||||||
TextColumn("[progress.description]{task.description}"),
|
|
||||||
SpinnerColumn(),
|
|
||||||
BarColumn(),
|
|
||||||
TaskProgressColumn(),
|
|
||||||
refresh_per_second=20,
|
|
||||||
auto_refresh=True,
|
|
||||||
expand=True,
|
|
||||||
transient=False, # Prevent progress from disappearing
|
|
||||||
)
|
|
||||||
progress_overall = Progress(
|
|
||||||
TextColumn("[progress.description]{task.description}"),
|
|
||||||
SpinnerColumn(),
|
|
||||||
BarColumn(),
|
|
||||||
TaskProgressColumn(),
|
|
||||||
refresh_per_second=20,
|
|
||||||
auto_refresh=True,
|
|
||||||
expand=True,
|
|
||||||
transient=False, # Prevent progress from disappearing
|
|
||||||
)
|
|
||||||
fn_logger = LoggerWrapped(progress.print)
|
|
||||||
dl = Download(
|
|
||||||
session=ctx.obj[CTX_TIDAL].session,
|
|
||||||
skip_existing=ctx.obj[CTX_TIDAL].settings.data.skip_existing,
|
|
||||||
path_base=settings.data.download_base_path,
|
|
||||||
fn_logger=fn_logger,
|
|
||||||
progress=progress,
|
|
||||||
progress_overall=progress_overall,
|
|
||||||
event_abort=handling_app.event_abort,
|
|
||||||
event_run=handling_app.event_run,
|
|
||||||
)
|
|
||||||
progress_table = Table.grid()
|
|
||||||
|
|
||||||
# Style Progress display.
|
|
||||||
progress_table.add_row(progress)
|
|
||||||
progress_table.add_row(progress_overall)
|
|
||||||
|
|
||||||
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():
|
|
||||||
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(
|
dl.item(
|
||||||
media=media,
|
media=media,
|
||||||
@@ -153,29 +76,180 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
|
|||||||
quality_audio=settings.data.quality_audio,
|
quality_audio=settings.data.quality_audio,
|
||||||
quality_video=settings.data.quality_video,
|
quality_video=settings.data.quality_video,
|
||||||
)
|
)
|
||||||
elif media_type in [MediaType.ALBUM, MediaType.PLAYLIST, MediaType.MIX, MediaType.ARTIST]:
|
|
||||||
item_ids: [int] = []
|
|
||||||
|
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:
|
if media_type == MediaType.ARTIST:
|
||||||
media_type = MediaType.ALBUM
|
media_type = MediaType.ALBUM
|
||||||
item_ids = item_ids + all_artist_album_ids(media)
|
item_ids += all_artist_album_ids(media)
|
||||||
else:
|
else:
|
||||||
item_ids.append(item_id)
|
item_ids.append(item_id)
|
||||||
|
|
||||||
for item_id in item_ids:
|
for _item_id in item_ids:
|
||||||
# Exit loop if abort signal is set.
|
|
||||||
if handling_app.event_abort.is_set():
|
if handling_app.event_abort.is_set():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
dl.items(
|
dl.items(
|
||||||
media_id=item_id,
|
media_id=_item_id,
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
file_template=file_template,
|
file_template=file_template,
|
||||||
video_download=ctx.obj[CTX_TIDAL].settings.data.video_download,
|
video_download=settings.data.video_download,
|
||||||
download_delay=settings.data.download_delay,
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
ctx.invoke(login, ctx)
|
||||||
|
|
||||||
|
settings: Settings = ctx.obj[CTX_TIDAL].settings
|
||||||
|
handling_app: HandlingApp = HandlingApp()
|
||||||
|
|
||||||
|
progress: Progress = Progress(
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
SpinnerColumn(),
|
||||||
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
refresh_per_second=20,
|
||||||
|
auto_refresh=True,
|
||||||
|
expand=True,
|
||||||
|
transient=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
progress_overall = Progress(
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
SpinnerColumn(),
|
||||||
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
refresh_per_second=20,
|
||||||
|
auto_refresh=True,
|
||||||
|
expand=True,
|
||||||
|
transient=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
fn_logger = LoggerWrapped(progress.print)
|
||||||
|
|
||||||
|
dl = Download(
|
||||||
|
session=ctx.obj[CTX_TIDAL].session,
|
||||||
|
skip_existing=settings.data.skip_existing,
|
||||||
|
path_base=settings.data.download_base_path,
|
||||||
|
fn_logger=fn_logger,
|
||||||
|
progress=progress,
|
||||||
|
progress_overall=progress_overall,
|
||||||
|
event_abort=handling_app.event_abort,
|
||||||
|
event_run=handling_app.event_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
progress_table = Table.grid()
|
||||||
|
progress_table.add_row(progress)
|
||||||
|
progress_table.add_row(progress_overall)
|
||||||
|
progress_group = Group(progress_table)
|
||||||
|
|
||||||
|
urls_pos_last = len(urls) - 1
|
||||||
|
|
||||||
|
with Live(progress_group, refresh_per_second=20, vertical_overflow="visible"):
|
||||||
|
try:
|
||||||
|
for idx, item in enumerate(urls):
|
||||||
|
if _process_url(dl, ctx, handling_app, item, idx, urls_pos_last) is False:
|
||||||
|
return False
|
||||||
finally:
|
finally:
|
||||||
# Clear and stop progress display
|
|
||||||
progress.refresh()
|
progress.refresh()
|
||||||
progress.stop()
|
progress.stop()
|
||||||
|
|
||||||
@@ -187,6 +261,12 @@ def callback_app(
|
|||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
version: Annotated[bool | None, typer.Option("--version", "-v", callback=version_callback, is_eager=True)] = None,
|
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}
|
ctx.obj = {"tidal": None}
|
||||||
|
|
||||||
|
|
||||||
@@ -196,16 +276,12 @@ def settings_management(
|
|||||||
editor: Annotated[
|
editor: Annotated[
|
||||||
bool, typer.Option("--editor", "-e", help="Open the settings file in your default editor.")
|
bool, typer.Option("--editor", "-e", help="Open the settings file in your default editor.")
|
||||||
] = False,
|
] = False,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""Print or set an option, or open the settings file in an editor.
|
||||||
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
|
|
||||||
|
|
||||||
:param editor: If set, your favorite system editor will be opened.
|
Args:
|
||||||
:param names: (Optional) None (list all options), one (list the value only for this option) or two arguments
|
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.
|
||||||
(set the value for the option).
|
editor (bool, optional): If set, your favorite system editor will be opened. Defaults to False.
|
||||||
"""
|
"""
|
||||||
if editor:
|
if editor:
|
||||||
config_path: Path = Path(path_file_settings())
|
config_path: Path = Path(path_file_settings())
|
||||||
@@ -223,8 +299,7 @@ def settings_management(
|
|||||||
if names:
|
if names:
|
||||||
if names[0] not in d_settings:
|
if names[0] not in d_settings:
|
||||||
print(f'Option "{names[0]}" is not valid!')
|
print(f'Option "{names[0]}" is not valid!')
|
||||||
else:
|
elif len(names) == 1:
|
||||||
if len(names) == 1:
|
|
||||||
print(f'{names[0]}: "{d_settings[names[0]]}"')
|
print(f'{names[0]}: "{d_settings[names[0]]}"')
|
||||||
elif len(names) > 1:
|
elif len(names) > 1:
|
||||||
settings.set_option(names[0], names[1])
|
settings.set_option(names[0], names[1])
|
||||||
@@ -236,7 +311,6 @@ def settings_management(
|
|||||||
table.add_column("Value", style="magenta")
|
table.add_column("Value", style="magenta")
|
||||||
table.add_column("Description", style="green")
|
table.add_column("Description", style="green")
|
||||||
|
|
||||||
# Iterate over the attributes of the dataclass
|
|
||||||
for key, value in sorted(d_settings.items()):
|
for key, value in sorted(d_settings.items()):
|
||||||
table.add_row(key, str(value), help_settings[key])
|
table.add_row(key, str(value), help_settings[key])
|
||||||
|
|
||||||
@@ -246,6 +320,14 @@ def settings_management(
|
|||||||
|
|
||||||
@app.command(name="login")
|
@app.command(name="login")
|
||||||
def login(ctx: typer.Context) -> bool:
|
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="")
|
print("Let us check, if you are already logged in... ", end="")
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
@@ -258,6 +340,11 @@ def login(ctx: typer.Context) -> bool:
|
|||||||
|
|
||||||
@app.command(name="logout")
|
@app.command(name="logout")
|
||||||
def logout() -> bool:
|
def logout() -> bool:
|
||||||
|
"""Logout from TIDAL.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if logout was successful, False otherwise.
|
||||||
|
"""
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
tidal = Tidal(settings)
|
tidal = Tidal(settings)
|
||||||
result = tidal.logout()
|
result = tidal.logout()
|
||||||
@@ -287,6 +374,16 @@ def download(
|
|||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
) -> bool:
|
) -> 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:
|
if not urls:
|
||||||
# Read the text file provided.
|
# Read the text file provided.
|
||||||
if file_urls:
|
if file_urls:
|
||||||
@@ -307,10 +404,11 @@ def download(
|
|||||||
def download_fav_tracks(ctx: typer.Context) -> bool:
|
def download_fav_tracks(ctx: typer.Context) -> bool:
|
||||||
"""Download your favorite track collection.
|
"""Download your favorite track collection.
|
||||||
|
|
||||||
:param ctx: Typer context object.
|
Args:
|
||||||
:type ctx: typer.Context
|
ctx (typer.Context): Typer context object.
|
||||||
:return: Download result.
|
|
||||||
:rtype: bool
|
Returns:
|
||||||
|
bool: Download result.
|
||||||
"""
|
"""
|
||||||
# Method name
|
# Method name
|
||||||
func_name_favorites: str = "tracks"
|
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:
|
def download_fav_artists(ctx: typer.Context) -> bool:
|
||||||
"""Download your favorite artist collection.
|
"""Download your favorite artist collection.
|
||||||
|
|
||||||
:param ctx: Typer context object.
|
Args:
|
||||||
:type ctx: typer.Context
|
ctx (typer.Context): Typer context object.
|
||||||
:return: Download result.
|
|
||||||
:rtype: bool
|
Returns:
|
||||||
|
bool: Download result.
|
||||||
"""
|
"""
|
||||||
# Method name
|
# Method name
|
||||||
func_name_favorites: str = "artists"
|
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:
|
def download_fav_albums(ctx: typer.Context) -> bool:
|
||||||
"""Download your favorite album collection.
|
"""Download your favorite album collection.
|
||||||
|
|
||||||
:param ctx: Typer context object.
|
Args:
|
||||||
:type ctx: typer.Context
|
ctx (typer.Context): Typer context object.
|
||||||
:return: Download result.
|
|
||||||
:rtype: bool
|
Returns:
|
||||||
|
bool: Download result.
|
||||||
"""
|
"""
|
||||||
# Method name
|
# Method name
|
||||||
func_name_favorites: str = "albums"
|
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:
|
def download_fav_videos(ctx: typer.Context) -> bool:
|
||||||
"""Download your favorite video collection.
|
"""Download your favorite video collection.
|
||||||
|
|
||||||
:param ctx: Typer context object.
|
Args:
|
||||||
:type ctx: typer.Context
|
ctx (typer.Context): Typer context object.
|
||||||
:return: Download result.
|
|
||||||
:rtype: bool
|
Returns:
|
||||||
|
bool: Download result.
|
||||||
"""
|
"""
|
||||||
# Method name
|
# Method name
|
||||||
func_name_favorites: str = "videos"
|
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:
|
def _download_fav_factory(ctx: typer.Context, func_name_favorites: str) -> bool:
|
||||||
"""Factory which helps to download items from the favorites collections.
|
"""Factory which helps to download items from the favorites collections.
|
||||||
|
|
||||||
:param ctx: Typer context object.
|
Args:
|
||||||
:type ctx: typer.Context
|
ctx (typer.Context): Typer context object.
|
||||||
:param func_name_favorites: Method name to call from `tidalapi` favorites object.
|
func_name_favorites (str): Method name to call from `tidalapi` favorites object.
|
||||||
:type func_name_favorites: str
|
|
||||||
:return: Download result.
|
Returns:
|
||||||
:rtype: bool
|
bool: Download result.
|
||||||
"""
|
"""
|
||||||
# Call login method to validate the token.
|
|
||||||
ctx.invoke(login, ctx)
|
ctx.invoke(login, ctx)
|
||||||
|
|
||||||
# Get the method from the module
|
|
||||||
func_favorites: Callable = getattr(ctx.obj[CTX_TIDAL].session.user.favorites, func_name_favorites)
|
func_favorites: Callable = getattr(ctx.obj[CTX_TIDAL].session.user.favorites, func_name_favorites)
|
||||||
# Get favorite videos
|
media_urls: list[str] = [media.share_url for media in func_favorites()]
|
||||||
media_urls: [str] = [media.share_url for media in func_favorites()]
|
|
||||||
|
|
||||||
return _download(ctx, media_urls, try_login=False)
|
return _download(ctx, media_urls, try_login=False)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def gui(ctx: typer.Context):
|
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
|
from tidal_dl_ng.gui import gui_activate
|
||||||
|
|
||||||
ctx.invoke(login, ctx)
|
ctx.invoke(login, ctx)
|
||||||
@@ -404,9 +505,9 @@ def gui(ctx: typer.Context):
|
|||||||
def handle_sigint_term(signum, frame):
|
def handle_sigint_term(signum, frame):
|
||||||
"""Set app abort event, so threads can check it and shutdown.
|
"""Set app abort event, so threads can check it and shutdown.
|
||||||
|
|
||||||
:param signum:
|
Args:
|
||||||
:param frame:
|
signum: Signal number.
|
||||||
:return:
|
frame: Current stack frame.
|
||||||
"""
|
"""
|
||||||
handling_app: HandlingApp = HandlingApp()
|
handling_app: HandlingApp = HandlingApp()
|
||||||
|
|
||||||
|
|||||||
+842
-158
File diff suppressed because it is too large
Load Diff
+560
-112
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,17 @@ from Crypto.Util import Counter
|
|||||||
|
|
||||||
def decrypt_security_token(security_token: str) -> (str, str):
|
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
|
# Do not change this
|
||||||
|
|||||||
+387
-184
@@ -24,6 +24,11 @@ from tidal_dl_ng.helper.tidal import name_builder_album_artist, name_builder_art
|
|||||||
|
|
||||||
|
|
||||||
def path_home() -> str:
|
def path_home() -> str:
|
||||||
|
"""Get the home directory path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The home directory path.
|
||||||
|
"""
|
||||||
if "XDG_CONFIG_HOME" in os.environ:
|
if "XDG_CONFIG_HOME" in os.environ:
|
||||||
return os.environ["XDG_CONFIG_HOME"]
|
return os.environ["XDG_CONFIG_HOME"]
|
||||||
elif "HOME" in os.environ:
|
elif "HOME" in os.environ:
|
||||||
@@ -35,6 +40,11 @@ def path_home() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def path_config_base() -> 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
|
# https://wiki.archlinux.org/title/XDG_Base_Directory
|
||||||
# X11 workaround: If user specified config path is set, do not point to "~/.config"
|
# X11 workaround: If user specified config path is set, do not point to "~/.config"
|
||||||
path_user_custom: str = os.environ.get("XDG_CONFIG_HOME", "")
|
path_user_custom: str = os.environ.get("XDG_CONFIG_HOME", "")
|
||||||
@@ -45,14 +55,29 @@ def path_config_base() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def path_file_log() -> 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")
|
return os.path.join(path_config_base(), "app.log")
|
||||||
|
|
||||||
|
|
||||||
def path_file_token() -> str:
|
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")
|
return os.path.join(path_config_base(), "token.json")
|
||||||
|
|
||||||
|
|
||||||
def path_file_settings() -> str:
|
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")
|
return os.path.join(path_config_base(), "settings.json")
|
||||||
|
|
||||||
|
|
||||||
@@ -91,130 +116,300 @@ def format_str_media(
|
|||||||
list_pos: int = 0,
|
list_pos: int = 0,
|
||||||
list_total: int = 0,
|
list_total: int = 0,
|
||||||
) -> str:
|
) -> 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:
|
try:
|
||||||
match name:
|
# Try each formatter function in sequence
|
||||||
case "artist_name":
|
for formatter in (
|
||||||
if isinstance(media, Track | Video):
|
_format_names,
|
||||||
if hasattr(media, "artists"):
|
_format_numbers,
|
||||||
result = name_builder_artist(media)
|
_format_ids,
|
||||||
elif hasattr(media, "artist"):
|
_format_durations,
|
||||||
result = media.artist.name
|
_format_metadata,
|
||||||
case "album_artist":
|
_format_volumes,
|
||||||
result = name_builder_album_artist(media, first_only=True)
|
):
|
||||||
case "album_artists":
|
result = formatter(name, media, album_track_num_pad_min, list_pos, list_total)
|
||||||
result = name_builder_album_artist(media)
|
if result is not None:
|
||||||
case "track_title":
|
return result
|
||||||
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)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TODO: Implement better exception logging.
|
# TODO: Implement better exception logging.
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
pass
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
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
|
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:
|
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
|
result: str
|
||||||
|
|
||||||
if items_max > 0:
|
if items_max > 0:
|
||||||
count_digits: int = int(math.log10(items_max)) + 1
|
count_digits = max(int(math.log10(items_max)) + 1, padding_minimum)
|
||||||
count_digits_computed: int = count_digits if count_digits > padding_minimum else padding_minimum
|
result = str(item_position).zfill(count_digits)
|
||||||
result = str(item_position).zfill(count_digits_computed)
|
|
||||||
else:
|
else:
|
||||||
result = str(item_position)
|
result = str(item_position)
|
||||||
|
|
||||||
@@ -224,6 +419,15 @@ def calculate_number_padding(padding_minimum: int, item_position: int, items_max
|
|||||||
def get_format_template(
|
def get_format_template(
|
||||||
media: Track | Album | Playlist | UserPlaylist | Video | Mix | MediaType, settings
|
media: Track | Album | Playlist | UserPlaylist | Video | Mix | MediaType, settings
|
||||||
) -> str | bool:
|
) -> 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
|
result = False
|
||||||
|
|
||||||
if isinstance(media, Track) or media == MediaType.TRACK:
|
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:
|
def path_file_sanitize(path_file: pathlib.Path, adapt: bool = False, uniquify: bool = False) -> pathlib.Path:
|
||||||
sanitized_filename: str = path_file.name
|
"""Sanitize a file path to ensure it is valid and optionally make it unique.
|
||||||
sanitized_path: pathlib.Path = path_file.parent
|
|
||||||
result: pathlib.Path
|
|
||||||
|
|
||||||
# Sanitize filename and make sure it does not exceed FILENAME_LENGTH_MAX
|
Args:
|
||||||
try:
|
path_file (pathlib.Path): The file path to sanitize.
|
||||||
# sanitize_filename can shorten the file name actually
|
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(
|
sanitized_filename = sanitize_filename(
|
||||||
sanitized_filename, replacement_text="_", validate_after_sanitize=True, platform="auto"
|
path_file.name, replacement_text="_", validate_after_sanitize=True, platform="auto"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if the file extension was removed by shortening the filename length
|
|
||||||
if not sanitized_filename.endswith(path_file.suffix):
|
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 = (
|
||||||
sanitized_filename[: -byte_ct - len(FILENAME_SANITIZE_PLACEHOLDER) - len(path_file.suffix)]
|
sanitized_filename[: -len(path_file.suffix)] + FILENAME_SANITIZE_PLACEHOLDER + path_file.suffix
|
||||||
+ FILENAME_SANITIZE_PLACEHOLDER
|
|
||||||
+ path_file.suffix
|
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Sanitize the path.
|
sanitized_path = pathlib.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(
|
|
||||||
sanitize_filename(part, replacement_text="_", validate_after_sanitize=True, platform="auto")
|
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:
|
try:
|
||||||
sanitized_path: pathlib.Path = sanitize_filepath(
|
sanitized_path = sanitize_filepath(
|
||||||
sanitized_path, replacement_text="_", validate_after_sanitize=True, platform="auto"
|
sanitized_path, replacement_text="_", validate_after_sanitize=True, platform="auto"
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
# If adaption of path is allowed in case of an error set path to HOME.
|
if adapt and str(e).startswith("[PV1101]"):
|
||||||
if adapt:
|
|
||||||
if str(e).startswith("[PV1101]"):
|
|
||||||
sanitized_path = pathlib.Path.home()
|
sanitized_path = pathlib.Path.home()
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
result = sanitized_path / sanitized_filename
|
result = sanitized_path / sanitized_filename
|
||||||
|
|
||||||
# Uniquify
|
return path_file_uniquify(result) if uniquify else result
|
||||||
if uniquify:
|
|
||||||
result = path_file_uniquify(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def path_file_uniquify(path_file: pathlib.Path) -> pathlib.Path:
|
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.
|
Args:
|
||||||
:type path_file: pathlib.Path
|
path_file (pathlib.Path): The file path to uniquify.
|
||||||
:return: Unique file name with path for given input.
|
|
||||||
:rtype: pathlib.Path
|
Returns:
|
||||||
|
pathlib.Path: The unique file path.
|
||||||
"""
|
"""
|
||||||
unique_suffix: str = file_unique_suffix(path_file)
|
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
|
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))
|
threshold_zfill: int = len(str(UNIQUIFY_THRESHOLD))
|
||||||
count: int = 0
|
count: int = 0
|
||||||
path_file_tmp: pathlib.Path = deepcopy(path_file)
|
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:
|
while check_file_exists(path_file_tmp) and count < UNIQUIFY_THRESHOLD:
|
||||||
count += 1
|
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)
|
path_file_tmp = path_file.parent / (path_file.stem + unique_suffix + path_file.suffix)
|
||||||
|
|
||||||
return unique_suffix
|
return unique_suffix
|
||||||
|
|
||||||
|
|
||||||
def check_file_exists(path_file: pathlib.Path, extension_ignore: bool = False) -> bool:
|
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:
|
if extension_ignore:
|
||||||
path_file_stem: str = pathlib.Path(path_file).stem
|
path_file_stem: str = pathlib.Path(path_file).stem
|
||||||
path_parent: pathlib.Path = pathlib.Path(path_file).parent
|
path_parent: pathlib.Path = pathlib.Path(path_file).parent
|
||||||
path_files: [str] = []
|
path_files: list[str] = []
|
||||||
|
|
||||||
for extension in AudioExtensions:
|
path_files.extend(str(path_parent.joinpath(path_file_stem + extension)) for extension in AudioExtensions)
|
||||||
path_files.append(str(path_parent.joinpath(path_file_stem + extension)))
|
|
||||||
else:
|
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 any(os.path.isfile(_file) for _file in path_files)
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def resource_path(relative_path):
|
def resource_path(relative_path: str) -> str:
|
||||||
try:
|
"""Get the absolute path to a resource.
|
||||||
base_path = sys._MEIPASS
|
|
||||||
except Exception:
|
Args:
|
||||||
base_path = os.path.abspath(".")
|
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)
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
|
|
||||||
def url_to_filename(url: str) -> str:
|
def url_to_filename(url: str) -> str:
|
||||||
"""Return basename corresponding to url.
|
"""Convert a URL to a valid filename.
|
||||||
>>> print(url_to_filename('http://example.com/path/to/file%C3%80?opt=1'))
|
|
||||||
fileÀ
|
Args:
|
||||||
>>> print(url_to_filename('http://example.com/slash%2fname')) # '/' in name
|
url (str): The URL to convert.
|
||||||
Taken from https://gist.github.com/zed/c2168b9c52b032b5fb7d
|
|
||||||
Traceback (most recent call last):
|
Returns:
|
||||||
...
|
str: The corresponding filename.
|
||||||
ValueError
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the URL contains invalid characters for a filename.
|
||||||
"""
|
"""
|
||||||
urlpath: str = urlsplit(url).path
|
urlpath: str = urlsplit(url).path
|
||||||
basename: str = posixpath.basename(unquote(urlpath))
|
basename: str = posixpath.basename(unquote(urlpath))
|
||||||
|
|||||||
Reference in New Issue
Block a user