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 ,. +
-
+
-
-
+
-