diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3696f9c5..e5cdd5ae 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -22,6 +22,7 @@
"consoletitle",
"cookiesfrombrowser",
"copyts",
+ "cronsim",
"daterange",
"dotenv",
"finaldir",
diff --git a/Pipfile.lock b/Pipfile.lock
index ff81629d..692862c6 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -35,91 +35,91 @@
},
"aiohttp": {
"hashes": [
- "sha256:04eb541ce1e03edc1e3be1917a0f45ac703e913c21a940111df73a2c2db11d73",
- "sha256:05582cb2d156ac7506e68b5eac83179faedad74522ed88f88e5861b78740dc0e",
- "sha256:0a29be28e60e5610d2437b5b2fed61d6f3dcde898b57fb048aa5079271e7f6f3",
- "sha256:0b2501f1b981e70932b4a552fc9b3c942991c7ae429ea117e8fba57718cdeed0",
- "sha256:0df3788187559c262922846087e36228b75987f3ae31dd0a1e5ee1034090d42f",
- "sha256:12c5869e7ddf6b4b1f2109702b3cd7515667b437da90a5a4a50ba1354fe41881",
- "sha256:14fc03508359334edc76d35b2821832f092c8f092e4b356e74e38419dfe7b6de",
- "sha256:1a7169ded15505f55a87f8f0812c94c9412623c744227b9e51083a72a48b68a5",
- "sha256:1c68e41c4d576cd6aa6c6d2eddfb32b2acfb07ebfbb4f9da991da26633a3db1a",
- "sha256:20412c7cc3720e47a47e63c0005f78c0c2370020f9f4770d7fc0075f397a9fb0",
- "sha256:22a8107896877212130c58f74e64b77f7007cb03cea8698be317272643602d45",
- "sha256:28a3d083819741592685762d51d789e6155411277050d08066537c5edc4066e6",
- "sha256:2b86efe23684b58a88e530c4ab5b20145f102916bbb2d82942cafec7bd36a647",
- "sha256:2d0b46abee5b5737cb479cc9139b29f010a37b1875ee56d142aefc10686a390b",
- "sha256:321238a42ed463848f06e291c4bbfb3d15ba5a79221a82c502da3e23d7525d06",
- "sha256:3a8a0d127c10b8d89e69bbd3430da0f73946d839e65fec00ae48ca7916a31948",
- "sha256:3a8b0321e40a833e381d127be993b7349d1564b756910b28b5f6588a159afef3",
- "sha256:3b420d076a46f41ea48e5fcccb996f517af0d406267e31e6716f480a3d50d65c",
- "sha256:3b512f1de1c688f88dbe1b8bb1283f7fbeb7a2b2b26e743bb2193cbadfa6f307",
- "sha256:413fe39fd929329f697f41ad67936f379cba06fcd4c462b62e5b0f8061ee4a77",
- "sha256:41cf0cefd9e7b5c646c2ef529c8335e7eafd326f444cc1cdb0c47b6bc836f9be",
- "sha256:4848ae31ad44330b30f16c71e4f586cd5402a846b11264c412de99fa768f00f3",
- "sha256:4b0a200e85da5c966277a402736a96457b882360aa15416bf104ca81e6f5807b",
- "sha256:4e2e8ef37d4bc110917d038807ee3af82700a93ab2ba5687afae5271b8bc50ff",
- "sha256:4edcbe34e6dba0136e4cabf7568f5a434d89cc9de5d5155371acda275353d228",
- "sha256:51ba80d473eb780a329d73ac8afa44aa71dfb521693ccea1dea8b9b5c4df45ce",
- "sha256:5409a59d5057f2386bb8b8f8bbcfb6e15505cedd8b2445db510563b5d7ea1186",
- "sha256:572def4aad0a4775af66d5a2b5923c7de0820ecaeeb7987dcbccda2a735a993f",
- "sha256:599b66582f7276ebefbaa38adf37585e636b6a7a73382eb412f7bc0fc55fb73d",
- "sha256:59a05cdc636431f7ce843c7c2f04772437dd816a5289f16440b19441be6511f1",
- "sha256:602d4db80daf4497de93cb1ce00b8fc79969c0a7cf5b67bec96fa939268d806a",
- "sha256:65c75b14ee74e8eeff2886321e76188cbe938d18c85cff349d948430179ad02c",
- "sha256:69bb252bfdca385ccabfd55f4cd740d421dd8c8ad438ded9637d81c228d0da49",
- "sha256:6d3986112e34eaa36e280dc8286b9dd4cc1a5bcf328a7f147453e188f6fe148f",
- "sha256:6dd9766da617855f7e85f27d2bf9a565ace04ba7c387323cd3e651ac4329db91",
- "sha256:70ab0f61c1a73d3e0342cedd9a7321425c27a7067bebeeacd509f96695b875fc",
- "sha256:749f1eb10e51dbbcdba9df2ef457ec060554842eea4d23874a3e26495f9e87b1",
- "sha256:781c8bd423dcc4641298c8c5a2a125c8b1c31e11f828e8d35c1d3a722af4c15a",
- "sha256:7e7abe865504f41b10777ac162c727af14e9f4db9262e3ed8254179053f63e6d",
- "sha256:7f2dadece8b85596ac3ab1ec04b00694bdd62abc31e5618f524648d18d9dd7fa",
- "sha256:86135c32d06927339c8c5e64f96e4eee8825d928374b9b71a3c42379d7437058",
- "sha256:8778620396e554b758b59773ab29c03b55047841d8894c5e335f12bfc45ebd28",
- "sha256:87f0e003fb4dd5810c7fbf47a1239eaa34cd929ef160e0a54c570883125c4831",
- "sha256:8aa5c68e1e68fff7cd3142288101deb4316b51f03d50c92de6ea5ce646e6c71f",
- "sha256:8d14e274828561db91e4178f0057a915f3af1757b94c2ca283cb34cbb6e00b50",
- "sha256:8d1dd75aa4d855c7debaf1ef830ff2dfcc33f893c7db0af2423ee761ebffd22b",
- "sha256:92007c89a8cb7be35befa2732b0b32bf3a394c1b22ef2dff0ef12537d98a7bda",
- "sha256:92868f6512714efd4a6d6cb2bfc4903b997b36b97baea85f744229f18d12755e",
- "sha256:948abc8952aff63de7b2c83bfe3f211c727da3a33c3a5866a0e2cf1ee1aa950f",
- "sha256:95d7787f2bcbf7cb46823036a8d64ccfbc2ffc7d52016b4044d901abceeba3db",
- "sha256:997b57e38aa7dc6caab843c5e042ab557bc83a2f91b7bd302e3c3aebbb9042a1",
- "sha256:99b8bbfc8111826aa8363442c0fc1f5751456b008737ff053570f06a151650b3",
- "sha256:9e73fa341d8b308bb799cf0ab6f55fc0461d27a9fa3e4582755a3d81a6af8c09",
- "sha256:a0d2c04a623ab83963576548ce098baf711a18e2c32c542b62322a0b4584b990",
- "sha256:a40087b82f83bd671cbeb5f582c233d196e9653220404a798798bfc0ee189fff",
- "sha256:ad1f2fb9fe9b585ea4b436d6e998e71b50d2b087b694ab277b30e060c434e5db",
- "sha256:b05774864c87210c531b48dfeb2f7659407c2dda8643104fb4ae5e2c311d12d9",
- "sha256:b41693b7388324b80f9acfabd479bd1c84f0bc7e8f17bab4ecd9675e9ff9c734",
- "sha256:b42dbd097abb44b3f1156b4bf978ec5853840802d6eee2784857be11ee82c6a0",
- "sha256:b4e7c7ec4146a94a307ca4f112802a8e26d969018fabed526efc340d21d3e7d0",
- "sha256:b59d096b5537ec7c85954cb97d821aae35cfccce3357a2cafe85660cc6295628",
- "sha256:b9c60d1de973ca94af02053d9b5111c4fbf97158e139b14f1be68337be267be6",
- "sha256:bccd2cb7aa5a3bfada72681bdb91637094d81639e116eac368f8b3874620a654",
- "sha256:c32593ead1a8c6aabd58f9d7ee706e48beac796bb0cb71d6b60f2c1056f0a65f",
- "sha256:c7571f99525c76a6280f5fe8e194eeb8cb4da55586c3c61c59c33a33f10cfce7",
- "sha256:c8b2df9feac55043759aa89f722a967d977d80f8b5865a4153fc41c93b957efc",
- "sha256:ca9f835cdfedcb3f5947304e85b8ca3ace31eef6346d8027a97f4de5fb687534",
- "sha256:cc9253069158d57e27d47a8453d8a2c5a370dc461374111b5184cf2f147a3cc3",
- "sha256:ced66c5c6ad5bcaf9be54560398654779ec1c3695f1a9cf0ae5e3606694a000a",
- "sha256:d173c0ac508a2175f7c9a115a50db5fd3e35190d96fdd1a17f9cb10a6ab09aa1",
- "sha256:d6edc538c7480fa0a3b2bdd705f8010062d74700198da55d16498e1b49549b9c",
- "sha256:daf20d9c3b12ae0fdf15ed92235e190f8284945563c4b8ad95b2d7a31f331cd3",
- "sha256:dc311634f6f28661a76cbc1c28ecf3b3a70a8edd67b69288ab7ca91058eb5a33",
- "sha256:e2bc827c01f75803de77b134afdbf74fa74b62970eafdf190f3244931d7a5c0d",
- "sha256:e365034c5cf6cf74f57420b57682ea79e19eb29033399dd3f40de4d0171998fa",
- "sha256:e906da0f2bcbf9b26cc2b144929e88cb3bf943dd1942b4e5af066056875c7618",
- "sha256:e9faafa74dbb906b2b6f3eb9942352e9e9db8d583ffed4be618a89bd71a4e914",
- "sha256:ec6cd1954ca2bbf0970f531a628da1b1338f594bf5da7e361e19ba163ecc4f3b",
- "sha256:f296d637a50bb15fb6a229fbb0eb053080e703b53dbfe55b1e4bb1c5ed25d325",
- "sha256:f30fc72daf85486cdcdfc3f5e0aea9255493ef499e31582b34abadbfaafb0965",
- "sha256:fe846f0a98aa9913c2852b630cd39b4098f296e0907dd05f6c7b30d911afa4c3"
+ "sha256:03ce9d2f01aef26cd6aaba2f330273c2364237db2f499b93c3e9f2e249f83cd2",
+ "sha256:05eea49d598c4ece6f285e00464de206f838b48ff21938d5aa9c394e115945b9",
+ "sha256:078b1ce274f967951b42a65d5b7ec115b7886343a5271f2eed330458ea87bb48",
+ "sha256:0987dcf32e4c47f22634d32e4b0ffbc368bbcf2b33b408cd1a3d2dc0a6a5cd34",
+ "sha256:0c7eba0f90e27ec4af64db051f35387fa17128e6eeb58ee0f2318f2627168cc2",
+ "sha256:0e97c1e55f6931f07ecaf53aef8352def8386adfd0cd3caa6429cc40e886d6a9",
+ "sha256:0ec98c22030ea2a430cb11afddda7d4737b7e5c236c704f0d7d15504978598f7",
+ "sha256:18733fa6bbe44698ff96138c1f1d682bbdf0846250a42c25c108eed328fef0d4",
+ "sha256:1bee0f9e2d4088b57243d63afcb06256bd2d9ff683080f51e74fa790c8cfedfd",
+ "sha256:204f6695b47a7d130ddf6680158920825d0d32202a870e0bc56a2ec637935b1a",
+ "sha256:20dda85988a4f506bf5a457b416b238e32ab944a2deb878ddf0af92df9254a9b",
+ "sha256:2241d862dc6a3c0c2662a6934e79078d3a1e51a76c0dca5d65b30f3debee6c9b",
+ "sha256:23397670f3739b6f3c4019da8226190f6cce5ab2008b664ed96a6d1f0fe7f069",
+ "sha256:23857adc919b64bba8a4db0eccb24e53fcaf85633e690ef1581c5562ed58cae7",
+ "sha256:269d145c593a65f78fb9a64dece90341561ddb2e91a96d42681132b2f706c42a",
+ "sha256:291f324f49ecede693dfb4820a412d1388cb10a2214ab60028252b505e105d6f",
+ "sha256:29cce2a7e009494e675018c0b1819a133befbab8629c797276c5d793bbdf1138",
+ "sha256:357355c9d51c8b12bbc7de43b27ce4b51f14cce050e00b5a87d0d5527d779395",
+ "sha256:36a490f1ebe0b982366314c77f02258f87bd5d9bd362839dc6a24188447f37eb",
+ "sha256:379882ab6a40e6e0879ad8e84dca74ddbadff94af4f314b59b7da89c8463a669",
+ "sha256:38368a32530dcdeccfa47544cf66724118d9cc8a889c057e116723ab62994380",
+ "sha256:3cfd9f4aeaec4a75a0b4986a9977ac0a09b3d87ae83415e4b461e86715c80897",
+ "sha256:41f82df6f0f895f0f843bc86762bea45b4d0fe876de49239ffc89d2365426399",
+ "sha256:430f9707f0c1239a92bff7769b0db185ef400278dc63c89f88ed1bd5153aab7a",
+ "sha256:433e7388b3063bba462b3362641988270b087a9ccae22390364f86b37a480c17",
+ "sha256:43625253e3dc018d34867b70909149f15f29eac0382802afe027f2fbf17bcb9c",
+ "sha256:437eee9e057a7907b11e4af2b18df56b6c795b28e0a3ac250691936cf6bf40eb",
+ "sha256:45d8c22c91bdb7417bd4f5119242dbd2e2140c0e9de47342c765f127f897eb57",
+ "sha256:48d790d05c76b6cb93a1259b44d3f0019b61d507f8ebf0d49da3ef5ac858b05d",
+ "sha256:4b03093d4140d926965d23497a059ec59c8c07e602d2489ce5fb990f3a897db4",
+ "sha256:4b85486e8914d4e778343f5322834aada678eaf4c5315e50d41d9b74817ff97b",
+ "sha256:558de86eef9a886e43c6ae5b75cecdce81203da5832d19d11da8de417967d478",
+ "sha256:576b56a34d26ea8a8f0e1a30b8a069ba4ab121fb8eb796d1b89fedda7c74c553",
+ "sha256:5a61df20fa77792e83307e266f76790f7cb67980dd476948948de212ee7ec64c",
+ "sha256:5b5edd482ff0a8585b3f4e8b3681819447324166a43a5588800a5bca340dbf27",
+ "sha256:5bd37d615cd26d09321bd0168305f8508778712cf38aeffeed550274fb48a2ee",
+ "sha256:63f8d6106566f98fcfde7a643c9da52d90679b8592dea76c4adfc3cd5d06d22c",
+ "sha256:65eb40e74e3126cba185da7a78815cf3a30140932193831b3bfd73c79965c723",
+ "sha256:6fd82d0b3f73c59c80dade0ca8e0342de1ee261e147140ade65a465be670e83c",
+ "sha256:71ed71d9431e770550aab27a77ef9d30d33ce6f558ab7818be7205ae6aca635d",
+ "sha256:73a7f6283634dd30f93b9a67c414c00517869478b50361c503535e075fa07eaf",
+ "sha256:74afb637cd06760afe0aa55a3ce82178ef4c950be65935add8f3809f701f36ca",
+ "sha256:79a08d675167c50f41d106d67bbcbc9e86e1b43d305c4b9f982d5c656a94a9ba",
+ "sha256:7ac49c32901489343b4dab064ab520f6b879a03fb4f9667c84620b66f07bed69",
+ "sha256:7cf4b2b5a0f7a738ecd759eaeaef800fc7c57683b7be9d8a43fcb86ca62701b4",
+ "sha256:7e20bd6aa51a5209c9131395e20ce126e8e317c0cf78a8180f026b4d73f018f6",
+ "sha256:825ec92391e3e4ddda74de79ed0f8b443e9b412a0c9c82618ca2163abd875df5",
+ "sha256:82ddf7f642b9c0b08063f3cf4e2818b22901bce8ebad05c232d9e295e77436a0",
+ "sha256:84370ff70c1677ee0c4db40fe2baee6ffc72e9d32def3ff18739c1390c62329f",
+ "sha256:85d73479b79172e7d667b466c170ca6097a334c09ecd83c95c210546031251b5",
+ "sha256:8b4d8d78fbd5290204dcf43957a2184acd5cee358f203f24a3a97f7d7984eeb7",
+ "sha256:8c6e39f0bb2418f839841f92b3cd64077ff5166d724c984ab450ca08d5e51d92",
+ "sha256:94c552a4864ed292dadf1d341213374284a3af72e49bea02e70ce6f07cb37004",
+ "sha256:9b97fae7f75a0b666ce4281627856d1b1a85477f26c2e8b266292e770c17df36",
+ "sha256:a0361cafb50b185356a5f346c169aea1d14783df99e6da714d626b104586e0b5",
+ "sha256:a3739daa52c0cff42f1c40f63b2fe818fcf41019d84c80a7add3224207a7060f",
+ "sha256:a3d706afcc808f6add4208dfa13f911fd93c2a3dab6be484fee4fd0602a0867e",
+ "sha256:a449c48f5b02c0c14f5310881558ca861bff8e00b1f79be4cf6a53f638464c30",
+ "sha256:a50b86eb9cf74fa5b6f1386e08e1520dcbe83d7dfd4c8bf6f2ca72b03d42e79f",
+ "sha256:a6d6d86443580f846ec9cf60f899b7cace34411f2ff5c95db5970047195e5bfa",
+ "sha256:ae3a5d9f2fbe736fec7d24be25c57aa78c2d78d96540439ea68a8abbed9906fc",
+ "sha256:b9b9a1e592ac8fcc4584baea240e41f77415e0de98932fdf19565aa3b6a02d0b",
+ "sha256:be11989cbc0728f81c0d022cef140ef8adb20d3012ad8f0ac61853bef571eb52",
+ "sha256:c28cbae1ce76dc48d0fcccb045ac21d00dcc1b306bb745910cf35585ce89404e",
+ "sha256:c2de66177e087999568638c02639cf0248011b5c7fca69b006947153c534fcab",
+ "sha256:c37aa3eb8eb92f3793f0c1e73f212a76cbc8d363e9990df54b5b7099f75ce740",
+ "sha256:c930064b79cc0eb63678e376b819d546b0e2360264cd7544c32119496f646f35",
+ "sha256:c982b2cbd7b8f4b31e9faf2de09e22b060a6cd0a693f20892dda41a8fb0f46ef",
+ "sha256:d8370d31e6d8ecccd97cd791c466d0acb56527df51b0c105d7ea54c7fcc0f75b",
+ "sha256:d99981304065f4ea407dd7495f74f8b8c10f0e26733f8a47dc174ece73744d14",
+ "sha256:d9c8417a24063f35b526f8bf14f6f4bdea6f3f49850457337b6ea928901adbbc",
+ "sha256:e13767424425bb607931a0b9e703b95d2693650011ef8f0166b4cd80066b66b9",
+ "sha256:e792131352418dde7b0c598e217e89ecf6a26889f46f35f910e5544ffdebf3ae",
+ "sha256:eba325409a0f990f9b43ed18916cbf5b9779bc4c979b8887c444e7be9c38ccca",
+ "sha256:ec3dd04138bd30e6a3403dbd3ab5a5ccfb501597c5a95196cd816936ed55b777",
+ "sha256:edcddb97574402ff7481bc6f70819ba863e77b0be58a840ed5f59d52d2f20a71",
+ "sha256:f00e7540a60460fbeaffff4a7dcf72fe8b710f8280a542a4d798273787c64e72",
+ "sha256:f1b6c639750bf2a228957e25fcab7a7ff11987c543d70bf73223369f0d7bdb27",
+ "sha256:f1e0369f0dc8c895e718ce37147f56d46142d37596be183ab7a34192c5e6e4c5",
+ "sha256:f30e6980ec5d6ad815a233e19e39fe27ea94b1081c31c8aa1df1b629da3737b8",
+ "sha256:fc270fe480e7e425c45054543f58510fe649f70b77f514171909bbfe585105c0"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
- "version": "==3.11.14"
+ "version": "==3.11.15"
},
"aiosignal": {
"hashes": [
@@ -625,101 +625,101 @@
},
"multidict": {
"hashes": [
- "sha256:0085b0afb2446e57050140240a8595846ed64d1cbd26cef936bfab3192c673b8",
- "sha256:042028348dc5a1f2be6c666437042a98a5d24cee50380f4c0902215e5ec41844",
- "sha256:05fefbc3cddc4e36da209a5e49f1094bbece9a581faa7f3589201fd95df40e5d",
- "sha256:063be88bd684782a0715641de853e1e58a2f25b76388538bd62d974777ce9bc2",
- "sha256:07bfa8bc649783e703263f783f73e27fef8cd37baaad4389816cf6a133141331",
- "sha256:08549895e6a799bd551cf276f6e59820aa084f0f90665c0f03dd3a50db5d3c48",
- "sha256:095a2eabe8c43041d3e6c2cb8287a257b5f1801c2d6ebd1dd877424f1e89cf29",
- "sha256:0b183a959fb88ad1be201de2c4bdf52fa8e46e6c185d76201286a97b6f5ee65c",
- "sha256:0c383d28857f66f5aebe3e91d6cf498da73af75fbd51cedbe1adfb85e90c0460",
- "sha256:0d57a01a2a9fa00234aace434d8c131f0ac6e0ac6ef131eda5962d7e79edfb5b",
- "sha256:0dc25a3293c50744796e87048de5e68996104d86d940bb24bc3ec31df281b191",
- "sha256:0e5a644e50ef9fb87878d4d57907f03a12410d2aa3b93b3acdf90a741df52c49",
- "sha256:0f249badb360b0b4d694307ad40f811f83df4da8cef7b68e429e4eea939e49dd",
- "sha256:0f74f2fc51555f4b037ef278efc29a870d327053aba5cb7d86ae572426c7cccc",
- "sha256:125dd82b40f8c06d08d87b3510beaccb88afac94e9ed4a6f6c71362dc7dbb04b",
- "sha256:13551d0e2d7201f0959725a6a769b6f7b9019a168ed96006479c9ac33fe4096b",
- "sha256:14ed9ed1bfedd72a877807c71113deac292bf485159a29025dfdc524c326f3e1",
- "sha256:163f4604e76639f728d127293d24c3e208b445b463168af3d031b92b0998bb90",
- "sha256:19e2819b0b468174de25c0ceed766606a07cedeab132383f1e83b9a4e96ccb4f",
- "sha256:1e2a2193d3aa5cbf5758f6d5680a52aa848e0cf611da324f71e5e48a9695cc86",
- "sha256:1f3c099d3899b14e1ce52262eb82a5f5cb92157bb5106bf627b618c090a0eadc",
- "sha256:214207dcc7a6221d9942f23797fe89144128a71c03632bf713d918db99bd36de",
- "sha256:2325105e16d434749e1be8022f942876a936f9bece4ec41ae244e3d7fae42aaf",
- "sha256:2529ddbdaa424b2c6c2eb668ea684dd6b75b839d0ad4b21aad60c168269478d7",
- "sha256:256d431fe4583c5f1e0f2e9c4d9c22f3a04ae96009b8cfa096da3a8723db0a16",
- "sha256:25bb96338512e2f46f615a2bb7c6012fe92a4a5ebd353e5020836a7e33120349",
- "sha256:2e87f1926e91855ae61769ba3e3f7315120788c099677e0842e697b0bfb659f2",
- "sha256:2fc6af8e39f7496047c7876314f4317736eac82bf85b54c7c76cf1a6f8e35d98",
- "sha256:3157126b028c074951839233647bd0e30df77ef1fedd801b48bdcad242a60f4e",
- "sha256:32c9b4878f48be3e75808ea7e499d6223b1eea6d54c487a66bc10a1871e3dc6a",
- "sha256:32ed748ff9ac682eae7859790d3044b50e3076c7d80e17a44239683769ff485e",
- "sha256:3501621d5e86f1a88521ea65d5cad0a0834c77b26f193747615b7c911e5422d2",
- "sha256:437c33561edb6eb504b5a30203daf81d4a9b727e167e78b0854d9a4e18e8950b",
- "sha256:48d39b1824b8d6ea7de878ef6226efbe0773f9c64333e1125e0efcfdd18a24c7",
- "sha256:4ac3fcf9a2d369bd075b2c2965544036a27ccd277fc3c04f708338cc57533081",
- "sha256:4ccfd74957ef53fa7380aaa1c961f523d582cd5e85a620880ffabd407f8202c0",
- "sha256:52b05e21ff05729fbea9bc20b3a791c3c11da61649ff64cce8257c82a020466d",
- "sha256:5389445f0173c197f4a3613713b5fb3f3879df1ded2a1a2e4bc4b5b9c5441b7e",
- "sha256:5c5e7d2e300d5cb3b2693b6d60d3e8c8e7dd4ebe27cd17c9cb57020cac0acb80",
- "sha256:5d26547423e5e71dcc562c4acdc134b900640a39abd9066d7326a7cc2324c530",
- "sha256:5dd7106d064d05896ce28c97da3f46caa442fe5a43bc26dfb258e90853b39b44",
- "sha256:5f8cb1329f42fadfb40d6211e5ff568d71ab49be36e759345f91c69d1033d633",
- "sha256:61d5541f27533f803a941d3a3f8a3d10ed48c12cf918f557efcbf3cd04ef265c",
- "sha256:639556758c36093b35e2e368ca485dada6afc2bd6a1b1207d85ea6dfc3deab27",
- "sha256:641cf2e3447c9ecff2f7aa6e9eee9eaa286ea65d57b014543a4911ff2799d08a",
- "sha256:6aed763b6a1b28c46c055692836879328f0b334a6d61572ee4113a5d0c859872",
- "sha256:6e2a2d6749e1ff2c9c76a72c6530d5baa601205b14e441e6d98011000f47a7ac",
- "sha256:7243c5a6523c5cfeca76e063efa5f6a656d1d74c8b1fc64b2cd1e84e507f7e2a",
- "sha256:76b34c12b013d813e6cb325e6bd4f9c984db27758b16085926bbe7ceeaace626",
- "sha256:781b5dd1db18c9e9eacc419027b0acb5073bdec9de1675c0be25ceb10e2ad133",
- "sha256:7c611345bbe7cb44aabb877cb94b63e86f2d0db03e382667dbd037866d44b4f8",
- "sha256:83b78c680d4b15d33042d330c2fa31813ca3974197bddb3836a5c635a5fd013f",
- "sha256:84e87a7d75fa36839a3a432286d719975362d230c70ebfa0948549cc38bd5b46",
- "sha256:89b3857652183b8206a891168af47bac10b970d275bba1f6ee46565a758c078d",
- "sha256:8cd1a0644ccaf27e9d2f6d9c9474faabee21f0578fe85225cc5af9a61e1653df",
- "sha256:8de4d42dffd5ced9117af2ce66ba8722402541a3aa98ffdf78dde92badb68932",
- "sha256:94a7bb972178a8bfc4055db80c51efd24baefaced5e51c59b0d598a004e8305d",
- "sha256:98aa8325c7f47183b45588af9c434533196e241be0a4e4ae2190b06d17675c02",
- "sha256:9e658d1373c424457ddf6d55ec1db93c280b8579276bebd1f72f113072df8a5d",
- "sha256:9f49585f4abadd2283034fc605961f40c638635bc60f5162276fec075f2e37a4",
- "sha256:9f6cad071960ba1914fa231677d21b1b4a3acdcce463cee41ea30bc82e6040cf",
- "sha256:a0cc398350ef31167e03f3ca7c19313d4e40a662adcb98a88755e4e861170bdd",
- "sha256:a1133414b771619aa3c3000701c11b2e4624a7f492f12f256aedde97c28331a2",
- "sha256:a33273a541f1e1a8219b2a4ed2de355848ecc0254264915b9290c8d2de1c74e1",
- "sha256:a3c0ff89fe40a152e77b191b83282c9664357dce3004032d42e68c514ceff27e",
- "sha256:a49994481b99cd7dedde07f2e7e93b1d86c01c0fca1c32aded18f10695ae17eb",
- "sha256:abf5b17bc0cf626a8a497d89ac691308dbd825d2ac372aa990b1ca114e470151",
- "sha256:ac380cacdd3b183338ba63a144a34e9044520a6fb30c58aa14077157a033c13e",
- "sha256:ad81012b24b88aad4c70b2cbc2dad84018783221b7f923e926f4690ff8569da3",
- "sha256:b2c00ad31fbc2cbac85d7d0fcf90853b2ca2e69d825a2d3f3edb842ef1544a2c",
- "sha256:b4c153863dd6569f6511845922c53e39c8d61f6e81f228ad5443e690fca403de",
- "sha256:b4f3d66dd0354b79761481fc15bdafaba0b9d9076f1f42cc9ce10d7fcbda205a",
- "sha256:b99aac6bb2c37db336fa03a39b40ed4ef2818bf2dfb9441458165ebe88b793af",
- "sha256:b9f6392d98c0bd70676ae41474e2eecf4c7150cb419237a41f8f96043fcb81d1",
- "sha256:c537da54ce4ff7c15e78ab1292e5799d0d43a2108e006578a57f531866f64025",
- "sha256:ca23db5fb195b5ef4fd1f77ce26cadefdf13dba71dab14dadd29b34d457d7c44",
- "sha256:cc826b9a8176e686b67aa60fd6c6a7047b0461cae5591ea1dc73d28f72332a8a",
- "sha256:cca83a629f77402cfadd58352e394d79a61c8015f1694b83ab72237ec3941f88",
- "sha256:cf8d370b2fea27fb300825ec3984334f7dd54a581bde6456799ba3776915a656",
- "sha256:d1175b0e0d6037fab207f05774a176d71210ebd40b1c51f480a04b65ec5c786d",
- "sha256:d1996ee1330e245cd3aeda0887b4409e3930524c27642b046e4fae88ffa66c5e",
- "sha256:d5a36953389f35f0a4e88dc796048829a2f467c9197265504593f0e420571547",
- "sha256:da51d8928ad8b4244926fe862ba1795f0b6e68ed8c42cd2f822d435db9c2a8f4",
- "sha256:e16e7297f29a544f49340012d6fc08cf14de0ab361c9eb7529f6a57a30cbfda1",
- "sha256:e25b11a0417475f093d0f0809a149aff3943c2c56da50fdf2c3c88d57fe3dfbd",
- "sha256:e4371591e621579cb6da8401e4ea405b33ff25a755874a3567c4075ca63d56e2",
- "sha256:e653d36b1bf48fa78c7fcebb5fa679342e025121ace8c87ab05c1cefd33b34fc",
- "sha256:e7d91a230c7f8af86c904a5a992b8c064b66330544693fd6759c3d6162382ecf",
- "sha256:e851e6363d0dbe515d8de81fd544a2c956fdec6f8a049739562286727d4a00c3",
- "sha256:ef7d48207926edbf8b16b336f779c557dd8f5a33035a85db9c4b0febb0706817",
- "sha256:f7716f7e7138252d88607228ce40be22660d6608d20fd365d596e7ca0738e019",
- "sha256:facaf11f21f3a4c51b62931feb13310e6fe3475f85e20d9c9fdce0d2ea561b87"
+ "sha256:02a239ad4e35d3c033e701bc8b7beb05013fc6617b13948497cf50e9e9c12f7f",
+ "sha256:091f3dfe63e9f48ae6c7da8d638e8d8e050ff11e348f69bafc20e65d11f7aa48",
+ "sha256:0a1a5f50ccc57a317db68582411ce6aca076ee3bc386290b416a5783e807afa1",
+ "sha256:0ba7d12ff77cadb59519aba674cac3a0b16d0fc5abaa3ef79b5ff3806e52e991",
+ "sha256:0c4a6ec469c30f4162745d6485b778432c497032b8a2cb3a0332eea9d3b6aef6",
+ "sha256:150cc33d5d972c1bd7b0e5c446cccfa4dc6b8e185ef54dab3327db101149f797",
+ "sha256:162af9c2e0743465a47e2b32d9b0a7c260b7843629b5e6f0a8a92819b5a40d27",
+ "sha256:168a02178b7e980899f3475ff008436eaab035d50e39cb8f7de641bbe9bbc3a6",
+ "sha256:1aa6e1a605c602c119a22368f7d3b9dfb102a7cd69edae5a89e55da34fd64c2b",
+ "sha256:1b293804f4b53be6297bc2afdeaf15aff76c1b6be69f3a3da785143eebdfb656",
+ "sha256:1c8836280abc310ea6260dac93126645d21284885008c317c2ac4281a90f1398",
+ "sha256:2264957a0e0fb92d8d538a27738178c48628c65ef0ec6b068d64d2e5d11cc00d",
+ "sha256:257bd1540b7c32982b17e86f024985b9c591da8542a3fb350ed7dad42d725998",
+ "sha256:299070f503a9a99e4af38d07da784ced28377cc62b678084b9e2e48fa51c57d3",
+ "sha256:2a83896925e3a2ddec736c48255572e6f15a95339024c02b800dab28e63461a3",
+ "sha256:2cef02d3878804909283549bc10d4789a14c610fcd286f17cd94a195b21fe469",
+ "sha256:2cf3e0781febf9f093eff3eca2d6dd7954ef2969ff46f6cd95173a4db8397fd8",
+ "sha256:2e87a8635a7db577617b50bd2f2080744ed20e556750b97e4f9988e6d20d3941",
+ "sha256:31c68e103b253643cf9b4704f5d655997c116626b20ef3bb953992b7f447485d",
+ "sha256:34ef116022119d3053ecce8ba62109c8b54a650a537b183e79de1db4078894a8",
+ "sha256:38e60e400e07ba678a755cc89b09955a49acb0188d47aee2829ed4efc1ccb053",
+ "sha256:3ac6ea86ac161198653dba8f0af047d7b718318ba6d6bf4c719bc288ac21017b",
+ "sha256:3ad737617d38c55c0b72cd3ea01bf5cbab0d75123de5e52c39643ddc6465cb5f",
+ "sha256:3ae6ea8e610143348199c1db9fea5392f6b11a0e37ac8e8e7cae9c408625c5d0",
+ "sha256:3bcb8cdfeb08cef0138d696e52ec08fffaf009ef4b1c7c5a40340af672bd9b60",
+ "sha256:3d783be54d076843f58bf061fdaf1071789fb924fb35a0eb84dbc2c8b68499a2",
+ "sha256:3da30fb560b37cede31306b890c982213a23fa7335d368cdc16cf7034170860b",
+ "sha256:430cb9ee0cf2d5a2fa91e78f306544352009dbda9ebb4bd04c5785e67fc04953",
+ "sha256:43ad68da1385738a1e96a188029f092e88381352e697a0687784191c1d45845b",
+ "sha256:478d29a270785fc58632c9d1e0f59f595d3fb06c5e63ac117136dba2d0e5f4f9",
+ "sha256:4c3da8aa4b28f83382f0dee288d030eb0fa212df9632dc2fe4e4fc2a8e2577a4",
+ "sha256:560af476d03256a65bd4ff05c83e7869ec3c2b40579d4ed1ac74075ef19e02bf",
+ "sha256:56225de73d69f5ee6b783648eb1936e1bbe874a529cb1e15d64038904c54efb2",
+ "sha256:5737f40ffb1926b8bf38d32fdac48a27c441b757f1bf44cbaa100dafef049a01",
+ "sha256:5a1996d963016e6486b6a672f64f868e6b4e7e9e2caf710994df11b04790903e",
+ "sha256:5a922601cb94f427dd96eec3a7c776537ce7490f2beb69e88a81859e537343e4",
+ "sha256:5bd70d75b4bbfc0a26fdbfc72e260b70090637020b0d7d27eb26c7ae5edf16da",
+ "sha256:5e0db6ea39ac96ae2ccc582291633ee55ae93c9cc6dca1f7ab4dd9f0b7e08f8a",
+ "sha256:5eb2a8415a891564054447a327a7ef7ec90e69e0e63d85d1ffb03f82e102c740",
+ "sha256:6193564d51d02a4aa6fc60bb40f04b65840e70e124282a7337cd94a9ed1b0b81",
+ "sha256:65e763769cec2fa9c79b70555e3f2a7903747d127ab14ec9eaa5f00763bb4152",
+ "sha256:66c108d8e02da2febb0aa7d7002e14c4a0571460993c9edf8249393cdae7eeef",
+ "sha256:67caf9435b1f0115042cbc37e9d60475891b2d9b2a711ade0876580da2a5e0df",
+ "sha256:6989ea9326f9a9171edc920a459003b1deffc6cb48605b8f2610846c7fbd201a",
+ "sha256:69983ee3e649e41dc41ddea68f6c2ab64d7f19fcab4e21a72a1accf6482c5605",
+ "sha256:6b25953a1d6a97746becbd663b49e3b436a5001c995a62662d65835a2ba996a7",
+ "sha256:6b7e30e4246c2cd5e6c2c907486d235e4f3e8a39979744e9e0b8090629c62da4",
+ "sha256:6be1182387fb9a95fd8a8cbfab2f2b61d61ea0ab4699780d4300ea207c8f8d71",
+ "sha256:6befa3c621caf26f5affc6121bf863b8f049f862a7a10aabb0d19cce34c6e363",
+ "sha256:6fbe184451480c17f1f8dac160c9f3f6d243010fdb8416de4d3d7ee69ea65aa4",
+ "sha256:7074f10bf324e4d38208e1bcc2b6abd87901675b1a567507035ff1e7403378c0",
+ "sha256:71a8ce430f6c341725aefc0031626c484e0858fd95d1bf3625e0d27919d8edca",
+ "sha256:71d92e846541669ae1d11e00906c408c1dc9890196e13f11d4d62bd235ac9ddb",
+ "sha256:72574693fad34d075a54fa771984a1a8809a9eff880b731152078e81c7d8c80f",
+ "sha256:72a1159144e4ad7d152b2c3f75bed8c9b60ad06e9da322e1965981f8371c0dee",
+ "sha256:72d15a61b76fa4b814390c21defd772893d1152157e3297e6afb328ef32d1711",
+ "sha256:74e45386528db2d9346618276e8746507ec6ccb91512c5f02d2049ccafb0576e",
+ "sha256:7594756e9607e5ffbc16c7f3b1715c0f7224a82ac389f0087fa5faaa65fb96e2",
+ "sha256:767ed12b1662fac32d84eb43707e4f778b12554fc12ce3c8f7793a02111a9e32",
+ "sha256:77e0406dc247851a1e250fdcfd46d13cdecc2ac5b8d1d038dea216da2e7fa9b7",
+ "sha256:79658460926a1c48b2ac10196a673cc7d81857dc268ca593a66fd661ed1b77ea",
+ "sha256:7bf0d43f2c56b600c6895c25a420e4b46c6a5acf91d6c9a6300713496f70e63b",
+ "sha256:80935f26af0eec490c4e52381a28f39b08c2bc4ef4562448890027e4a4cfa3a4",
+ "sha256:82bd76a12588bd2bb27965aaf9fcff2258ffb03c9a290368288ed4571ed539ab",
+ "sha256:864684b36e0488715aac531785abe7514e872bfe83ecc768828e9ddaadeed320",
+ "sha256:883c07b404229e98d30b1ef64f07d4e3422d431ecd727c2ebba8549f70b2ba16",
+ "sha256:88548dec2f6f69e547252fe3113bf1b3b08c0879f5b44633927be07ed18c5cc0",
+ "sha256:8895999e0de5ddddddd146bb1bfc0316af808580637fea158873b84785f7a9e9",
+ "sha256:8ab4ea5f49166b990411c522c1f5f901014932ead15a463616ec93e10fff2c05",
+ "sha256:904895323a59dd50387cddd1337c3eac48ec0fe6cb4d2e4106dc1a3b80091727",
+ "sha256:90c3c60985e7da13e44aeaaf2e1c10fe1b7825788a18c82b0f9eaeb6c4e9d9c6",
+ "sha256:9584441b7a603a12aa382adf8c093ddc5a22328a108dabc6a4a112fa5b4facae",
+ "sha256:9ca652d9c6f68535537d75502b549ed0ca07fa6d3908f84f29f92148ec7310f2",
+ "sha256:9efe23b3d0671eeb8b1e70791612538dcdc1b39275563ca78f338aa0ce9619d2",
+ "sha256:a466c14574152b7caed5d76f1e46ae2963d33f5b0eb2dd915fa33870183b0f26",
+ "sha256:a48fd39ba69f399f39b911885469bdfedd1de4a095146211e39e8b5db0e3a937",
+ "sha256:a716344c6e92551bdf842b999c7d7b34a13e32facf3e6c5942690c9883d45e3a",
+ "sha256:b6df1b707adab4858dfaa74877f60f07d9d6be01a57015cf4e62aa8f00f3576b",
+ "sha256:b7d3053742a9a559dda8598a52e0c1bcbd18258cc199cba52137ce8c8e92c537",
+ "sha256:b95d96a0640decaa24cd9cf386fd4d8a96c973aafa42dd9c65609f9f0d66cc34",
+ "sha256:bc15e81587b652bbeba9d4d7d3707bcaaa1d7b4aab2024c14be477662003a211",
+ "sha256:be034aa2841c57ea43a55bc123d8f3f41cc2d3d102a22390c863791ba6bf23f1",
+ "sha256:c5223ab45639ce93c7233d518f6c3199422b49dbd0ebfb1d7917b5da2636712e",
+ "sha256:d9c2b1ca98e5454b78cd434f29fc33eb8f8a2f343efc5f975225d92070b9f7f6",
+ "sha256:db61c6ae9ee7268dc69a078ea22deaaf861350ad2e4c194c70798b8ec9789131",
+ "sha256:dc4e82c5db4e65703c842b0947699dd958a7262a8b854d1c19bbfe2d27be9333",
+ "sha256:e18db3b38ac30d8cc1ddbdfc7f6d3d814c1abb8936c57bd1c09c5da02873c8c4",
+ "sha256:e31697466812ff1cdabd7a9db7fb7f37d319e58e4c8a2ce1065012c1a5cfebde",
+ "sha256:eeca775446b00826ec8ca34eed8c902c6a1ae7dc42d78094b1f73ecd38d1dcf8",
+ "sha256:fa476e5f763f59270bda050695e22078b2461dc14bc62a0d27756c5906bca86c",
+ "sha256:fb3269a3641b868ab7856c3cd939782ee1c802fa120b470b597286d2f9e93aee",
+ "sha256:fd289ef26241bc2c0b7b047417a33786790bf2a01f6c92d9688eb2693c9116df"
],
"markers": "python_version >= '3.9'",
- "version": "==6.2.0"
+ "version": "==6.3.0"
},
"mutagen": {
"hashes": [
@@ -732,107 +732,107 @@
},
"propcache": {
"hashes": [
- "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e",
- "sha256:03acd9ff19021bd0567582ac88f821b66883e158274183b9e5586f678984f8fe",
- "sha256:03c091bb752349402f23ee43bb2bff6bd80ccab7c9df6b88ad4322258d6960fc",
- "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829",
- "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863",
- "sha256:119e244ab40f70a98c91906d4c1f4c5f2e68bd0b14e7ab0a06922038fae8a20f",
- "sha256:11ae6a8a01b8a4dc79093b5d3ca2c8a4436f5ee251a9840d7790dccbd96cb649",
- "sha256:15010f29fbed80e711db272909a074dc79858c6d28e2915704cfc487a8ac89c6",
- "sha256:19d36bb351ad5554ff20f2ae75f88ce205b0748c38b146c75628577020351e3c",
- "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a",
- "sha256:2383a17385d9800b6eb5855c2f05ee550f803878f344f58b6e194de08b96352c",
- "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545",
- "sha256:2578541776769b500bada3f8a4eeaf944530516b6e90c089aa368266ed70c49e",
- "sha256:26a67e5c04e3119594d8cfae517f4b9330c395df07ea65eab16f3d559b7068fe",
- "sha256:2b975528998de037dfbc10144b8aed9b8dd5a99ec547f14d1cb7c5665a43f075",
- "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57",
- "sha256:2d913d36bdaf368637b4f88d554fb9cb9d53d6920b9c5563846555938d5450bf",
- "sha256:3302c5287e504d23bb0e64d2a921d1eb4a03fb93a0a0aa3b53de059f5a5d737d",
- "sha256:36ca5e9a21822cc1746023e88f5c0af6fce3af3b85d4520efb1ce4221bed75cc",
- "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0",
- "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1",
- "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64",
- "sha256:42924dc0c9d73e49908e35bbdec87adedd651ea24c53c29cac103ede0ea1d340",
- "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db",
- "sha256:46ed02532cb66612d42ae5c3929b5e98ae330ea0f3900bc66ec5f4862069519b",
- "sha256:49ea05212a529c2caffe411e25a59308b07d6e10bf2505d77da72891f9a05641",
- "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626",
- "sha256:507c5357a8d8b4593b97fb669c50598f4e6cccbbf77e22fa9598aba78292b4d7",
- "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92",
- "sha256:58e6d2a5a7cb3e5f166fd58e71e9a4ff504be9dc61b88167e75f835da5764d07",
- "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e",
- "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787",
- "sha256:5fa159dcee5dba00c1def3231c249cf261185189205073bde13797e57dd7540a",
- "sha256:6032231d4a5abd67c7f71168fd64a47b6b451fbcb91c8397c2f7610e67683810",
- "sha256:63f26258a163c34542c24808f03d734b338da66ba91f410a703e505c8485791d",
- "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0",
- "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b",
- "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043",
- "sha256:6985a593417cdbc94c7f9c3403747335e450c1599da1647a5af76539672464d3",
- "sha256:6a1948df1bb1d56b5e7b0553c0fa04fd0e320997ae99689488201f19fa90d2e7",
- "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d",
- "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf",
- "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138",
- "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c",
- "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d",
- "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46",
- "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6",
- "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa",
- "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e",
- "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05",
- "sha256:7cedd25e5f678f7738da38037435b340694ab34d424938041aa630d8bac42663",
- "sha256:7e2e068a83552ddf7a39a99488bcba05ac13454fb205c847674da0352602082f",
- "sha256:8319293e85feadbbfe2150a5659dbc2ebc4afdeaf7d98936fb9a2f2ba0d4c35c",
- "sha256:8526b0941ec5a40220fc4dfde76aed58808e2b309c03e9fa8e2260083ef7157f",
- "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7",
- "sha256:8cb625bcb5add899cb8ba7bf716ec1d3e8f7cdea9b0713fa99eadf73b6d4986f",
- "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7",
- "sha256:8ee1983728964d6070ab443399c476de93d5d741f71e8f6e7880a065f878e0b9",
- "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667",
- "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86",
- "sha256:9ddd49258610499aab83b4f5b61b32e11fce873586282a0e972e5ab3bcadee51",
- "sha256:9ecde3671e62eeb99e977f5221abcf40c208f69b5eb986b061ccec317c82ebd0",
- "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a",
- "sha256:a254537b9b696ede293bfdbc0a65200e8e4507bc9f37831e2a0318a9b333c85c",
- "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568",
- "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af",
- "sha256:a7080b0159ce05f179cfac592cda1a82898ca9cd097dacf8ea20ae33474fbb25",
- "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5",
- "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe",
- "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf",
- "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9",
- "sha256:b58229a844931bca61b3a20efd2be2a2acb4ad1622fc026504309a6883686fbf",
- "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767",
- "sha256:be90c94570840939fecedf99fa72839aed70b0ced449b415c85e01ae67422c90",
- "sha256:bf0d9a171908f32d54f651648c7290397b8792f4303821c42a74e7805bfb813c",
- "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d",
- "sha256:bf4298f366ca7e1ad1d21bbb58300a6985015909964077afd37559084590c929",
- "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e",
- "sha256:cacea77ef7a2195f04f9279297684955e3d1ae4241092ff0cfcef532bb7a1c32",
- "sha256:cd54895e4ae7d32f1e3dd91261df46ee7483a735017dc6f987904f194aa5fd14",
- "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8",
- "sha256:d383bf5e045d7f9d239b38e6acadd7b7fdf6c0087259a84ae3475d18e9a2ae8b",
- "sha256:d3e7420211f5a65a54675fd860ea04173cde60a7cc20ccfbafcccd155225f8bc",
- "sha256:d8074c5dd61c8a3e915fa8fc04754fa55cfa5978200d2daa1e2d4294c1f136aa",
- "sha256:df03cd88f95b1b99052b52b1bb92173229d7a674df0ab06d2b25765ee8404bce",
- "sha256:e45377d5d6fefe1677da2a2c07b024a6dac782088e37c0b1efea4cfe2b1be19b",
- "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e",
- "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf",
- "sha256:ec5060592d83454e8063e487696ac3783cc48c9a329498bafae0d972bc7816c9",
- "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac",
- "sha256:ed7161bccab7696a473fe7ddb619c1d75963732b37da4618ba12e60899fefe4f",
- "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374",
- "sha256:ee25f1ac091def37c4b59d192bbe3a206298feeb89132a470325bf76ad122a1e",
- "sha256:efa44f64c37cc30c9f05932c740a8b40ce359f51882c70883cc95feac842da4d",
- "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e",
- "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121",
- "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5",
- "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54"
+ "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e",
+ "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b",
+ "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf",
+ "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b",
+ "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5",
+ "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c",
+ "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c",
+ "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a",
+ "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf",
+ "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8",
+ "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5",
+ "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42",
+ "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035",
+ "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0",
+ "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e",
+ "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46",
+ "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d",
+ "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24",
+ "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d",
+ "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de",
+ "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf",
+ "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7",
+ "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371",
+ "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833",
+ "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259",
+ "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136",
+ "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25",
+ "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005",
+ "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef",
+ "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7",
+ "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f",
+ "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53",
+ "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0",
+ "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb",
+ "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566",
+ "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a",
+ "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908",
+ "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf",
+ "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458",
+ "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64",
+ "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9",
+ "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71",
+ "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b",
+ "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5",
+ "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037",
+ "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5",
+ "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894",
+ "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe",
+ "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757",
+ "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3",
+ "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976",
+ "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6",
+ "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641",
+ "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7",
+ "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649",
+ "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120",
+ "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd",
+ "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40",
+ "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e",
+ "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229",
+ "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c",
+ "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7",
+ "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111",
+ "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654",
+ "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f",
+ "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294",
+ "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da",
+ "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f",
+ "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7",
+ "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0",
+ "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073",
+ "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7",
+ "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11",
+ "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f",
+ "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27",
+ "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70",
+ "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7",
+ "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519",
+ "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5",
+ "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180",
+ "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f",
+ "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee",
+ "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18",
+ "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815",
+ "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e",
+ "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a",
+ "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7",
+ "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6",
+ "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c",
+ "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc",
+ "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8",
+ "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98",
+ "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256",
+ "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5",
+ "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744",
+ "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723",
+ "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277",
+ "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"
],
"markers": "python_version >= '3.9'",
- "version": "==0.3.0"
+ "version": "==0.3.1"
},
"pycparser": {
"hashes": [
@@ -1187,11 +1187,11 @@
},
"typing-extensions": {
"hashes": [
- "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
- "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
+ "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b",
+ "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"
],
"markers": "python_version < '3.13'",
- "version": "==4.12.2"
+ "version": "==4.13.0"
},
"tzlocal": {
"hashes": [
diff --git a/app/library/DataStore.py b/app/library/DataStore.py
index 372e6569..7356a67d 100644
--- a/app/library/DataStore.py
+++ b/app/library/DataStore.py
@@ -8,6 +8,7 @@ from sqlite3 import Connection
from .config import Config
from .Download import Download
from .ItemDTO import ItemDTO
+from .Utils import clean_item
class DataStore:
@@ -68,7 +69,7 @@ class DataStore:
for row in cursor:
rowDate = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
- data: dict = json.loads(row["data"])
+ data, _ = clean_item(json.loads(row["data"]), keys=ItemDTO.removed_fields())
key: str = data.pop("_id")
item: ItemDTO = ItemDTO(**data)
item._id = key
diff --git a/app/library/Download.py b/app/library/Download.py
index f05eda47..723e5d6a 100644
--- a/app/library/Download.py
+++ b/app/library/Download.py
@@ -48,7 +48,6 @@ class Download:
temp_dir: str = None
template: str = None
template_chapter: str = None
- ytdl_opts: dict = None
info: ItemDTO = None
default_ytdl_opts: dict = None
debug: bool = False
@@ -92,7 +91,6 @@ class Download:
self.template = info.template
self.template_chapter = info.template_chapter
self.preset = info.preset
- self.ytdl_opts = info.config if info.config else {}
self.info = info
self.id = info._id
self.default_ytdl_opts = config.ytdl_options
@@ -147,7 +145,7 @@ class Download:
YTDLPOpts.get_instance()
.preset(self.preset)
.add({"break_on_existing": True})
- .add(self.ytdl_opts, from_user=True)
+ .add_cli(self.info.cli, from_user=True)
.add(
{
"color": "no_color",
diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py
index 491910ff..76b04ecd 100644
--- a/app/library/DownloadQueue.py
+++ b/app/library/DownloadQueue.py
@@ -1,12 +1,13 @@
import asyncio
import functools
-import json
+import glob
import logging
import os
import time
import uuid
from datetime import UTC, datetime
from email.utils import formatdate
+from pathlib import Path
from sqlite3 import Connection
import anyio
@@ -18,7 +19,7 @@ from .config import Config
from .DataStore import DataStore
from .Download import Download
from .Events import EventBus, Events
-from .ItemDTO import ItemDTO
+from .ItemDTO import Item, ItemDTO
from .Presets import Presets
from .Singleton import Singleton
from .Utils import arg_converter, calc_download_path, extract_info, is_downloaded
@@ -168,45 +169,23 @@ class DownloadQueue(metaclass=Singleton):
except Exception as e:
LOG.error(f"Failed to cancel downloads. {e!s}")
- async def __add_entry(
- self,
- entry: dict,
- preset: str,
- folder: str,
- config: dict | None = None,
- cookies: str = "",
- template: str = "",
- extras: dict | None = None,
- cli: str = "",
- already=None,
- ):
+ async def __add_entry(self, entry: dict, item: Item, already=None):
"""
Add an entry to the download queue.
Args:
entry (dict): The entry to add to the download queue.
- preset (str): The preset to use for the download.
- folder (str): The folder to save the download to.
- config (dict): The yt-dlp configuration to use for the download.
- cookies (str): The cookies to use for the download.
- template (str): The output template to use for the download.
- extras (dict): The extra information to add to the download.
- cli (str): The yt-dlp command line options to use for the download.
+ item (Item): The item to add to the download queue.
already (set): The set of already downloaded items.
Returns:
dict: The status of the operation.
"""
- if not config:
- config = {}
-
- if not extras:
- extras = {}
- else:
- for key in extras.copy():
+ if item.has_extras():
+ for key in item.extras.copy():
if not str(key).startswith("playlist"):
- extras.pop(key, None)
+ item.extras.pop(key, None)
if not entry:
return {"status": "error", "msg": "Invalid/empty data was given."}
@@ -239,13 +218,7 @@ class DownloadQueue(metaclass=Singleton):
results.append(
await self.__add_entry(
entry=etr,
- preset=preset,
- folder=folder,
- config=config,
- cookies=cookies,
- template=template,
- extras=extras,
- cli=cli,
+ item=item.new_with(url=etr.get("url") or etr.get("webpage_url")),
already=already,
)
)
@@ -291,7 +264,7 @@ class DownloadQueue(metaclass=Singleton):
is_live = bool(entry.get("is_live") or live_in or entry.get("live_status") in live_status)
try:
- download_dir = calc_download_path(base_path=self.config.download_path, folder=folder)
+ download_dir = calc_download_path(base_path=self.config.download_path, folder=item.folder)
except Exception as e:
LOG.exception(e)
return {"status": "error", "msg": str(e)}
@@ -299,31 +272,30 @@ class DownloadQueue(metaclass=Singleton):
fields: tuple = ("uploader", "channel", "thumbnail")
for field in fields:
if entry.get(field):
- extras[field] = entry.get(field)
+ item.extras[field] = entry.get(field)
for key in entry:
if isinstance(key, str) and key.startswith("playlist") and entry.get(key):
- extras[key] = entry.get(key)
+ item.extras[key] = entry.get(key)
dl = ItemDTO(
id=str(entry.get("id")),
title=str(entry.get("title")),
url=str(entry.get("webpage_url") or entry.get("url")),
- preset=preset,
- folder=folder,
+ preset=item.preset,
+ folder=item.folder,
download_dir=download_dir,
temp_dir=self.config.temp_path,
- cookies=cookies,
- config=config,
- template=template if template else self.config.output_template,
+ cookies=item.cookies,
+ template=item.template if item.template else self.config.output_template,
template_chapter=self.config.output_template_chapter,
datetime=formatdate(time.time()),
error=error,
is_live=is_live,
live_in=live_in,
options=options,
- cli=cli,
- extras=extras,
+ cli=item.cli,
+ extras=item.extras,
)
dlInfo: Download = Download(info=dl, info_dict=entry)
@@ -347,44 +319,16 @@ class DownloadQueue(metaclass=Singleton):
return {"status": "ok"}
if eventType.startswith("url"):
- return await self.add(
- url=str(entry.get("url")),
- preset=preset,
- folder=folder,
- config=config,
- cookies=cookies,
- template=template,
- extras=extras,
- cli=cli,
- already=already,
- )
+ return await self.add(item=item.new_with(url=entry.get("url")), already=already)
return {"status": "error", "msg": f'Unsupported resource "{eventType}"'}
- async def add(
- self,
- url: str,
- preset: str,
- folder: str,
- config: dict | None = None,
- cookies: str = "",
- template: str = "",
- cli: str = "",
- extras: dict | None = None,
- already=None,
- ):
+ async def add(self, item: Item, already: set | None = None):
"""
Add an item to the download queue.
Args:
- url (str): The url to be added to the queue.
- preset (str): The preset to be used for the download.
- folder (str): The folder to save the download to.
- config (dict): The yt-dlp config to be used for the download.
- cookies (str): The cookies to be used for the download.
- template (str): The template to be used for the download.
- cli (str): The yt-dlp cli options to be used for the download.
- extras (dict): Extra data to be added to the download
+ item (Item): The item to be added to the queue.
already (set): Set of already downloaded items.
Returns:
@@ -392,62 +336,47 @@ class DownloadQueue(metaclass=Singleton):
{ "status": "text" }
"""
- _preset = Presets.get_instance().get(name=preset)
+ _preset = Presets.get_instance().get(item.preset)
- config = config if config else {}
- if cli:
+ if item.has_cli():
try:
- config = arg_converter(args=cli, level=True)
+ arg_converter(args=item.cli, level=True)
except Exception as e:
- LOG.error(f"Invalid cli options '{cli}'. {e!s}")
- return {"status": "error", "msg": f"Invalid cli options '{cli}'. {e!s}"}
-
- folder = str(folder) if folder else ""
- if not extras:
- extras = {}
+ LOG.error(f"Invalid cli options '{item.cli}'. {e!s}")
+ return {"status": "error", "msg": f"Invalid cli options '{item.cli}'. {e!s}"}
if _preset:
- if _preset.folder and not folder:
- folder = _preset.folder
+ if _preset.folder and not item.folder:
+ item.folder = _preset.folder
- if _preset.template and not template:
- template = _preset.template
+ if _preset.template and not item.template:
+ item.template = _preset.template
- if _preset.cookies and not cookies:
- cookies = _preset.cookies
+ if _preset.cookies and not item.cookies:
+ item.cookies = _preset.cookies
- filePath = calc_download_path(base_path=self.config.download_path, folder=folder)
yt_conf = {}
cookie_file = os.path.join(self.config.temp_path, f"c_{uuid.uuid4().hex}.txt")
- LOG.info(
- f"Adding 'URL: {url}' to 'Folder: {filePath}' with 'Preset: {preset}' 'Naming: {template}', 'Cookies: {len(cookies)}/chars' 'CLI: {cli}' 'Extras: {extras}'."
- )
-
- if isinstance(config, str):
- try:
- config = json.loads(config)
- except Exception as e:
- LOG.error(f"Unable to load '{config=}'. {e!s}")
- return {"status": "error", "msg": f"Failed to parse json yt-dlp config. {e!s}"}
+ LOG.info(f"Adding '{item.__repr__()}'.")
already = set() if already is None else already
- if url in already:
- LOG.warning(f"Recursion detected with url '{url}' skipping.")
+ if item.url in already:
+ LOG.warning(f"Recursion detected with url '{item.url}' skipping.")
return {"status": "ok"}
- already.add(url)
+ already.add(item.url)
try:
- downloaded, id_dict = self._is_downloaded(url)
+ downloaded, id_dict = self._is_downloaded(item.url)
if downloaded is True and id_dict:
message = f"This url with ID '{id_dict.get('id')}' has been downloaded already and recorded in archive."
LOG.info(message)
return {"status": "error", "msg": message}
started = time.perf_counter()
- LOG.debug(f"extract_info: checking {url=}")
+ LOG.debug(f"extract_info: checking '{item.url}'.")
logs = []
@@ -456,16 +385,18 @@ class DownloadQueue(metaclass=Singleton):
"func": lambda _, msg: logs.append(msg),
"level": logging.WARNING,
},
- **YTDLPOpts.get_instance().preset(name=preset).add(config=config, from_user=True).get_all(),
+ **YTDLPOpts.get_instance().preset(name=item.preset).add_cli(args=item.cli, from_user=True).get_all(),
}
- if cookies:
+ if item.cookies:
try:
async with await anyio.open_file(cookie_file, "w") as f:
- await f.write(cookies)
+ await f.write(item.cookies)
yt_conf["cookiefile"] = f.name
except ValueError as e:
- LOG.error(f"Failed to create cookie file for '{self.info.id}: {self.info.title}'. '{e!s}'.")
+ msg = f"Failed to create cookie file for '{self.info.id}: {self.info.title}'. '{e!s}'."
+ LOG.error(msg)
+ return {"status": "error", "msg": msg}
entry = await asyncio.wait_for(
fut=asyncio.get_running_loop().run_in_executor(
@@ -473,7 +404,7 @@ class DownloadQueue(metaclass=Singleton):
functools.partial(
extract_info,
config=yt_conf,
- url=url,
+ url=item.url,
debug=bool(self.config.ytdl_debug),
no_archive=False,
follow_redirect=True,
@@ -486,7 +417,7 @@ class DownloadQueue(metaclass=Singleton):
return {"status": "error", "msg": "Unable to extract info." + "\n".join(logs)}
LOG.debug(
- f"extract_info: for 'URL: {url}' is done in '{time.perf_counter() - started}'. Length: '{len(entry)}'."
+ f"extract_info: for 'URL: {item.url}' is done in '{time.perf_counter() - started}'. Length: '{len(entry)}/keys'."
)
except yt_dlp.utils.ExistingVideoReached as exc:
LOG.error(f"Video has been downloaded already and recorded in archive.log file. '{exc!s}'.")
@@ -508,17 +439,7 @@ class DownloadQueue(metaclass=Singleton):
except Exception as e:
LOG.error(f"Failed to remove cookie file '{yt_conf['cookiefile']}'. {e!s}")
- return await self.__add_entry(
- entry=entry,
- preset=preset,
- folder=folder,
- config=config,
- cookies=cookies,
- template=template,
- already=already,
- extras=extras,
- cli=cli,
- )
+ return await self.__add_entry(entry=entry, item=item, already=already)
async def cancel(self, ids: list[str]) -> dict[str, str]:
"""
@@ -588,7 +509,7 @@ class DownloadQueue(metaclass=Singleton):
continue
itemRef: str = f"{id=} {item.info.id=} {item.info.title=}"
- fileDeleted: bool = False
+ removed_files = 0
filename: str = ""
if remove_file and self.config.remove_files and "finished" == item.info.status:
@@ -602,9 +523,13 @@ class DownloadQueue(metaclass=Singleton):
folder=filename,
create_path=False,
)
- if realFile and os.path.exists(realFile):
- os.remove(realFile)
- fileDeleted = True
+ rf = Path(realFile)
+ if rf.is_file() and rf.exists():
+ for f in rf.parent.glob(f"{glob.escape(rf.stem)}.*"):
+ if f.is_file() and f.exists() and not f.name.startswith("."):
+ removed_files += 1
+ LOG.debug(f"Removing '{itemRef}' local file '{f.name}'.")
+ os.remove(f)
else:
LOG.warning(f"Failed to remove '{itemRef}' local file '{filename}'. File not found.")
except Exception as e:
@@ -614,8 +539,8 @@ class DownloadQueue(metaclass=Singleton):
await self._notify.emit(Events.CLEARED, data=item.info.serialize())
msg = f"Deleted completed download '{itemRef}'."
- if fileDeleted and filename:
- msg += f" and removed local file '{filename}'."
+ if removed_files > 0:
+ msg += f" and removed '{removed_files}' local files."
LOG.info(msg=msg)
status[id] = "ok"
diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py
index 74387c55..51a02169 100644
--- a/app/library/HttpAPI.py
+++ b/app/library/HttpAPI.py
@@ -27,6 +27,7 @@ from .DownloadQueue import DownloadQueue
from .encoder import Encoder
from .Events import EventBus, Events, message
from .ffprobe import ffprobe
+from .ItemDTO import Item
from .M3u8 import M3u8
from .Notifications import Notification, NotificationEvents
from .Playlist import Playlist
@@ -475,7 +476,8 @@ class HttpAPI(Common):
err = err.split("\n")[-1] if "\n" in err else err
err = err.replace("main.py: error: ", "").strip().capitalize()
return web.json_response(
- data={"error": f"Failed to command line arguments for yt-dlp. '{err}'."}, status=web.HTTPBadRequest.status_code
+ data={"error": f"Failed to parse command arguments for yt-dlp. '{err}'."},
+ status=web.HTTPBadRequest.status_code,
)
@route("GET", "api/yt-dlp/url/info")
@@ -501,7 +503,7 @@ class HttpAPI(Common):
preset = request.query.get("preset")
if preset:
- exists = Presets.get_instance().get(name=preset)
+ exists = Presets.get_instance().get(preset)
if not exists:
return web.json_response(
data={"status": False, "message": f"Preset '{preset}' does not exist."},
@@ -674,7 +676,7 @@ class HttpAPI(Common):
preset = request.query.get("preset")
if preset:
- exists = Presets.get_instance().get(name=preset)
+ exists = Presets.get_instance().get(preset)
if not exists:
return web.json_response(
data={"status": False, "message": f"Preset '{preset}' does not exist."},
@@ -683,7 +685,7 @@ class HttpAPI(Common):
data["preset"] = preset
try:
- status = await self.add(**self.format_item(data))
+ status = await self.add(item=Item.format(data))
except ValueError as e:
return web.json_response(data={"status": False, "message": str(e)}, status=web.HTTPBadRequest.status_code)
@@ -710,15 +712,16 @@ class HttpAPI(Common):
if isinstance(data, dict):
data = [data]
+ items = []
for item in data:
try:
- self.format_item(item)
+ items.append(Item.format(item))
except ValueError as e:
return web.json_response(data={"error": str(e), "data": item}, status=web.HTTPBadRequest.status_code)
return web.json_response(
data=await asyncio.wait_for(
- fut=asyncio.gather(*[self.add(**self.format_item(item)) for item in data]),
+ fut=asyncio.gather(*[self.add(item=item) for item in items]),
timeout=None,
),
status=web.HTTPOk.status_code,
@@ -790,18 +793,6 @@ class HttpAPI(Common):
if not item.get("id", None) or not validate_uuid(item.get("id"), version=4):
item["id"] = str(uuid.uuid4())
- if not item.get("args", None) or str(item.get("args")).strip() == "":
- item["args"] = {}
-
- if item.get("args", None) and isinstance(item.get("args"), str):
- item["args"] = json.loads(item.get("args"))
-
- if not item.get("postprocessors", None) or str(item.get("postprocessors")).strip() == "":
- item["postprocessors"] = []
-
- if item.get("postprocessors", None) and isinstance(item.get("postprocessors"), str):
- item["postprocessors"] = json.loads(item.get("postprocessors"))
-
try:
cls.validate(item)
except ValueError as e:
diff --git a/app/library/HttpSocket.py b/app/library/HttpSocket.py
index 5d3d22b4..fae33413 100644
--- a/app/library/HttpSocket.py
+++ b/app/library/HttpSocket.py
@@ -17,6 +17,7 @@ from .config import Config
from .DownloadQueue import DownloadQueue
from .encoder import Encoder
from .Events import Event, EventBus, Events, error
+from .ItemDTO import Item
from .Presets import Presets
from .Utils import is_downloaded
@@ -79,7 +80,7 @@ class HttpSocket(Common):
self._notify.subscribe(
Events.ADD_URL,
- lambda data, _, **kwargs: self.add(**self.format_item(data.data)), # noqa: ARG005
+ lambda data, _, **kwargs: self.add(item=Item.format(data.data)), # noqa: ARG005
f"{__class__.__name__}.socket_add_url",
)
@@ -188,14 +189,16 @@ class HttpSocket(Common):
return
try:
- item = self.format_item(data)
+ await self._notify.emit(
+ event=Events.STATUS,
+ data=await self.add(item=Item.format(data)),
+ to=sid,
+ )
except ValueError as e:
+ LOG.exception(e)
await self._notify.emit(Events.ERROR, data=error(str(e)), to=sid)
return
- status = await self.add(**item)
- await self._notify.emit(Events.STATUS, data=status, to=sid)
-
@ws_event
async def item_cancel(self, sid: str, id: str):
if not id:
diff --git a/app/library/ItemDTO.py b/app/library/ItemDTO.py
index 36bcd3c0..e9fc570e 100644
--- a/app/library/ItemDTO.py
+++ b/app/library/ItemDTO.py
@@ -1,10 +1,171 @@
import json
+import logging
import time
import uuid
from dataclasses import dataclass, field
from email.utils import formatdate
from typing import Any
+LOG = logging.getLogger("ItemDTO")
+
+
+@dataclass(kw_only=True)
+class Item:
+ """
+ This class represents a single download request for data transfer purposes.
+ """
+
+ url: str
+ """The URL of the item to be downloaded."""
+
+ preset: str = field(default_factory=lambda: Item._default_preset())
+ """The preset to be used for the download."""
+
+ folder: str = ""
+ """The folder to save the download to."""
+
+ cookies: str = ""
+ """The cookies to be used for the download."""
+
+ template: str = ""
+ """The template to be used for the download."""
+
+ cli: str = ""
+ """The yt-dlp cli options to be used for the download."""
+
+ extras: dict = field(default_factory=dict)
+ """Extra data to be added to the download."""
+
+ def serialize(self) -> dict:
+ return self.__dict__.copy()
+
+ def json(self) -> str:
+ return json.dumps(self.serialize(), default=lambda o: o.__dict__, sort_keys=True, indent=4)
+
+ def get(self, key: str, default: Any = None) -> Any:
+ return self.__dict__.get(key, default)
+
+ def has_extras(self) -> bool:
+ """
+ Check if the item has any extras data associated with it.
+
+ Returns:
+ bool: True if the item has extras, False otherwise.
+
+ """
+ return self.extras and len(self.extras) > 0
+
+ def has_cli(self) -> bool:
+ """
+ Check if the item has any yt-dlp cli options associated with it.
+
+ Returns:
+ bool: True if the item has yt-dlp cli options, False otherwise.
+
+ """
+ return self.cli and len(self.cli) > 2
+
+ def _default_preset() -> str:
+ from .config import Config
+
+ return Config.get_instance().default_preset
+
+ def new_with(self, **kwargs) -> "Item":
+ """
+ Create a new instance of Item with the given parameters.
+
+ Args:
+ **kwargs: The parameters to be used for creating the new instance.
+
+ Returns:
+ Item: A new instance of Item with the given parameters.
+
+ """
+ return Item.format({**self.serialize(), **kwargs})
+
+ def __repr__(self):
+ from .config import Config
+ from .Utils import calc_download_path, strip_newline
+
+ data = {}
+ for k, v in self.serialize().items():
+ if not v:
+ continue
+
+ if k == "cli":
+ data[k] = strip_newline(v)
+ elif k == "extras":
+ data[k] = f"{len(v)} items"
+ elif k == "cookies":
+ data[k] = f"{len(v)}/chars"
+ elif k == "folder":
+ data[k] = calc_download_path(base_path=Config.get_instance().download_path, folder=v)
+ else:
+ data[k] = v
+
+ items = "".join(f'{k}="{v}", ' for k, v in data.items() if v)
+ return f"Item({items.strip(', ')})"
+
+ @staticmethod
+ def format(item: dict) -> "Item":
+ """
+ Format the item to be added to the download queue.
+
+ Args:
+ item (dict): The item to be formatted.
+
+ Raises:
+ ValueError: If the url is not provided.
+ ValueError: If the yt-cli command line arguments are not valid.
+
+ Returns:
+ Item: The formatted item.
+
+ """
+ url: str = item.get("url")
+
+ if not url or not isinstance(url, str):
+ msg = "url param is required."
+ raise ValueError(msg)
+
+ data = {
+ "url": url,
+ }
+
+ preset = item.get("preset")
+ if preset and isinstance(preset, str) and preset != Item._default_preset():
+ data["preset"] = preset
+
+ if item.get("folder") and isinstance(item.get("folder"), str):
+ data["folder"] = item.get("folder")
+
+ if item.get("cookies") and isinstance(item.get("cookies"), str):
+ data["cookies"] = item.get("cookies")
+
+ if item.get("template") and isinstance(item.get("template"), str):
+ data["template"] = item.get("template")
+
+ extras = item.get("extras")
+ if extras and isinstance(extras, dict) and len(extras) > 0:
+ data["extras"] = extras
+
+ cli = item.get("cli")
+ if cli and len(cli) > 2:
+ from .Utils import arg_converter
+
+ try:
+ removed_options = []
+ arg_converter(args=cli, level=True, removed_options=removed_options)
+ if len(removed_options) > 0:
+ LOG.warning("Removed the following options '%s' for '%s'.", ", ".join(removed_options), url)
+
+ data["cli"] = cli
+ except Exception as e:
+ msg = f"Failed to parse yt-dlp cli options. {e!s}"
+ raise ValueError(msg) from e
+
+ return Item(**data)
+
@dataclass(kw_only=True)
class ItemDTO:
@@ -49,45 +210,8 @@ class ItemDTO:
speed: str | None = None
eta: str | None = None
- # @Deprecated: These fields are deprecated and will be removed in the future.
- thumbnail: str | None = None
- ytdlp_cookies: str | None = None
- ytdlp_config: dict = field(default_factory=dict)
- output_template: str | None = None
- output_template_chapter: str | None = None
- config: dict = field(default_factory=dict)
-
def serialize(self) -> dict:
- deprecated: tuple = (
- "thumbnail",
- "quality",
- "format",
- "ytdlp_cookies",
- "ytdlp_config",
- "output_template",
- "output_template_chapter",
- "config",
- )
-
- if self.thumbnail and "thumbnail" not in self.extras:
- self.extras["thumbnail"] = self.thumbnail
-
- mapper: dict = {
- "cookies": "ytdlp_cookies",
- "config": "ytdlp_config",
- "template": "output_template",
- "template_chapter": "output_template_chapter",
- }
-
- for k, v in mapper.items():
- if not self.get(k) and self.get(v):
- self.__dict__[k] = self.get(v)
-
- dump = self.__dict__.copy()
- for f in deprecated:
- dump.pop(f, None)
-
- return dump
+ return self.__dict__.copy()
def json(self) -> str:
return json.dumps(self.serialize(), default=lambda o: o.__dict__, sort_keys=True, indent=4)
@@ -97,3 +221,17 @@ class ItemDTO:
def get_id(self) -> str:
return self._id
+
+ @staticmethod
+ def removed_fields() -> tuple:
+ """Fields that once existed but are no longer used."""
+ return (
+ "thumbnail",
+ "quality",
+ "format",
+ "ytdlp_cookies",
+ "ytdlp_config",
+ "output_template",
+ "output_template_chapter",
+ "config",
+ )
diff --git a/app/library/Notifications.py b/app/library/Notifications.py
index d3486405..20deb566 100644
--- a/app/library/Notifications.py
+++ b/app/library/Notifications.py
@@ -141,9 +141,6 @@ class Notification(metaclass=Singleton):
except Exception:
pass
- if os.path.getsize(self._file) > 10:
- self.load()
-
@staticmethod
def get_instance() -> "Notification":
if Notification._instance is None:
@@ -371,7 +368,9 @@ class Notification(metaclass=Singleton):
if "form" == target.request.type.lower():
reqBody["data"]["data"] = self._encoder.encode(reqBody["data"]["data"])
- logging.getLogger("httpx").setLevel(logging.WARNING)
+ if not self._debug:
+ logging.getLogger("httpx").setLevel(logging.WARNING)
+
response = await self._client.request(**reqBody)
respData = {"url": target.request.url, "status": response.status_code, "text": response.text}
@@ -404,7 +403,7 @@ class Notification(metaclass=Singleton):
data[k] = [self._deep_unpack(i) for i in v]
if isinstance(v, datetime):
data[k] = v.isoformat()
- if isinstance(v, ItemDTO):
+ if isinstance(v, object) and hasattr(v, "serialize"):
data[k] = v.serialize()
return data
diff --git a/app/library/Presets.py b/app/library/Presets.py
index 83f65e16..cfec885b 100644
--- a/app/library/Presets.py
+++ b/app/library/Presets.py
@@ -11,6 +11,7 @@ from .config import Config
from .encoder import Encoder
from .Events import EventBus, Events
from .Singleton import Singleton
+from .Utils import arg_converter, clean_item
LOG = logging.getLogger("presets")
@@ -26,12 +27,6 @@ class Preset:
format: str
"""The format of the preset."""
- args: dict[str, list[str] | bool] = field(default_factory=dict)
- """The arguments of the preset."""
-
- postprocessors: list = field(default_factory=list)
- """The postprocessors of the preset."""
-
folder: str = ""
"""The default download folder to use if non is given."""
@@ -83,13 +78,19 @@ class Presets(metaclass=Singleton):
pass
with open(os.path.join(os.path.dirname(__file__), "presets.json")) as f:
- self._default_presets = [Preset(**preset) for preset in json.load(f)]
+ for i, preset in enumerate(json.load(f)):
+ try:
+ self.validate(preset)
+ self._default_presets.append(Preset(**preset))
+ except Exception as e:
+ LOG.error(f"Failed to parse default preset '{i}'. '{e!s}'.")
+ continue
- EventBus.get_instance().subscribe(
- Events.PRESETS_ADD,
- lambda data, _, **kwargs: self.add(**data.data), # noqa: ARG005
- f"{__class__.__name__}.save",
- )
+ def event_handler(_, __):
+ msg = "Not implemented"
+ raise Exception(msg)
+
+ EventBus.get_instance().subscribe(Events.PRESETS_ADD, event_handler, f"{__class__.__name__}.save")
@staticmethod
def get_instance() -> "Presets":
@@ -150,22 +151,27 @@ class Presets(metaclass=Singleton):
LOG.info(f"No presets were defined in '{self._file}'.")
return self
- needSaving = False
+ need_save = False
for i, preset in enumerate(presets):
try:
if "id" not in preset:
preset["id"] = str(uuid.uuid4())
- needSaving = True
+ need_save = True
+ preset, preset_status = clean_item(preset, keys=("args", "postprocessors"))
preset = Preset(**preset)
+
+ if preset_status:
+ need_save = True
+
self._presets.append(preset)
except Exception as e:
LOG.error(f"Failed to parse preset at list position '{i}'. '{e!s}'.")
continue
- if needSaving:
- LOG.info("Saving presets due to missing ids.")
+ if need_save:
+ LOG.info("Saving presets due to format, or id change.")
self.save(self._presets)
return self
@@ -198,7 +204,7 @@ class Presets(metaclass=Singleton):
"""
if not isinstance(preset, dict):
if not isinstance(preset, Preset):
- msg = "Invalid preset type."
+ msg = f"Invalid preset type. Was expecting a (Preset|dict), but got '{type(preset).__name__}'."
raise ValueError(msg) # noqa: TRY004
preset = preset.serialize()
@@ -215,18 +221,8 @@ class Presets(metaclass=Singleton):
msg = "No format found."
raise ValueError(msg)
- if preset.get("args") and not isinstance(preset.get("args"), dict):
- msg = "Invalid args type. expected dict."
- raise ValueError(msg)
-
- if preset.get("postprocessors") and not isinstance(preset.get("postprocessors"), list):
- msg = "Invalid postprocessors type. expected list."
- raise ValueError(msg)
-
if preset.get("cli"):
try:
- from .Utils import arg_converter
-
arg_converter(args=preset.get("cli"))
except Exception as e:
msg = f"Invalid cli options. '{e!s}'."
@@ -270,26 +266,24 @@ class Presets(metaclass=Singleton):
return self
- def get(self, id: str | None = None, name: str | None = None) -> Preset | None:
+ def get(self, id_or_name: str) -> Preset | None:
"""
Get the preset by id or name.
Args:
- id (str|None): The id of the preset.
- name (str|None): The name of the preset.
+ id_or_name (str): The id or name of the preset.
Returns:
Preset|None: The preset if found, None otherwise.
"""
- if not id and not name:
+ if not id_or_name:
return None
for preset in self.get_all():
- if preset.id == id:
- return preset
+ if id_or_name not in (preset.id, preset.name):
+ continue
- if preset.name == name:
- return preset
+ return preset
return None
diff --git a/app/library/Tasks.py b/app/library/Tasks.py
index dc695114..b768190d 100644
--- a/app/library/Tasks.py
+++ b/app/library/Tasks.py
@@ -15,6 +15,7 @@ from .encoder import Encoder
from .Events import EventBus, Events, error, info, success
from .Scheduler import Scheduler
from .Singleton import Singleton
+from .Utils import clean_item
LOG = logging.getLogger("tasks")
@@ -140,13 +141,13 @@ class Tasks(metaclass=Singleton):
LOG.info(f"No tasks were defined in '{self._file}'.")
return self
- need_update = False
+ need_save = False
for i, task in enumerate(tasks):
try:
- task, task_status = self.clean_task(task)
+ task, task_status = clean_item(task, keys=("cookies", "config"))
task = Task(**task)
if task_status:
- need_update = True
+ need_save = True
except Exception as e:
LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.")
continue
@@ -155,12 +156,20 @@ class Tasks(metaclass=Singleton):
self._scheduler.add(timer=task.timer, func=self._runner, args=(task,), id=task.id)
self._tasks.append(task)
- LOG.info(f"Task '{i}: {task.name}' queued to be executed every '{task.timer}'.")
+ try:
+ from cronsim import CronSim
+
+ cs = CronSim(task.timer, datetime.now(UTC))
+ schedule_time = cs.explain()
+ except Exception:
+ schedule_time = task.timer
+
+ LOG.info(f"Task '{i}: {task.name}' queued to be executed '{schedule_time}'.")
except Exception as e:
LOG.exception(e)
LOG.error(f"Failed to queue task '{i}: {task.name}'. '{e!s}'.")
- if need_update:
+ if need_save:
LOG.info("Updating tasks file to remove old keys.")
self.save(self.get_all())
@@ -223,6 +232,7 @@ class Tasks(metaclass=Singleton):
if task.get("cli"):
try:
from .Utils import arg_converter
+
arg_converter(args=task.get("cli"))
except Exception as e:
msg = f"Invalid cli options. '{e!s}'."
@@ -266,27 +276,6 @@ class Tasks(metaclass=Singleton):
return self
- def clean_task(self, task: dict) -> tuple[dict, bool]:
- """
- Clean the task from old keys.
-
- Args:
- task (dict): The task to clean.
-
- Returns:
- tuple[dict, bool]: The cleaned task and a status if the task was cleaned.
-
- """
- status = False
- removedKeys = ["cookies", "config"]
-
- for key in removedKeys:
- if key in task:
- status = True
- task.pop(key)
-
- return task, status
-
async def _runner(self, task: Task):
"""
Run the task.
diff --git a/app/library/Utils.py b/app/library/Utils.py
index 155f1ceb..0e5ae8b8 100644
--- a/app/library/Utils.py
+++ b/app/library/Utils.py
@@ -522,8 +522,6 @@ def arg_converter(
bad_options.update(item.items())
- LOG.debug("Removed %i the following options: '%s'.", level, ", ".join(bad_options.values()))
-
for key in diff.copy():
if key not in bad_options:
continue
@@ -889,3 +887,60 @@ def get_files(base_path: str, dir: str | None = None):
)
return contents
+
+
+def clean_item(item: dict, keys: list | tuple) -> tuple[dict, bool]:
+ """
+ Remove given keys from a dictionary.
+ This function modifies the dictionary in place and returns a tuple
+ containing the modified dictionary and a boolean indicating
+ whether any keys were removed.
+
+ Args:
+ item (dict): The item to clean.
+ keys (list|tuple): The keys to remove.
+
+ Returns:
+ tuple[dict, bool]: The cleaned item and a the status of cleaning operation.
+
+ Raises:
+ TypeError: If item is not a dictionary or keys is not a list or tuple.
+
+ """
+ status = False
+
+ if not isinstance(item, dict):
+ msg = "Item must be a dictionary."
+ raise TypeError(msg)
+
+ if not isinstance(keys, list | tuple):
+ msg = "Keys must be a list or tuple."
+ raise TypeError(msg)
+
+ for key in keys:
+ if key not in item:
+ continue
+
+ status = True
+ item.pop(key)
+
+ return item, status
+
+
+def strip_newline(string: str) -> str:
+ """
+ Remove newlines from a string.
+
+ Args:
+ string (str): The string to process.
+
+ Returns:
+ str: The string without newlines.
+
+ """
+ if not string:
+ return ""
+
+ res = re.sub(r"(\r\n|\r|\n)", " ", string)
+
+ return res.strip() if res else ""
diff --git a/app/library/YTDLPOpts.py b/app/library/YTDLPOpts.py
index 2a87ca91..266e9edd 100644
--- a/app/library/YTDLPOpts.py
+++ b/app/library/YTDLPOpts.py
@@ -36,6 +36,30 @@ class YTDLPOpts(metaclass=Singleton):
return YTDLPOpts._instance
+ def add_cli(self, args: str, from_user: int | bool = False) -> "YTDLPOpts":
+ """
+ Prase and add yt-dlp cli options to the item options.
+
+ Args:
+ args (str): The cli options to add
+ from_user (bool): If the options are from the user
+
+ Returns:
+ YTDLPOpts: The instance of the class
+
+ """
+ if not args or len(args) < 2 or not isinstance(args, str):
+ return self
+
+ removed_options = []
+
+ self._item_opts.update(arg_converter(args=args, level=from_user, removed_options=removed_options))
+
+ if len(removed_options) > 0:
+ LOG.warning("Removed the following options: '%s'.", ", ".join(removed_options))
+
+ return self
+
def add(self, config: dict, from_user: bool = False) -> "YTDLPOpts":
"""
Add the options to the item options.
@@ -78,7 +102,7 @@ class YTDLPOpts(metaclass=Singleton):
YTDLPOpts: The instance of the class
"""
- preset = Presets.get_instance().get(name=name)
+ preset = Presets.get_instance().get(name)
if not preset or "default" == name:
return self
@@ -118,25 +142,6 @@ class YTDLPOpts(metaclass=Singleton):
"temp": self._config.temp_path,
}
- # @Deprecated - To be removed in future versions.
- if not preset.cli:
- if preset.postprocessors and isinstance(preset.postprocessors, list) and len(preset.postprocessors) > 0:
- self._preset_opts["postprocessors"] = preset.postprocessors
-
- if preset.args and isinstance(preset.args, dict) and len(preset.args) > 0:
- bad_options = {k: v for d in REMOVE_KEYS for k, v in d.items()}
- removed_options = []
- for key, value in preset.args.items():
- if key in bad_options:
- removed_options.append(bad_options[key])
- continue
- self._preset_opts[key] = value
-
- if len(removed_options) > 0:
- LOG.warning(
- "Removed the following options '%s' from '%s' args.", ", ".join(removed_options), preset.name
- )
-
return self
def get_all(self, keep: bool = False) -> dict:
@@ -166,25 +171,4 @@ class YTDLPOpts(metaclass=Singleton):
if "format" in data and data["format"] in ["not_set", "default"]:
data["format"] = None
- # @Deprecated - To be removed in future versions, All the checks below this line are deprecated.
- if "daterange" in data and isinstance(data["daterange"], dict):
- from yt_dlp.utils import DateRange
-
- data["daterange"] = DateRange(data["daterange"]["start"], data["daterange"]["end"])
-
- if "impersonate" in data and isinstance(data["impersonate"], str):
- from yt_dlp.networking.impersonate import ImpersonateTarget
-
- data["impersonate"] = ImpersonateTarget.from_str(data["impersonate"])
-
- if (
- "match_filter" in data
- and isinstance(data["match_filter"], dict)
- and "filters" in data["match_filter"]
- and len(data["match_filter"]["filters"]) > 0
- ):
- from yt_dlp.utils import match_filter_func
-
- data["match_filter"] = match_filter_func(data["match_filter"]["filters"])
-
return data
diff --git a/app/library/common.py b/app/library/common.py
index 2f26b4a9..f8ff5fda 100644
--- a/app/library/common.py
+++ b/app/library/common.py
@@ -1,10 +1,9 @@
-import json
import logging
from .config import Config
from .DownloadQueue import DownloadQueue
from .encoder import Encoder
-from .Utils import arg_converter
+from .ItemDTO import Item
LOG = logging.getLogger("common")
@@ -27,105 +26,16 @@ class Common:
config = config or Config.get_instance()
self.default_preset = config.default_preset
- async def add(
- self,
- url: str,
- preset: str,
- folder: str,
- cookies: str,
- # @deprecated: config: dict,
- config: dict,
- template: str,
- extras: dict | None = None,
- cli: str = "",
- ) -> dict[str, str]:
+ async def add(self, item: Item) -> dict[str, str]:
"""
Add an item to the download queue.
Args:
- url (str): The url to be added to the queue.
- preset (str): The preset to be used for the download.
- folder (str): The folder to save the download to.
- cookies (str): The cookies to be used for the download.
- config (dict): The yt-dlp config to be used for the download.
- template (str): The template to be used for the download.
- extras (dict): Extra data to be added to the download
- cli (str): The yt-dlp cli options to be used for the download.
+ item (Item): The item to be added to the queue.
Returns:
dict[str, str]: The status of the download.
{ "status": "text" }
"""
- if cookies and isinstance(cookies, dict):
- cookies = self.encoder.encode(cookies)
-
- return await self.queue.add(
- url=url,
- preset=preset if preset else self.config.default_preset,
- folder=folder,
- cookies=cookies,
- config=config if isinstance(config, dict) else {},
- template=template,
- cli=cli,
- extras=extras,
- )
-
- def format_item(self, item: dict) -> dict:
- """
- Format the item to be added to the download queue.
-
- Args:
- item (dict): The item to be formatted.
-
- Raises:
- ValueError: If the url is not provided.
- ValueError: If the yt-dlp config is not a valid json.
-
- Returns:
- dict: The formatted item
-
- """
- url: str = item.get("url")
-
- if not url:
- msg = "url param is required."
- raise ValueError(msg)
-
- preset: str = str(item.get("preset", self.config.default_preset))
- folder: str = str(item.get("folder")) if item.get("folder") else ""
- cookies: str = str(item.get("cookies")) if item.get("cookies") else ""
- template: str = str(item.get("template")) if item.get("template") else ""
- extras = item.get("extras", {})
-
- config = item.get("config")
- if isinstance(config, str) and config:
- try:
- config = json.loads(config)
- except Exception as e:
- msg = f"Failed to parse json yt-dlp config for '{url}'. {e!s}"
- raise ValueError(msg) from e
-
- cli = item.get("cli")
- if cli and len(cli) > 2:
- try:
- removed_options = []
- arg_converter(args=cli, level=True, removed_options=removed_options)
- if len(removed_options) > 0:
- LOG.warning("Removed the following options '%s'.", ", ".join(removed_options))
-
- config = {}
- except Exception as e:
- msg = f"Failed to parse yt-dlp cli options. {e!s}"
- raise ValueError(msg) from e
-
- return {
- "url": url,
- "preset": preset,
- "folder": folder,
- "cookies": cookies,
- "config": config if isinstance(config, dict) else {},
- "template": template,
- "cli": cli if isinstance(cli, str) else "",
- "extras": extras if isinstance(extras, dict) else {},
- }
+ return await self.queue.add(item=item)
diff --git a/app/library/config.py b/app/library/config.py
index fb4c0ab3..4f117760 100644
--- a/app/library/config.py
+++ b/app/library/config.py
@@ -12,7 +12,7 @@ import coloredlogs
from dotenv import load_dotenv
from yt_dlp.version import __version__ as YTDLP_VERSION
-from .Utils import REMOVE_KEYS, arg_converter, load_file, merge_dict
+from .Utils import arg_converter
from .version import APP_VERSION
@@ -321,11 +321,10 @@ class Config:
except Exception as e:
LOG.error(f"Error starting debugpy server at '0.0.0.0:{self.debugpy_port}'. {e}")
- ytdlp_cli: str = os.path.join(self.config_path, "ytdlp.cli")
- optsFile: str = os.path.join(self.config_path, "ytdlp.json")
- if os.path.exists(ytdlp_cli) and os.path.getsize(ytdlp_cli) > 2:
- LOG.info(f"Loading yt-dlp custom options from '{ytdlp_cli}'.")
- with open(ytdlp_cli) as f:
+ opts_file: str = os.path.join(self.config_path, "ytdlp.cli")
+ if os.path.exists(opts_file) and os.path.getsize(opts_file) > 2:
+ LOG.info(f"Loading yt-dlp custom options from '{opts_file}'.")
+ with open(opts_file) as f:
ytdlp_cli_opts = f.read().strip()
if ytdlp_cli_opts:
try:
@@ -343,41 +342,15 @@ class Config:
if len(removed_options) > 0:
LOG.warning(
- "Removed the following options: '%s' from '%s'", ", ".join(removed_options), ytdlp_cli
+ "Removed the following options: '%s' from '%s'", ", ".join(removed_options), opts_file
)
except Exception as e:
- msg = f"Failed to parse yt-dlp cli options from '{ytdlp_cli}'. '{e!s}'."
+ msg = f"Failed to parse yt-dlp cli options from '{opts_file}'. '{e!s}'."
raise ValueError(msg) from e
else:
- LOG.warning(f"Empty yt-dlp custom options file '{ytdlp_cli}'.")
- # @Deprecated - To be removed in future versions.
- elif os.path.exists(optsFile) and os.path.getsize(optsFile) > 5:
- LOG.warning("The JSON ytdlp.json options file is deprecated, please use 'ytdlp.cli' file instead.")
- LOG.info(f"Loading yt-dlp custom options from '{optsFile}'.")
-
- (opts, status, error) = load_file(optsFile, dict)
- if not status:
- LOG.error(f"Could not load yt-dlp custom options from '{optsFile}'. {error}")
- sys.exit(1)
- if isinstance(opts, dict):
- bad_options = {k: v for d in REMOVE_KEYS for k, v in d.items()}
- removed_options = []
- for key in opts.copy():
- if key not in bad_options:
- continue
-
- removed_options.append(bad_options[key])
- opts.pop(key, None)
-
- if len(removed_options) > 0:
- LOG.warning(
- "Removed the following options: '%s' from 'ytdlp.json' file.", ", ".join(removed_options)
- )
- self.ytdl_options = merge_dict(self.ytdl_options, opts)
- else:
- LOG.error(f"Invalid yt-dlp custom options file '{optsFile}'.")
+ LOG.warning(f"Empty yt-dlp custom options file '{opts_file}'.")
else:
- LOG.info(f"No yt-dlp custom options found at '{ytdlp_cli}'.")
+ LOG.info(f"No yt-dlp custom options found at '{opts_file}'.")
self.ytdl_options["socket_timeout"] = self.socket_timeout
diff --git a/ui/components/NewDownload.vue b/ui/components/NewDownload.vue
index 57852735..8ff3f473 100644
--- a/ui/components/NewDownload.vue
+++ b/ui/components/NewDownload.vue
@@ -5,31 +5,39 @@
-
+
+
-
-
-
+
+
+ You can add multiple URLs separated by a comma ,.
+