✨ Merge with master.
This commit is contained in:
Generated
+148
-121
@@ -97,13 +97,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "bandit"
|
||||
version = "1.7.10"
|
||||
version = "1.8.0"
|
||||
description = "Security oriented static analyser for python code."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"},
|
||||
{file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"},
|
||||
{file = "bandit-1.8.0-py3-none-any.whl", hash = "sha256:b1a61d829c0968aed625381e426aa378904b996529d048f8d908fa28f6b13e38"},
|
||||
{file = "bandit-1.8.0.tar.gz", hash = "sha256:b5bfe55a095abd9fe20099178a7c6c060f844bfd4fe4c76d28e35e4c52b9d31e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -367,73 +367,73 @@ cron = ["capturer (>=2.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.4"
|
||||
version = "7.6.8"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"},
|
||||
{file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"},
|
||||
{file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"},
|
||||
{file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"},
|
||||
{file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -636,13 +636,13 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.2"
|
||||
version = "2.6.3"
|
||||
description = "File identification library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"},
|
||||
{file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"},
|
||||
{file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"},
|
||||
{file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -971,13 +971,13 @@ pyyaml = ">=5.1"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.5.44"
|
||||
version = "9.5.46"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca"},
|
||||
{file = "mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0"},
|
||||
{file = "mkdocs_material-9.5.46-py3-none-any.whl", hash = "sha256:98f0a2039c62e551a68aad0791a8d41324ff90c03a6e6cea381a384b84908b83"},
|
||||
{file = "mkdocs_material-9.5.46.tar.gz", hash = "sha256:ae2043f4238e572f9a40e0b577f50400d6fc31e2fef8ea141800aebf3bd273d7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1011,24 +1011,24 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings"
|
||||
version = "0.24.3"
|
||||
version = "0.27.0"
|
||||
description = "Automatic documentation from sources, for MkDocs."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "mkdocstrings-0.24.3-py3-none-any.whl", hash = "sha256:5c9cf2a32958cd161d5428699b79c8b0988856b0d4a8c5baf8395fc1bf4087c3"},
|
||||
{file = "mkdocstrings-0.24.3.tar.gz", hash = "sha256:f327b234eb8d2551a306735436e157d0a22d45f79963c60a8b585d5f7a94c1d2"},
|
||||
{file = "mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332"},
|
||||
{file = "mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=7.0"
|
||||
Jinja2 = ">=2.11.1"
|
||||
Markdown = ">=3.3"
|
||||
Markdown = ">=3.6"
|
||||
MarkupSafe = ">=1.1"
|
||||
mkdocs = ">=1.4"
|
||||
mkdocs-autorefs = ">=0.3.1"
|
||||
mkdocs-autorefs = ">=1.2"
|
||||
mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
|
||||
platformdirs = ">=2.2.0"
|
||||
platformdirs = ">=2.2"
|
||||
pymdown-extensions = ">=6.3"
|
||||
|
||||
[package.extras]
|
||||
@@ -1038,18 +1038,19 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings-python"
|
||||
version = "1.10.0"
|
||||
version = "1.12.2"
|
||||
description = "A Python handler for mkdocstrings."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "mkdocstrings_python-1.10.0-py3-none-any.whl", hash = "sha256:ba833fbd9d178a4b9d5cb2553a4df06e51dc1f51e41559a4d2398c16a6f69ecc"},
|
||||
{file = "mkdocstrings_python-1.10.0.tar.gz", hash = "sha256:71678fac657d4d2bb301eed4e4d2d91499c095fd1f8a90fa76422a87a5693828"},
|
||||
{file = "mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a"},
|
||||
{file = "mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
griffe = ">=0.44"
|
||||
mkdocstrings = ">=0.24.2"
|
||||
griffe = ">=0.49"
|
||||
mkdocs-autorefs = ">=1.2"
|
||||
mkdocstrings = ">=0.26"
|
||||
|
||||
[[package]]
|
||||
name = "mpegdash"
|
||||
@@ -1940,23 +1941,23 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "75.5.0"
|
||||
version = "75.6.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"},
|
||||
{file = "setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef"},
|
||||
{file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"},
|
||||
{file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"]
|
||||
core = ["importlib-metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
|
||||
core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
|
||||
type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"]
|
||||
type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
@@ -2006,13 +2007,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "stevedore"
|
||||
version = "5.3.0"
|
||||
version = "5.4.0"
|
||||
description = "Manage dynamic plugins for Python applications"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"},
|
||||
{file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"},
|
||||
{file = "stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857"},
|
||||
{file = "stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2020,26 +2021,22 @@ pbr = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "tidalapi"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
description = "Unofficial API for TIDAL music streaming service."
|
||||
optional = false
|
||||
python-versions = "^3.8"
|
||||
files = []
|
||||
develop = false
|
||||
python-versions = "<4.0,>=3.8"
|
||||
files = [
|
||||
{file = "tidalapi-0.8.2-py3-none-any.whl", hash = "sha256:46998ecb9103aee6d1346ad2974df05fe9bc03a429a89d5a701571fe4d9c4525"},
|
||||
{file = "tidalapi-0.8.2.tar.gz", hash = "sha256:9eab07291f92f506694b6d326dd1e6651349eaa71597cef634a62915312db38c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
isodate = "^0.7.2"
|
||||
mpegdash = "^0.4.0"
|
||||
python-dateutil = "^2.8.2"
|
||||
ratelimit = "^2.2.1"
|
||||
requests = "^2.32.3"
|
||||
typing-extensions = "^4.12.2"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "git@github.com:exislow/python-tidal.git"
|
||||
reference = "302-return-raw-values"
|
||||
resolved_reference = "c543cca8db60730038c52db3329c81cec27e340d"
|
||||
isodate = ">=0.7.2,<0.8.0"
|
||||
mpegdash = ">=0.4.0,<0.5.0"
|
||||
python-dateutil = ">=2.8.2,<3.0.0"
|
||||
ratelimit = ">=2.2.1,<3.0.0"
|
||||
requests = ">=2.32.3,<3.0.0"
|
||||
typing-extensions = ">=4.12.2,<5.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "tokenize-rt"
|
||||
@@ -2065,13 +2062,43 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.1.0"
|
||||
version = "2.2.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
|
||||
{file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
|
||||
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
|
||||
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2114,13 +2141,13 @@ test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.12.5"
|
||||
version = "0.14.0"
|
||||
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"},
|
||||
{file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"},
|
||||
{file = "typer-0.14.0-py3-none-any.whl", hash = "sha256:f476233a25770ab3e7b2eebf7c68f3bc702031681a008b20167573a4b7018f09"},
|
||||
{file = "typer-0.14.0.tar.gz", hash = "sha256:af58f737f8d0c0c37b9f955a6d39000b9ff97813afcbeef56af5e37cf743b45a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2174,13 +2201,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.27.1"
|
||||
version = "20.28.0"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"},
|
||||
{file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"},
|
||||
{file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"},
|
||||
{file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2240,4 +2267,4 @@ gui = ["pyqtdarktheme-fork", "pyside6"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.13"
|
||||
content-hash = "8cd14249efdfe09f484027075e96b19112fccb535cd0da53229de4823fa87c9a"
|
||||
content-hash = "5d06aa39f3567f397125c1d081fbde9f3ca9eb7f454385cd02bdaeac3dbfd8e1"
|
||||
|
||||
+9
-8
@@ -1,7 +1,7 @@
|
||||
[tool.poetry]
|
||||
name = "tidal-dl-ng"
|
||||
authors = ["Robert Honz <cen.sored@gmail.com>"]
|
||||
version = "0.15.6"
|
||||
version = "0.20.3"
|
||||
description = "TIDAL Medial Downloader Next Generation!"
|
||||
repository = "https://github.com/exislow/tidal-dl-ng"
|
||||
documentation = "https://exislow.github.io/tidal-dl-ng/"
|
||||
@@ -33,14 +33,14 @@ dataclasses-json = "^0.6.7"
|
||||
pathvalidate = "^3.2.1"
|
||||
m3u8 = "^6.0.0"
|
||||
coloredlogs = "^15.0.1"
|
||||
pyside6 = {version = "^6.8.0.2", optional = true}
|
||||
pyside6 = {version = "^6.7.0", optional = true}
|
||||
pyqtdarktheme-fork = {version = "^2.3.2", optional = true}
|
||||
mpegdash = "^0.4.0"
|
||||
rich = "^13.9.4"
|
||||
toml = "^0.10.2"
|
||||
typer = "^0.12.5"
|
||||
typer = "^0.14.0"
|
||||
ffmpeg-python = "^0.2.0"
|
||||
tidalapi = { git = "git@github.com:exislow/python-tidal.git", branch = "302-return-raw-values" }
|
||||
tidalapi = "^0.8.2"
|
||||
|
||||
[tool.poetry.extras]
|
||||
gui = ["pyside6", "pyqtdarktheme-fork"]
|
||||
@@ -70,9 +70,10 @@ black = "^24.10.0"
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs = "^1.5.3"
|
||||
mkdocs-material = "^9.4.14"
|
||||
mkdocstrings = { extras = ["python"], version = "^0.24.0" }
|
||||
mkdocs = "^1.6.1"
|
||||
mkdocs-material = "^9.5.44"
|
||||
mkdocstrings = { extras = ["python"], version = "^0.27.0" }
|
||||
griffe = "^1.5.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
@@ -171,7 +172,7 @@ profile = "black"
|
||||
multi_line_output = 3
|
||||
indent = 4
|
||||
color_output = true
|
||||
known_third_party = ["Crypto", "PySide6", "coloredlogs", "constants", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"]
|
||||
known_third_party = ["Crypto", "PySide6", "coloredlogs", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"]
|
||||
|
||||
[tool.mypy]
|
||||
# https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file
|
||||
|
||||
@@ -6,6 +6,9 @@ EXTENSION_LYRICS: str = ".lrc"
|
||||
UNIQUIFY_THRESHOLD: int = 99
|
||||
FILENAME_SANITIZE_PLACEHOLDER: str = "_"
|
||||
COVER_NAME: str = "cover.jpg"
|
||||
BLOCK_SIZE: int = 4096
|
||||
BLOCKS: int = 1024
|
||||
CHUNK_SIZE: int = BLOCK_SIZE * BLOCKS
|
||||
|
||||
|
||||
class QualityVideo(StrEnum):
|
||||
@@ -24,13 +27,6 @@ class MediaType(StrEnum):
|
||||
ARTIST: str = "artist"
|
||||
|
||||
|
||||
class SkipExisting(StrEnum):
|
||||
Disabled: str = "False"
|
||||
Filename: str = "exact"
|
||||
ExtensionIgnore: str = "extension_ignore"
|
||||
Append: str = "append"
|
||||
|
||||
|
||||
class CoverDimensions(StrEnum):
|
||||
Px80: str = "80"
|
||||
Px160: str = "160"
|
||||
|
||||
@@ -10,7 +10,7 @@ from tidalapi import Quality as QualityAudio
|
||||
|
||||
from tidal_dl_ng import __version__
|
||||
from tidal_dl_ng.config import Settings
|
||||
from tidal_dl_ng.constants import CoverDimensions, QualityVideo, SkipExisting
|
||||
from tidal_dl_ng.constants import CoverDimensions, QualityVideo
|
||||
from tidal_dl_ng.model.cfg import HelpSettings
|
||||
from tidal_dl_ng.model.cfg import Settings as ModelSettings
|
||||
from tidal_dl_ng.model.meta import ReleaseLatest
|
||||
@@ -159,7 +159,6 @@ class DialogPreferences(QtWidgets.QDialog):
|
||||
|
||||
def _init_comboboxes(self):
|
||||
self.parameters_combo = [
|
||||
("skip_existing", SkipExisting),
|
||||
("quality_audio", QualityAudio),
|
||||
("quality_video", QualityVideo),
|
||||
("metadata_cover_dimension", CoverDimensions),
|
||||
@@ -175,6 +174,7 @@ class DialogPreferences(QtWidgets.QDialog):
|
||||
"extract_flac",
|
||||
"metadata_cover_embed",
|
||||
"cover_album_file",
|
||||
"skip_existing",
|
||||
]
|
||||
|
||||
def gui_populate(self):
|
||||
|
||||
+235
-121
@@ -5,22 +5,31 @@ import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from concurrent import futures
|
||||
from uuid import uuid4
|
||||
|
||||
import ffmpeg
|
||||
import m3u8
|
||||
import requests
|
||||
from constants import COVER_NAME
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
from requests.exceptions import HTTPError
|
||||
from rich.progress import Progress, TaskID
|
||||
from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video
|
||||
from tidalapi.exceptions import TooManyRequests
|
||||
from tidalapi.media import AudioExtensions, Codec, Quality, StreamManifest, VideoExtensions
|
||||
|
||||
from tidal_dl_ng.config import Settings
|
||||
from tidal_dl_ng.constants import EXTENSION_LYRICS, REQUESTS_TIMEOUT_SEC, MediaType, QualityVideo, SkipExisting
|
||||
from tidal_dl_ng.constants import (
|
||||
CHUNK_SIZE,
|
||||
COVER_NAME,
|
||||
EXTENSION_LYRICS,
|
||||
REQUESTS_TIMEOUT_SEC,
|
||||
MediaType,
|
||||
QualityVideo,
|
||||
)
|
||||
from tidal_dl_ng.helper.decryption import decrypt_file, decrypt_security_token
|
||||
from tidal_dl_ng.helper.exceptions import MediaMissing
|
||||
from tidal_dl_ng.helper.path import check_file_exists, format_path_media, path_file_sanitize
|
||||
from tidal_dl_ng.helper.path import check_file_exists, format_path_media, path_file_sanitize, url_to_filename
|
||||
from tidal_dl_ng.helper.tidal import (
|
||||
instantiate_media,
|
||||
items_results_all,
|
||||
@@ -30,6 +39,7 @@ from tidal_dl_ng.helper.tidal import (
|
||||
name_builder_title,
|
||||
)
|
||||
from tidal_dl_ng.metadata import Metadata
|
||||
from tidal_dl_ng.model.downloader import DownloadSegmentResult
|
||||
from tidal_dl_ng.model.gui_data import ProgressBars
|
||||
|
||||
|
||||
@@ -51,7 +61,7 @@ class RequestsClient:
|
||||
class Download:
|
||||
settings: Settings
|
||||
session: Session
|
||||
skip_existing: SkipExisting = SkipExisting.Disabled
|
||||
skip_existing: bool = False
|
||||
fn_logger: Callable
|
||||
progress_gui: ProgressBars
|
||||
progress: Progress
|
||||
@@ -61,7 +71,7 @@ class Download:
|
||||
session: Session,
|
||||
path_base: str,
|
||||
fn_logger: Callable,
|
||||
skip_existing: SkipExisting = SkipExisting.Disabled,
|
||||
skip_existing: bool = False,
|
||||
progress_gui: ProgressBars = None,
|
||||
progress: Progress = None,
|
||||
):
|
||||
@@ -88,27 +98,34 @@ class Download:
|
||||
def _download(
|
||||
self,
|
||||
media: Track | Video,
|
||||
path_file: str,
|
||||
) -> str:
|
||||
stream_manifest: StreamManifest,
|
||||
path_file: pathlib.Path,
|
||||
) -> (bool, pathlib.Path):
|
||||
media_name: str = name_builder_item(media)
|
||||
urls: [str]
|
||||
path_base: pathlib.Path = path_file.parent
|
||||
result_segments: bool = True
|
||||
dl_segment_results: [DownloadSegmentResult] = []
|
||||
result_merge: bool = False
|
||||
|
||||
# Get urls for media.
|
||||
try:
|
||||
if isinstance(media, Track):
|
||||
urls = media.get_stream().get_stream_manifest().urls
|
||||
stream_manifest: StreamManifest = media.get_stream().get_stream_manifest()
|
||||
urls = stream_manifest.get_urls()
|
||||
elif isinstance(media, Video):
|
||||
m3u8_variant: m3u8.M3U8 = m3u8.load(media.get_url())
|
||||
# Find the desired video resolution or the next best one.
|
||||
m3u8_playlist, codecs = self._extract_video_stream(m3u8_variant, int(self.settings.data.quality_video))
|
||||
# Populate urls.
|
||||
urls = m3u8_playlist.files
|
||||
except Exception:
|
||||
return False, path_file
|
||||
|
||||
# Set the correct progress output channel.
|
||||
if self.progress_gui is None:
|
||||
progress_stdout: bool = True
|
||||
progress_to_stdout: bool = True
|
||||
else:
|
||||
progress_stdout: bool = False
|
||||
progress_to_stdout: bool = False
|
||||
# Send signal to GUI with media name
|
||||
self.progress_gui.item_name.emit(media_name[:30])
|
||||
|
||||
@@ -119,8 +136,11 @@ class Download:
|
||||
progress_total: int = urls_count
|
||||
block_size: int | None = None
|
||||
elif urls_count == 1:
|
||||
# Will be computed later.
|
||||
progress_total: float = None
|
||||
# Get file size and compute progress steps
|
||||
r = requests.head(urls[0], timeout=REQUESTS_TIMEOUT_SEC)
|
||||
total_size_in_bytes: int = int(r.headers.get("content-length", 0))
|
||||
block_size: int | None = 1048576
|
||||
progress_total: float = total_size_in_bytes / block_size
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
@@ -128,57 +148,115 @@ class Download:
|
||||
p_task: TaskID = self.progress.add_task(
|
||||
f"[blue]Item '{media_name[:30]}'",
|
||||
total=progress_total,
|
||||
visible=progress_stdout,
|
||||
visible=progress_to_stdout,
|
||||
)
|
||||
|
||||
# Write content to file until progress is finished.
|
||||
# Download segments until progress is finished.
|
||||
# TODO: Compute download speed (https://github.com/Textualize/rich/blob/master/examples/downloader.py)
|
||||
while not self.progress.tasks[p_task].finished:
|
||||
with open(path_file, "wb") as f:
|
||||
for url in urls:
|
||||
with futures.ThreadPoolExecutor(
|
||||
max_workers=self.settings.data.downloads_simultaneous_per_track_max
|
||||
) as executor:
|
||||
# Dispatch all download tasks to worker threads
|
||||
l_futures: [any] = [
|
||||
executor.submit(self._download_segment, url, path_base, block_size, p_task, progress_to_stdout)
|
||||
for url in urls
|
||||
]
|
||||
# Report results as they become available
|
||||
for future in futures.as_completed(l_futures):
|
||||
# Retrieve result
|
||||
result_dl_segment: DownloadSegmentResult = future.result()
|
||||
|
||||
dl_segment_results.append(result_dl_segment)
|
||||
|
||||
# check for a link that was skipped
|
||||
if not result_dl_segment.result and (result_dl_segment.url is not urls[-1]):
|
||||
# Sometimes it happens, if a track is very short (< 8 seconds or so), that the last URL in `urls` is
|
||||
# invalid (HTTP Error 500) and not necessary. File won't be corrupt.
|
||||
# If this is NOT the case, but any other URL has resulted in an error,
|
||||
# mark the whole thing as corrupt.
|
||||
result_segments = False
|
||||
self.fn_logger.error(f"Something went wrong while downloading {media_name}. File is corrupt!")
|
||||
|
||||
tmp_path_file_decrypted: pathlib.Path = path_file
|
||||
|
||||
# Only if no error happened while downloading.
|
||||
if result_segments:
|
||||
# Bring list into right order, so segments can be easily merged.
|
||||
dl_segment_results.sort(key=lambda x: x.id_segment)
|
||||
result_merge: bool = self._segments_merge(path_file, dl_segment_results)
|
||||
|
||||
if not result_merge:
|
||||
self.fn_logger.error(f"Something went wrong while writing to {media_name}. File is corrupt!")
|
||||
elif result_merge and isinstance(media, Track) and stream_manifest.is_encrypted:
|
||||
key, nonce = decrypt_security_token(stream_manifest.encryption_key)
|
||||
tmp_path_file_decrypted = path_file.with_suffix(".decrypted")
|
||||
decrypt_file(path_file, tmp_path_file_decrypted, key, nonce)
|
||||
|
||||
return result_merge, tmp_path_file_decrypted
|
||||
|
||||
def _segments_merge(self, path_file, dl_segment_results) -> bool:
|
||||
result: bool
|
||||
|
||||
# Copy the content of all segments into one file.
|
||||
try:
|
||||
with path_file.open("wb") as f_target:
|
||||
for dl_segment_result in dl_segment_results:
|
||||
with dl_segment_result.path_segment.open("rb") as f_segment:
|
||||
# Read and write junks, which gives better HDD write performance
|
||||
while segment := f_segment.read(CHUNK_SIZE):
|
||||
f_target.write(segment)
|
||||
|
||||
# Delete segment from HDD
|
||||
dl_segment_result.path_segment.unlink()
|
||||
|
||||
result = True
|
||||
except Exception:
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
def _download_segment(
|
||||
self, url: str, path_base: pathlib.Path, block_size: int | None, p_task: TaskID, progress_to_stdout: bool
|
||||
) -> DownloadSegmentResult:
|
||||
result: bool = False
|
||||
path_segment: pathlib.Path = path_base / url_to_filename(url)
|
||||
# Calculate the segment ID based on the file name within the URL.
|
||||
filename_stem: str = str(path_segment.stem).split("_")[-1]
|
||||
# CAUTION: This is a workaround, so BTS (LOW quality) track will work. They usually have only ONE link.
|
||||
id_segment: int = int(filename_stem) if filename_stem.isdecimal() else 0
|
||||
error: HTTPError | None = None
|
||||
|
||||
# Retry download on failed segments, with an exponential delay between retries
|
||||
s = requests.Session()
|
||||
retries = Retry(total=5, backoff_factor=1) # , status_forcelist=[ 502, 503, 504 ])
|
||||
|
||||
s.mount("https://", HTTPAdapter(max_retries=retries))
|
||||
|
||||
try:
|
||||
# Create the request object with stream=True, so the content won't be loaded into memory at once.
|
||||
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT_SEC)
|
||||
r = s.get(url, stream=True, timeout=REQUESTS_TIMEOUT_SEC)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
# Compute progress iterations based on the file size and update task details.
|
||||
if not progress_total:
|
||||
# Get file size and compute progress steps
|
||||
total_size_in_bytes: int = int(r.headers.get("content-length", 0))
|
||||
block_size: int | None = 1048576
|
||||
progress_total: float = total_size_in_bytes / block_size
|
||||
|
||||
self.progress.update(p_task, total=progress_total)
|
||||
|
||||
# Write the content to disk. If `chunk_size` is set to `None` the whole file will be written at once.
|
||||
with path_segment.open("wb") as f:
|
||||
for data in r.iter_content(chunk_size=block_size):
|
||||
f.write(data)
|
||||
# Advance progress bar.
|
||||
self.progress.advance(p_task)
|
||||
except HTTPError as e:
|
||||
if url is urls[-1]:
|
||||
# It happens, if a track is very short (< 8 seconds or so), that the last URL in `urls` is
|
||||
# invalid (HTTP Error 500) and not necessary. File won't be corrupt.
|
||||
# Thus, advance progress bar to avoid infinity loops.
|
||||
|
||||
result = True
|
||||
except Exception:
|
||||
self.progress.advance(p_task)
|
||||
else:
|
||||
# Finish downloading early and report error.
|
||||
# TODO: The track should somehow be marked as corrupt.
|
||||
self.progress.update(p_task, completed=progress_total)
|
||||
self.fn_logger.error(e)
|
||||
finally:
|
||||
|
||||
# To send the progress to the GUI, we need to emit the percentage.
|
||||
if not progress_stdout:
|
||||
if not progress_to_stdout:
|
||||
self.progress_gui.item.emit(self.progress.tasks[p_task].percentage)
|
||||
|
||||
if isinstance(media, Track) and stream_manifest.is_encrypted:
|
||||
key, nonce = decrypt_security_token(stream_manifest.encryption_key)
|
||||
tmp_path_file_decrypted = path_file + "_decrypted"
|
||||
decrypt_file(path_file, tmp_path_file_decrypted, key, nonce)
|
||||
else:
|
||||
tmp_path_file_decrypted = path_file
|
||||
|
||||
return tmp_path_file_decrypted
|
||||
return DownloadSegmentResult(
|
||||
result=result, url=url, path_segment=path_segment, id_segment=id_segment, error=error
|
||||
)
|
||||
|
||||
def item(
|
||||
self,
|
||||
@@ -219,57 +297,85 @@ class Download:
|
||||
|
||||
return False, ""
|
||||
|
||||
# Create file name and path
|
||||
file_extension_dummy: str = AudioExtensions.FLAC
|
||||
file_name_relative = format_path_media(file_template, media)
|
||||
path_media_dst: pathlib.Path = (
|
||||
(pathlib.Path(self.path_base).expanduser() / (file_name_relative + file_extension_dummy))
|
||||
.resolve()
|
||||
.absolute()
|
||||
)
|
||||
|
||||
# Sanitize final path_file to fit into OS boundaries.
|
||||
path_media_dst = pathlib.Path(path_file_sanitize(str(path_media_dst), adapt=True))
|
||||
|
||||
# Compute if and how downloads need to be skipped.
|
||||
if self.skip_existing:
|
||||
skip_file: bool = check_file_exists(path_media_dst, extension_ignore=True)
|
||||
else:
|
||||
skip_file: bool = False
|
||||
|
||||
if not skip_file:
|
||||
# Get extension.
|
||||
file_extension: str
|
||||
do_flac_extract = False
|
||||
# If a quality is explicitly set, change it and remember the previously set quality.
|
||||
quality_audio_old: Quality = self.adjust_quality_audio(quality_audio) if quality_audio else quality_audio
|
||||
quality_video_old: QualityVideo = (
|
||||
self.adjust_quality_video(quality_video) if quality_video else quality_video
|
||||
)
|
||||
|
||||
try:
|
||||
media_stream = media.get_stream()
|
||||
stream_manifest: StreamManifest = media_stream.get_stream_manifest()
|
||||
except TooManyRequests:
|
||||
self.fn_logger.exception(
|
||||
f"Too many requests against TIDAL backend. Skipping '{name_builder_item(media)}'. "
|
||||
f"Consider to activate delay between downloads."
|
||||
)
|
||||
|
||||
return False, ""
|
||||
except Exception:
|
||||
self.fn_logger.exception(f"Something went wrong. Skipping '{name_builder_item(media)}'.")
|
||||
|
||||
return False, ""
|
||||
|
||||
if isinstance(media, Track):
|
||||
# If a quality is explicitly set, change it.
|
||||
if quality_audio:
|
||||
quality_audio_old: Quality = self.adjust_quality_audio(quality_audio)
|
||||
# TODO: HOTFIX! Go back to
|
||||
# file_extension = media_stream.get_stream_manifest().file_extension
|
||||
# After tidalapi has fixed #304
|
||||
if stream_manifest.file_extension is VideoExtensions.TS:
|
||||
file_extension = stream_manifest.file_extension
|
||||
elif stream_manifest.dash_info and AudioExtensions.FLAC in stream_manifest.dash_info.first_url:
|
||||
file_extension = AudioExtensions.FLAC
|
||||
else:
|
||||
file_extension = AudioExtensions.M4A
|
||||
|
||||
file_extension = media.get_stream().get_stream_manifest().file_extension
|
||||
# Use M4A extension for MP4 audio tracks, because it looks better and is completely interchangeable.
|
||||
file_extension = AudioExtensions.M4A if file_extension == AudioExtensions.MP4 else file_extension
|
||||
|
||||
if self.settings.data.extract_flac:
|
||||
do_flac_extract = False
|
||||
|
||||
if (
|
||||
media.get_stream().get_stream_manifest().codecs.upper() == Codec.FLAC
|
||||
and file_extension != AudioExtensions.FLAC
|
||||
if self.settings.data.extract_flac and (
|
||||
stream_manifest.codecs.upper() == Codec.FLAC and file_extension != AudioExtensions.FLAC
|
||||
):
|
||||
file_extension = AudioExtensions.FLAC
|
||||
do_flac_extract = True
|
||||
elif isinstance(media, Video):
|
||||
if quality_video:
|
||||
quality_video_old: QualityVideo = self.adjust_quality_video(quality_video)
|
||||
|
||||
file_extension = AudioExtensions.MP4 if self.settings.data.video_convert_mp4 else VideoExtensions.TS
|
||||
|
||||
# Create file name and path
|
||||
file_name_relative = format_path_media(file_template, media)
|
||||
path_media_dst = os.path.abspath(
|
||||
os.path.normpath(os.path.join(os.path.expanduser(self.path_base), file_name_relative))
|
||||
)
|
||||
# Compute file name and create destination directory
|
||||
path_media_dst = path_media_dst.with_suffix(file_extension)
|
||||
os.makedirs(path_media_dst.parent, exist_ok=True)
|
||||
|
||||
# Sanitize final path_file to fit into OS boundaries.
|
||||
uniquify: bool = self.skip_existing == SkipExisting.Append
|
||||
path_media_dst = path_file_sanitize(path_media_dst + file_extension, adapt=True, uniquify=uniquify)
|
||||
|
||||
# Compute if and how downloads need to be skipped.
|
||||
if self.skip_existing in (SkipExisting.ExtensionIgnore, SkipExisting.Filename):
|
||||
extension_ignore: bool = self.skip_existing == SkipExisting.ExtensionIgnore
|
||||
file_exists: bool = check_file_exists(path_media_dst, extension_ignore=extension_ignore)
|
||||
else:
|
||||
file_exists: bool = False
|
||||
|
||||
if not file_exists:
|
||||
# Create a temp directory and file.
|
||||
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp_path_dir:
|
||||
tmp_path_file = os.path.join(tmp_path_dir, str(uuid4()))
|
||||
# Download media.
|
||||
tmp_path_file = self._download(media=media, path_file=tmp_path_file)
|
||||
tmp_path_file: pathlib.Path = pathlib.Path(tmp_path_dir) / str(uuid4())
|
||||
|
||||
# Create empty file
|
||||
tmp_path_file.touch()
|
||||
|
||||
# Download media.
|
||||
result_download, tmp_path_file = self._download(
|
||||
media=media, stream_manifest=stream_manifest, path_file=tmp_path_file
|
||||
)
|
||||
|
||||
if result_download:
|
||||
# Convert video from TS to MP4
|
||||
if isinstance(media, Video) and self.settings.data.video_convert_mp4:
|
||||
# Convert `*.ts` file to `*.mp4` using ffmpeg
|
||||
@@ -288,21 +394,20 @@ class Download:
|
||||
media, tmp_path_file, is_parent_album
|
||||
)
|
||||
|
||||
# Move final file to the configured destination directory.
|
||||
os.makedirs(os.path.dirname(path_media_dst), exist_ok=True)
|
||||
shutil.move(tmp_path_file, path_media_dst)
|
||||
|
||||
# Move lyrics file
|
||||
if self.settings.data.lyrics_file and not isinstance(media, Video):
|
||||
if self.settings.data.lyrics_file and not isinstance(media, Video) and tmp_path_lyrics:
|
||||
self._move_lyrics(tmp_path_lyrics, path_media_dst)
|
||||
|
||||
# Move cover file
|
||||
if self.settings.data.cover_album_file:
|
||||
# TODO: Cover is downloaded with every track of the album. Needs refactoring, so cover is only
|
||||
# downloaded for an album once.
|
||||
if self.settings.data.cover_album_file and tmp_path_cover:
|
||||
self._move_cover(tmp_path_cover, path_media_dst)
|
||||
else:
|
||||
self.fn_logger.debug(f"Download skipped, since file exists: '{path_media_dst}'")
|
||||
|
||||
status_download: bool = not file_exists
|
||||
self.fn_logger.info(f"Downloaded item '{name_builder_item(media)}'.")
|
||||
|
||||
# Move final file to the configured destination directory.
|
||||
shutil.move(tmp_path_file, path_media_dst)
|
||||
|
||||
if quality_audio:
|
||||
# Set quality back to the global user value
|
||||
@@ -311,11 +416,20 @@ class Download:
|
||||
if quality_video:
|
||||
# Set quality back to the global user value
|
||||
self.adjust_quality_video(quality_video_old)
|
||||
else:
|
||||
self.fn_logger.debug(f"Download skipped, since file exists: '{path_media_dst}'")
|
||||
|
||||
status_download: bool = not skip_file
|
||||
|
||||
# Whether a file was downloaded or skipped and the download delay is enabled, wait until the next download.
|
||||
# Only use this, if you have a list of several Track items.
|
||||
if download_delay:
|
||||
time_sleep: float = round(random.SystemRandom().uniform(2, 5), 1)
|
||||
if download_delay and not skip_file:
|
||||
time_sleep: float = round(
|
||||
random.SystemRandom().uniform(
|
||||
self.settings.data.download_delay_sec_min, self.settings.data.download_delay_sec_max
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
self.fn_logger.debug(f"Next download will start in {time_sleep} seconds.")
|
||||
time.sleep(time_sleep)
|
||||
@@ -340,7 +454,7 @@ class Download:
|
||||
result: bool
|
||||
|
||||
# Check if the file was downloaded
|
||||
if path_file_source and os.path.isfile(path_file_source):
|
||||
if path_file_source and path_file_source.is_file():
|
||||
# Move it.
|
||||
shutil.move(path_file_source, path_file_destination)
|
||||
|
||||
@@ -350,16 +464,16 @@ class Download:
|
||||
|
||||
return result
|
||||
|
||||
def _move_lyrics(self, path_lyrics: pathlib.Path, file_media_dst: str) -> bool:
|
||||
def _move_lyrics(self, path_lyrics: pathlib.Path, file_media_dst: pathlib.Path) -> bool:
|
||||
# Build tmp lyrics filename
|
||||
path_file_lyrics: str = os.path.splitext(file_media_dst)[0] + EXTENSION_LYRICS
|
||||
path_file_lyrics: pathlib.Path = file_media_dst.with_suffix(EXTENSION_LYRICS)
|
||||
result: bool = self._move_file(path_lyrics, path_file_lyrics)
|
||||
|
||||
return result
|
||||
|
||||
def _move_cover(self, path_cover: pathlib.Path, file_media_dst: str) -> bool:
|
||||
def _move_cover(self, path_cover: pathlib.Path, file_media_dst: pathlib.Path) -> bool:
|
||||
# Build tmp lyrics filename
|
||||
path_file_cover: pathlib.Path = pathlib.Path(file_media_dst).parent.absolute() / COVER_NAME
|
||||
path_file_cover: pathlib.Path = file_media_dst.parent / COVER_NAME
|
||||
result: bool = self._move_file(path_cover, path_file_cover)
|
||||
|
||||
return result
|
||||
@@ -403,7 +517,7 @@ class Download:
|
||||
return result
|
||||
|
||||
def metadata_write(
|
||||
self, track: Track, path_media: str, is_parent_album: str
|
||||
self, track: Track, path_media: pathlib.Path, is_parent_album: bool
|
||||
) -> (bool, pathlib.Path | None, pathlib.Path | None):
|
||||
result: bool = False
|
||||
path_lyrics: pathlib.Path | None = None
|
||||
@@ -433,14 +547,14 @@ class Download:
|
||||
print(f"Could not retrieve lyrics for `{name_builder_item(track)}`.")
|
||||
|
||||
if lyrics and self.settings.data.lyrics_file:
|
||||
path_lyrics = self.lyrics_to_file(pathlib.Path(path_media).parent, lyrics)
|
||||
path_lyrics = self.lyrics_to_file(path_media.parent, lyrics)
|
||||
|
||||
if self.settings.data.metadata_cover_embed or (self.settings.data.cover_album_file and is_parent_album):
|
||||
url_cover = track.album.image(int(self.settings.data.metadata_cover_dimension))
|
||||
cover_data = self.cover_data(url=url_cover)
|
||||
|
||||
if cover_data and self.settings.data.cover_album_file and is_parent_album:
|
||||
path_cover = self.cover_to_file(pathlib.Path(path_media).parent, cover_data)
|
||||
path_cover = self.cover_to_file(path_media.parent, cover_data)
|
||||
|
||||
# `None` values are not allowed.
|
||||
m: Metadata = Metadata(
|
||||
@@ -487,29 +601,22 @@ class Download:
|
||||
file_name_relative = format_path_media(file_template, media)
|
||||
|
||||
# Get the name of the list and check, if videos should be included.
|
||||
videos_include: bool = True
|
||||
|
||||
if isinstance(media, Mix):
|
||||
list_media_name = media.title[:30]
|
||||
elif video_download:
|
||||
list_media_name = name_builder_title(media)[:30]
|
||||
else:
|
||||
videos_include = False
|
||||
list_media_name = name_builder_title(media)[:30]
|
||||
list_media_name: str = name_builder_title(media)
|
||||
list_media_name_short: str = list_media_name[:30]
|
||||
|
||||
# Get all items of the list.
|
||||
items = items_results_all(media, videos_include=videos_include)
|
||||
items = items_results_all(media, videos_include=video_download)
|
||||
|
||||
# Determine where to redirect the progress information.
|
||||
if self.progress_gui is None:
|
||||
progress_stdout: bool = True
|
||||
else:
|
||||
progress_stdout: bool = False
|
||||
self.progress_gui.list_name.emit(list_media_name[:30])
|
||||
self.progress_gui.list_name.emit(list_media_name_short[:30])
|
||||
|
||||
# Create the list progress task.
|
||||
p_task1: TaskID = self.progress.add_task(
|
||||
f"[green]List '{list_media_name}'", total=len(items), visible=progress_stdout
|
||||
f"[green]List '{list_media_name_short}'", total=len(items), visible=progress_stdout
|
||||
)
|
||||
|
||||
is_album: bool = isinstance(media, Album)
|
||||
@@ -518,7 +625,7 @@ class Download:
|
||||
while not self.progress.finished:
|
||||
for item_media in items:
|
||||
# Download the item.
|
||||
status_download, result_path_file = self.item(
|
||||
status, result_path_file = self.item(
|
||||
media=item_media,
|
||||
file_template=file_name_relative,
|
||||
quality_audio=quality_audio,
|
||||
@@ -533,22 +640,29 @@ class Download:
|
||||
if not progress_stdout:
|
||||
self.progress_gui.list_item.emit(self.progress.tasks[p_task1].percentage)
|
||||
|
||||
def _video_convert(self, path_file: str) -> str:
|
||||
path_file_out = path_file + AudioExtensions.MP4
|
||||
self.fn_logger.info(f"Finished list '{list_media_name}'.")
|
||||
|
||||
def _video_convert(self, path_file: pathlib.Path) -> pathlib.Path:
|
||||
path_file_out: pathlib.Path = path_file.with_suffix(AudioExtensions.MP4)
|
||||
result, _ = (
|
||||
ffmpeg.input(path_file)
|
||||
.output(path_file_out, map=0, c="copy", loglevel="quiet")
|
||||
.output(str(path_file_out), map=0, c="copy", loglevel="quiet")
|
||||
.run(cmd=self.settings.data.path_binary_ffmpeg)
|
||||
)
|
||||
|
||||
return path_file_out
|
||||
|
||||
def _extract_flac(self, path_media_src: str) -> str:
|
||||
path_media_out = path_media_src + AudioExtensions.FLAC
|
||||
def _extract_flac(self, path_media_src: pathlib.Path) -> pathlib.Path:
|
||||
path_media_out = path_media_src.with_suffix(AudioExtensions.FLAC)
|
||||
result, _ = (
|
||||
ffmpeg.input(path_media_src)
|
||||
.output(
|
||||
path_media_out, map=0, movflags="use_metadata_tags", acodec="copy", map_metadata="0:g", loglevel="quiet"
|
||||
str(path_media_out),
|
||||
map=0,
|
||||
movflags="use_metadata_tags",
|
||||
acodec="copy",
|
||||
map_metadata="0:g",
|
||||
loglevel="quiet",
|
||||
)
|
||||
.run(cmd=self.settings.data.path_binary_ffmpeg)
|
||||
)
|
||||
|
||||
@@ -516,6 +516,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._init_dl()
|
||||
|
||||
def search(self, query: str, types_media: SearchTypes) -> [ResultItem]:
|
||||
query = query.strip()
|
||||
|
||||
# If a direct link was searched for, skip search and create the object from the link directly.
|
||||
if "http" in query:
|
||||
media_type = get_tidal_media_type(query)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import base64
|
||||
import pathlib
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util import Counter
|
||||
@@ -35,7 +36,7 @@ def decrypt_security_token(security_token: str) -> (str, str):
|
||||
return key, nonce
|
||||
|
||||
|
||||
def decrypt_file(path_file_encrypted: str, path_file_destination: str, key: str, nonce: str) -> None:
|
||||
def decrypt_file(path_file_encrypted: pathlib.Path, path_file_destination: pathlib.Path, key: str, nonce: str) -> None:
|
||||
"""
|
||||
Decrypts an encrypted MQA file given the file, key and nonce.
|
||||
TODO: Is it really only necessary for MQA of for all other formats, too?
|
||||
@@ -46,9 +47,9 @@ def decrypt_file(path_file_encrypted: str, path_file_destination: str, key: str,
|
||||
decryptor = AES.new(key, AES.MODE_CTR, counter=counter)
|
||||
|
||||
# Open and decrypt
|
||||
with open(path_file_encrypted, "rb") as f_src:
|
||||
with path_file_encrypted.open("rb") as f_src:
|
||||
audio_decrypted = decryptor.decrypt(f_src.read())
|
||||
|
||||
# Replace with decrypted file
|
||||
with open(path_file_destination, "wb") as f_dst:
|
||||
with path_file_destination.open("wb") as f_dst:
|
||||
f_dst.write(audio_decrypted)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import math
|
||||
import os
|
||||
import pathlib
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path, PosixPath
|
||||
from urllib.parse import unquote, urlsplit
|
||||
|
||||
from pathvalidate import sanitize_filename, sanitize_filepath
|
||||
from pathvalidate.error import ValidationError
|
||||
@@ -77,7 +79,6 @@ def format_str_media(name: str, media: Track | Album | Playlist | UserPlaylist |
|
||||
elif hasattr(media, "artist"):
|
||||
result = media.artist.name
|
||||
case "album_artist":
|
||||
if isinstance(media, Album):
|
||||
result = name_builder_album_artist(media)
|
||||
case "track_title":
|
||||
if isinstance(media, Track | Video):
|
||||
@@ -195,7 +196,7 @@ def get_format_template(
|
||||
def path_file_sanitize(path_file: str, adapt: bool = False, uniquify: bool = False) -> (bool, str):
|
||||
# Split into path and filename
|
||||
pathname, filename = os.path.split(path_file)
|
||||
file_extension: str = Path(path_file).suffix
|
||||
file_extension: str = pathlib.Path(path_file).suffix
|
||||
|
||||
# Sanitize path
|
||||
try:
|
||||
@@ -205,7 +206,7 @@ def path_file_sanitize(path_file: str, adapt: bool = False, uniquify: bool = Fal
|
||||
except ValidationError:
|
||||
# If adaption of path is allowed in case of an error set path to HOME.
|
||||
if adapt:
|
||||
pathname_sanitized: str = Path.home()
|
||||
pathname_sanitized: str = str(pathlib.Path.home())
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -272,10 +273,10 @@ def file_unique_suffix(path_file: str, seperator: str = "_") -> str:
|
||||
return unique_suffix
|
||||
|
||||
|
||||
def check_file_exists(path_file: str, extension_ignore: bool = False) -> bool:
|
||||
def check_file_exists(path_file: pathlib.Path, extension_ignore: bool = False) -> bool:
|
||||
if extension_ignore:
|
||||
path_file_stem: str = Path(path_file).stem
|
||||
path_parent: PosixPath = Path(path_file).parent
|
||||
path_file_stem: str = pathlib.Path(path_file).stem
|
||||
path_parent: pathlib.Path = pathlib.Path(path_file).parent
|
||||
path_files: [str] = []
|
||||
|
||||
for extension in AudioExtensions:
|
||||
@@ -295,3 +296,22 @@ def resource_path(relative_path):
|
||||
base_path = os.path.abspath(".")
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
|
||||
def url_to_filename(url: str) -> str:
|
||||
"""Return basename corresponding to url.
|
||||
>>> print(url_to_filename('http://example.com/path/to/file%C3%80?opt=1'))
|
||||
fileÀ
|
||||
>>> print(url_to_filename('http://example.com/slash%2fname')) # '/' in name
|
||||
Taken from https://gist.github.com/zed/c2168b9c52b032b5fb7d
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError
|
||||
"""
|
||||
urlpath: str = urlsplit(url).path
|
||||
basename: str = posixpath.basename(unquote(urlpath))
|
||||
|
||||
if os.path.basename(basename) != basename or unquote(posixpath.basename(urlpath)) != basename:
|
||||
raise ValueError # reject '%2f' or 'dir%5Cbasename.ext' on Windows
|
||||
|
||||
return basename
|
||||
|
||||
@@ -25,8 +25,10 @@ def name_builder_album_artist(media: Track | Album) -> str:
|
||||
return ", ".join(artists_tmp)
|
||||
|
||||
|
||||
def name_builder_title(media: Track | Video) -> str:
|
||||
result: str = media.full_name if hasattr(media, "full_name") else media.name
|
||||
def name_builder_title(media: Track | Video | Mix | Playlist | Album | Video) -> str:
|
||||
result: str = (
|
||||
media.title if isinstance(media, Mix) else media.full_name if hasattr(media, "full_name") else media.name
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -186,7 +188,7 @@ def instantiate_media(
|
||||
def quality_audio_highest(media: Track | Album) -> Quality:
|
||||
quality: Quality
|
||||
|
||||
if MediaMetadataTags.hires_lossless in media.media_metadata_tags:
|
||||
if MediaMetadataTags.hi_res_lossless in media.media_metadata_tags:
|
||||
quality = Quality.hi_res_lossless
|
||||
elif MediaMetadataTags.lossless in media.media_metadata_tags:
|
||||
quality = Quality.high_lossless
|
||||
|
||||
@@ -21,3 +21,6 @@ class LoggerWrapped:
|
||||
|
||||
def critical(self, value):
|
||||
self.fn_print(value)
|
||||
|
||||
def exception(self, value):
|
||||
self.fn_print(value)
|
||||
|
||||
@@ -111,6 +111,8 @@ class Metadata:
|
||||
self.m.tags["LYRICS"] = self.lyrics
|
||||
|
||||
def set_mp3(self):
|
||||
# ID3 Frame (tags) overview: https://exiftool.org/TagNames/ID3.html / https://id3.org/id3v2.3.0
|
||||
# Mapping overview: https://docs.mp3tag.de/mapping/
|
||||
self.m.tags.add(TIT2(encoding=3, text=self.title))
|
||||
self.m.tags.add(TALB(encoding=3, text=self.album))
|
||||
self.m.tags.add(TOPE(encoding=3, text=self.albumartist))
|
||||
|
||||
@@ -3,13 +3,13 @@ from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json
|
||||
from tidalapi import Quality
|
||||
|
||||
from tidal_dl_ng.constants import CoverDimensions, QualityVideo, SkipExisting
|
||||
from tidal_dl_ng.constants import CoverDimensions, QualityVideo
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class Settings:
|
||||
skip_existing: SkipExisting = SkipExisting.Disabled
|
||||
skip_existing: bool = True
|
||||
lyrics_embed: bool = False
|
||||
lyrics_file: bool = False
|
||||
# TODO: Implement API KEY selection.
|
||||
@@ -25,7 +25,7 @@ class Settings:
|
||||
quality_video: QualityVideo = QualityVideo.P480
|
||||
format_album: str = (
|
||||
"Albums/{album_artist} - {album_title}{album_explicit}/{track_volume_num_optional}"
|
||||
"{album_track_num}. {artist_name} - {track_title}"
|
||||
"{album_track_num}. {artist_name} - {track_title}{album_explicit}"
|
||||
)
|
||||
format_playlist: str = "Playlists/{playlist_name}/{artist_name} - {track_title}"
|
||||
format_mix: str = "Mix/{mix_name}/{artist_name} - {track_title}"
|
||||
@@ -37,16 +37,15 @@ class Settings:
|
||||
metadata_cover_embed: bool = True
|
||||
cover_album_file: bool = True
|
||||
extract_flac: bool = True
|
||||
downloads_simultaneous_per_track_max: int = 20
|
||||
download_delay_sec_min: float = 3.0
|
||||
download_delay_sec_max: float = 5.0
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class HelpSettings:
|
||||
skip_existing: str = (
|
||||
"Do not download, if file already exists. Possible option false = do not skip, "
|
||||
"'exact' = if filename already exists, 'extension_ignore' = skip even if a file with a "
|
||||
"different file extension exists."
|
||||
)
|
||||
skip_existing: str = "Skip download if file already exists."
|
||||
album_cover_save: str = "Safe cover to album folder."
|
||||
lyrics_embed: str = "Embed lyrics in audio file, if lyrics are available."
|
||||
lyrics_file: str = "Save lyrics to separate *.lrc file, if lyrics are available."
|
||||
@@ -82,6 +81,9 @@ class HelpSettings:
|
||||
metadata_cover_embed: str = "Embed album cover into file."
|
||||
cover_album_file: str = "Save cover to 'cover.jpg', if an album is downloaded."
|
||||
extract_flac: str = "Extract FLAC audio tracks from MP4 containers and save them as `*.flac` (uses FFmpeg)."
|
||||
downloads_simultaneous_per_track_max: str = "Maximum number of simultaneous chunk downloads per track."
|
||||
download_delay_sec_min: float = "Lower boundary for the calculation of the download delay in seconds."
|
||||
download_delay_sec_max: float = "Upper boundary for the calculation of the download delay in seconds."
|
||||
|
||||
|
||||
@dataclass_json
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import pathlib
|
||||
from dataclasses import dataclass
|
||||
|
||||
from requests import HTTPError
|
||||
|
||||
|
||||
@dataclass
|
||||
class DownloadSegmentResult:
|
||||
result: bool
|
||||
url: str
|
||||
path_segment: pathlib.Path
|
||||
id_segment: int
|
||||
error: HTTPError | None = None
|
||||
@@ -153,6 +153,24 @@ class Ui_DialogSettings:
|
||||
|
||||
self.lv_flags.addLayout(self.lh_flags_4)
|
||||
|
||||
self.horizontalLayout = QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.lv_flag_skip_existing = QVBoxLayout()
|
||||
self.lv_flag_skip_existing.setObjectName("lv_flag_skip_existing")
|
||||
self.cb_skip_existing = QCheckBox(self.gb_flags)
|
||||
self.cb_skip_existing.setObjectName("cb_skip_existing")
|
||||
|
||||
self.lv_flag_skip_existing.addWidget(self.cb_skip_existing)
|
||||
|
||||
self.horizontalLayout.addLayout(self.lv_flag_skip_existing)
|
||||
|
||||
self.verticalLayout_3 = QVBoxLayout()
|
||||
self.verticalLayout_3.setObjectName("verticalLayout_3")
|
||||
|
||||
self.horizontalLayout.addLayout(self.verticalLayout_3)
|
||||
|
||||
self.lv_flags.addLayout(self.horizontalLayout)
|
||||
|
||||
self.lv_main.addWidget(self.gb_flags)
|
||||
|
||||
self.gb_choices = QGroupBox(DialogSettings)
|
||||
@@ -164,38 +182,13 @@ class Ui_DialogSettings:
|
||||
self.gb_choices.setSizePolicy(sizePolicy3)
|
||||
self.lv_choices = QVBoxLayout(self.gb_choices)
|
||||
self.lv_choices.setObjectName("lv_choices")
|
||||
self.lh_choices_skip_existing = QHBoxLayout()
|
||||
self.lh_choices_skip_existing.setObjectName("lh_choices_skip_existing")
|
||||
self.l_icon_skip_existing = QLabel(self.gb_choices)
|
||||
self.l_icon_skip_existing.setObjectName("l_icon_skip_existing")
|
||||
sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
sizePolicy4.setHorizontalStretch(0)
|
||||
sizePolicy4.setVerticalStretch(0)
|
||||
sizePolicy4.setHeightForWidth(self.l_icon_skip_existing.sizePolicy().hasHeightForWidth())
|
||||
self.l_icon_skip_existing.setSizePolicy(sizePolicy4)
|
||||
|
||||
self.lh_choices_skip_existing.addWidget(self.l_icon_skip_existing)
|
||||
|
||||
self.l_skip_existing = QLabel(self.gb_choices)
|
||||
self.l_skip_existing.setObjectName("l_skip_existing")
|
||||
sizePolicy4.setHeightForWidth(self.l_skip_existing.sizePolicy().hasHeightForWidth())
|
||||
self.l_skip_existing.setSizePolicy(sizePolicy4)
|
||||
|
||||
self.lh_choices_skip_existing.addWidget(self.l_skip_existing)
|
||||
|
||||
self.c_skip_existing = QComboBox(self.gb_choices)
|
||||
self.c_skip_existing.setObjectName("c_skip_existing")
|
||||
|
||||
self.lh_choices_skip_existing.addWidget(self.c_skip_existing)
|
||||
|
||||
self.lh_choices_skip_existing.setStretch(2, 50)
|
||||
|
||||
self.lv_choices.addLayout(self.lh_choices_skip_existing)
|
||||
|
||||
self.lh_choices_quality_audio = QHBoxLayout()
|
||||
self.lh_choices_quality_audio.setObjectName("lh_choices_quality_audio")
|
||||
self.l_icon_quality_audio = QLabel(self.gb_choices)
|
||||
self.l_icon_quality_audio.setObjectName("l_icon_quality_audio")
|
||||
sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
sizePolicy4.setHorizontalStretch(0)
|
||||
sizePolicy4.setVerticalStretch(0)
|
||||
sizePolicy4.setHeightForWidth(self.l_icon_quality_audio.sizePolicy().hasHeightForWidth())
|
||||
self.l_icon_quality_audio.setSizePolicy(sizePolicy4)
|
||||
|
||||
@@ -532,9 +525,8 @@ class Ui_DialogSettings:
|
||||
self.cb_extract_flac.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None))
|
||||
self.cb_metadata_cover_embed.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None))
|
||||
self.cb_cover_album_file.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None))
|
||||
self.cb_skip_existing.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None))
|
||||
self.gb_choices.setTitle(QCoreApplication.translate("DialogSettings", "Choices", None))
|
||||
self.l_icon_skip_existing.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
|
||||
self.l_skip_existing.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
|
||||
self.l_icon_quality_audio.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
|
||||
self.l_quality_audio.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
|
||||
self.l_icon_quality_video.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
|
||||
|
||||
@@ -210,6 +210,24 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="lv_flag_skip_existing">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cb_skip_existing">
|
||||
<property name="text">
|
||||
<string>CheckBox</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -225,39 +243,6 @@
|
||||
<string>Choices</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="lv_choices">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="lh_choices_skip_existing" stretch="0,0,50">
|
||||
<item>
|
||||
<widget class="QLabel" name="l_icon_skip_existing">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="l_skip_existing">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="c_skip_existing"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="lh_choices_quality_audio" stretch="0,0,50">
|
||||
<item>
|
||||
|
||||
Reference in New Issue
Block a user