diff --git a/Pipfile b/Pipfile index 9a95eed..4deaf3b 100644 --- a/Pipfile +++ b/Pipfile @@ -36,6 +36,8 @@ redis = {extras = ["hiredis"], version = "*"} dramatiq = {extras = ["redis", "watch"], version = "*"} gunicorn = "*" scrapy = "*" +aioqbt = {git = "git+https://github.com/mhdzumair/aioqbt.git"} +aiowebdav = "*" [dev-packages] pysocks = "*" diff --git a/Pipfile.lock b/Pipfile.lock index a48506c..96cd019 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "30880a6cb492bb705c7671889e64ef1113f718c771ed9c6545b3431e37e1f396" + "sha256": "c4f0cccc167e5c86cc664635228cca7445feea11d2c58ebefd1d1a3cbbadb7b9" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,118 @@ ] }, "default": { + "aiofiles": { + "hashes": [ + "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107", + "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.1" + }, + "aiohttp": { + "hashes": [ + "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168", + "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb", + "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5", + "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f", + "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc", + "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c", + "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29", + "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4", + "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc", + "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc", + "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63", + "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e", + "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d", + "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a", + "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60", + "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38", + "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b", + "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2", + "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53", + "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5", + "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4", + "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96", + "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58", + "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa", + "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321", + "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae", + "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce", + "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8", + "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194", + "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c", + "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf", + "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d", + "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869", + "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b", + "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52", + "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528", + "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5", + "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1", + "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4", + "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8", + "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d", + "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7", + "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5", + "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54", + "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3", + "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5", + "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c", + "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29", + "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3", + "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747", + "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672", + "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5", + "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11", + "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca", + "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768", + "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6", + "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2", + "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533", + "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6", + "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266", + "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d", + "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec", + "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5", + "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1", + "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b", + "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679", + "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283", + "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb", + "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b", + "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3", + "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051", + "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511", + "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e", + "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d", + "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542", + "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f" + ], + "markers": "python_version >= '3.8'", + "version": "==3.9.3" + }, + "aioqbt": { + "git": "git+https://github.com/mhdzumair/aioqbt.git", + "markers": "python_version >= '3.7'", + "ref": "4be694b899a13df86d420b88ab777ab559bef8e9" + }, + "aiosignal": { + "hashes": [ + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "aiowebdav": { + "hashes": [ + "sha256:1b7be6122ab69f06f1639bfea9fd3beb43a7d375b5e1ced560b832e04c000026", + "sha256:8345f8a55a5eea4e029de0b5c7ba38fbeaaee3e0fdc1e0ca29e60d997643cc2f" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.1.0rc5" + }, "annotated-types": { "hashes": [ "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", @@ -41,6 +153,14 @@ "markers": "python_version >= '3.6'", "version": "==3.10.4" }, + "async-timeout": { + "hashes": [ + "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", + "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" + ], + "markers": "python_version >= '3.7'", + "version": "==4.0.3" + }, "attrs": { "hashes": [ "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", @@ -396,6 +516,89 @@ ], "version": "==0.2.1" }, + "frozenlist": { + "hashes": [ + "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", + "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", + "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", + "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", + "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", + "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", + "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", + "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", + "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", + "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", + "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", + "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", + "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", + "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", + "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", + "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", + "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", + "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", + "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", + "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", + "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", + "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", + "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", + "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", + "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", + "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", + "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", + "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", + "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", + "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", + "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", + "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", + "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", + "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", + "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", + "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", + "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", + "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", + "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", + "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", + "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", + "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", + "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", + "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", + "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", + "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", + "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", + "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", + "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", + "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", + "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", + "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", + "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", + "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", + "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", + "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", + "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", + "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", + "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", + "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", + "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", + "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", + "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", + "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", + "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", + "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", + "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", + "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", + "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", + "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", + "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", + "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", + "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", + "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", + "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", + "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", + "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.1" + }, "gevent": { "hashes": [ "sha256:17ebb6f981389c17321b95bc59ff6a65edeb98f3205884babaec9cb514aaa0d3", @@ -1217,12 +1420,12 @@ }, "pydantic": { "hashes": [ - "sha256:37a5432e54b12fecaa1049c5195f3d860a10e01bdfd24f1840ef14bd0d3aeab3", - "sha256:a09be1c3d28f3abe37f8a78af58284b236a92ce520105ddc91a6d29ea1176ba7" + "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a", + "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.6.2" + "version": "==2.6.3" }, "pydantic-core": { "hashes": [ @@ -1442,12 +1645,12 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "python-dotenv": { "hashes": [ @@ -1629,11 +1832,11 @@ "hiredis" ], "hashes": [ - "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f", - "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f" + "sha256:3f82cc80d350e93042c8e6e7a5d0596e4dd68715babffba79492733e1f367037", + "sha256:4caa8e1fcb6f3c0ef28dba99535101d80934b7d4cd541bbb47f4a3826ee472d1" ], "markers": "python_version >= '3.7'", - "version": "==5.0.1" + "version": "==5.0.2" }, "requests": { "hashes": [ @@ -1703,11 +1906,11 @@ }, "sniffio": { "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.3.1" }, "soupsieve": { "hashes": [ @@ -1815,19 +2018,19 @@ }, "twisted": { "hashes": [ - "sha256:4ae8bce12999a35f7fe6443e7f1893e6fe09588c8d2bed9c35cdce8ff2d5b444", - "sha256:987847a0790a2c597197613686e2784fd54167df3a55d0fb17c8412305d76ce5" + "sha256:039f2e6a49ab5108abd94de187fa92377abe5985c7a72d68d0ad266ba19eae63", + "sha256:6b38b6ece7296b5e122c9eb17da2eeab3d98a198f50ca9efd00fb03e5b4fd4ae" ], "markers": "python_full_version >= '3.8.0'", - "version": "==23.10.0" + "version": "==24.3.0" }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.10.0" }, "tzlocal": { "hashes": [ diff --git a/db/schemas.py b/db/schemas.py index 4bcbaff..c44bea9 100644 --- a/db/schemas.py +++ b/db/schemas.py @@ -62,6 +62,18 @@ class Streams(BaseModel): streams: Optional[list[Stream]] = [] +class QBittorrentConfig(BaseModel): + qbittorrent_url: str + qbittorrent_username: str + qbittorrent_password: str + seeding_time_limit: int = 1440 # 24 hours + seeding_ratio_limit: float = 1.0 + play_video_after: int = Field(default=100, le=100, ge=0) + webdav_url: str + webdav_username: str + webdav_password: str + + class StreamingProvider(BaseModel): service: Literal[ "realdebrid", @@ -72,17 +84,29 @@ class StreamingProvider(BaseModel): "pikpak", "torbox", "premiumize", + "qbittorrent", ] token: str | None = None username: str | None = None password: str | None = None enable_watchlist_catalogs: bool = True + qbittorrent_config: QBittorrentConfig | None = None @model_validator(mode="after") def validate_token_or_username_password(self) -> "StreamingProvider": - # validating the token or username and password - if not self.token and not self.username and not self.password: - raise ValueError("Either token or username and password must be present") + # validating the token or (username and password) or qbittorrent_config + if self.service is None: + return self + if self.service == "pikpak": + if not self.username or not self.password: + raise ValueError("Username and password are required") + elif self.service == "qbittorrent": + if not self.qbittorrent_config: + raise ValueError("qbittorrent_config is required") + else: + if not self.token or not (self.username and self.password): + raise ValueError("Token or username and password are required") + return self class Config: diff --git a/resources/exceptions/qbittorrent_error.mp4 b/resources/exceptions/qbittorrent_error.mp4 new file mode 100644 index 0000000..89a0e8c Binary files /dev/null and b/resources/exceptions/qbittorrent_error.mp4 differ diff --git a/resources/exceptions/webdav_error.mp4 b/resources/exceptions/webdav_error.mp4 new file mode 100644 index 0000000..082e235 Binary files /dev/null and b/resources/exceptions/webdav_error.mp4 differ diff --git a/resources/html/configure.html b/resources/html/configure.html index 8172fbe..004582b 100644 --- a/resources/html/configure.html +++ b/resources/html/configure.html @@ -39,6 +39,7 @@ + @@ -100,6 +101,74 @@ + +
diff --git a/resources/js/config_script.js b/resources/js/config_script.js index 8aa2bf6..d761074 100644 --- a/resources/js/config_script.js +++ b/resources/js/config_script.js @@ -177,9 +177,15 @@ function updateProviderFields(isChangeEvent = false) { if (servicesRequiringCredentials.includes(provider)) { setElementDisplay('credentials', 'block'); setElementDisplay('token_input', 'none'); + setElementDisplay('qbittorrent_config', 'none'); + } else if (provider === 'qbittorrent') { + setElementDisplay('qbittorrent_config', 'block'); + setElementDisplay('credentials', 'none'); + setElementDisplay('token_input', 'none'); } else { setElementDisplay('credentials', 'none'); setElementDisplay('token_input', 'block'); + setElementDisplay('qbittorrent_config', 'none'); } setElementDisplay('watchlist_section', 'block'); watchlistLabel.textContent = `Enable ${provider.charAt(0).toUpperCase() + provider.slice(1)} Watchlist`; @@ -187,6 +193,7 @@ function updateProviderFields(isChangeEvent = false) { setElementDisplay('credentials', 'none'); setElementDisplay('token_input', 'none'); setElementDisplay('watchlist_section', 'none'); + setElementDisplay('qbittorrent_config', 'none'); } // Reset the fields only if this is triggered by an onchange event @@ -260,6 +267,23 @@ function getUserData() { validateInput('password', document.getElementById('password').value); streamingProviderData.username = document.getElementById('username').value; streamingProviderData.password = document.getElementById('password').value; + } else if (provider === 'qbittorrent') { + streamingProviderData.qbittorrent_config = { + qbittorrent_url: document.getElementById('qbittorrent_url').value, + qbittorrent_username: document.getElementById('qbittorrent_username').value, + qbittorrent_password: document.getElementById('qbittorrent_password').value, + seeding_time_limit: parseInt(document.getElementById('seeding_time_limit').value, 10), + seeding_ratio_limit: parseFloat(document.getElementById('seeding_ratio_limit').value), + play_video_after: parseInt(document.getElementById('play_video_after_download').value, 10), + webdav_url: document.getElementById('webdav_url').value, + webdav_username: document.getElementById('webdav_username').value, + webdav_password: document.getElementById('webdav_password').value, + }; + // Validate qBittorrent-specific inputs + validateInput('qbittorrent_url', streamingProviderData.qbittorrent_config.qbittorrent_url); + validateInput('seeding_time_limit', !isNaN(streamingProviderData.qbittorrent_config.seeding_time_limit)); + validateInput('seeding_ratio_limit', !isNaN(streamingProviderData.qbittorrent_config.seeding_ratio_limit)); + validateInput('play_video_after_download', !isNaN(streamingProviderData.qbittorrent_config.play_video_after)); } else { validateInput('provider_token', document.getElementById('provider_token').value); streamingProviderData.token = document.getElementById('provider_token').value; @@ -413,6 +437,8 @@ document.addEventListener('DOMContentLoaded', function () { setupPasswordToggle('password', 'togglePassword', 'togglePasswordIcon'); setupPasswordToggle('api_password', 'toggleApiPassword', 'toggleApiPasswordIcon'); + setupPasswordToggle('qbittorrent_password', 'toggleQbittorrentPassword', 'toggleQbittorrentPasswordIcon'); + setupPasswordToggle('webdav_password', 'toggleWebdavPassword', 'toggleWebdavPasswordIcon'); if (navigator.share) { document.getElementById('shareBtn').style.display = 'block'; diff --git a/streaming_providers/qbittorrent/__init__.py b/streaming_providers/qbittorrent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streaming_providers/qbittorrent/utils.py b/streaming_providers/qbittorrent/utils.py new file mode 100644 index 0000000..cefb63f --- /dev/null +++ b/streaming_providers/qbittorrent/utils.py @@ -0,0 +1,332 @@ +import asyncio +from datetime import timedelta +from os import path +from urllib.parse import urljoin, urlparse, quote + +from aiohttp import ClientConnectorError +from aioqbt.api import AddFormBuilder, TorrentInfo +from aioqbt.client import create_client, APIClient +from aioqbt.exc import LoginError, AddTorrentError +from aiowebdav.client import Client as WebDavClient +from aiowebdav.exceptions import RemoteResourceNotFound, NoConnection +from thefuzz import fuzz + +from db.models import TorrentStreams +from db.schemas import UserData +from streaming_providers.exceptions import ProviderException + + +async def check_torrent_status( + qbittorrent: APIClient, info_hash: str +) -> TorrentInfo | None: + """Checks the status of a torrent with a given info_hash or torrent_name.""" + available_torrents = await qbittorrent.torrents.info(hashes=[info_hash]) + return available_torrents[0] if available_torrents else None + + +async def add_magnet( + qbittorrent: APIClient, magnet_link: str, info_hash: str, user_data: UserData +): + """Adds a magnet link to the qbittorrent server.""" + try: + await qbittorrent.torrents.add( + AddFormBuilder.with_client(qbittorrent) + .include_url(magnet_link) + .savepath(info_hash) + .seeding_time_limit( + timedelta( + minutes=user_data.streaming_provider.qbittorrent_config.seeding_time_limit + ) + ) + .ratio_limit( + user_data.streaming_provider.qbittorrent_config.seeding_ratio_limit + ) + .build() + ) + except AddTorrentError: + raise ProviderException( + "Failed to add the torrent to qBittorrent", "add_torrent_failed.mp4" + ) + + +async def wait_for_torrent_to_complete( + qbittorrent: APIClient, + info_hash: str, + play_video_after: int, + max_retries: int, + retry_interval: int, +): + """Waits for a torrent with the given info_hash to complete downloading.""" + retries = 0 + while retries < max_retries: + torrent = await check_torrent_status(qbittorrent, info_hash) + if torrent and torrent.progress * 100 >= play_video_after: + return + await asyncio.sleep(retry_interval) + retries += 1 + + raise ProviderException("Torrent not downloaded yet.", "torrent_not_downloaded.mp4") + + +async def get_files_from_folder( + webdav: WebDavClient, base_url_path: str, root_path: str +) -> list[dict]: + """Helper function to recursively get files from a folder including subfolders.""" + try: + contents = await webdav.list(root_path, get_info=True) + except RemoteResourceNotFound: + return [] + + files = [] + for item in contents: + # skip the root folder + if item["path"].endswith(root_path.removesuffix("/") + "/"): + continue + + if item["isdir"]: + files.extend( + await get_files_from_folder( + webdav, base_url_path, item["path"].removeprefix(base_url_path) + ) + ) + else: + item.update( + { + "name": path.basename(item["path"]), + "size": int(item["size"]), + } + ) + files.append(item) + + return files + + +async def find_file_in_folder_tree( + webdav: WebDavClient, user_data: UserData, info_hash: str, filename: str +) -> dict | None: + base_url_path = urlparse( + user_data.streaming_provider.qbittorrent_config.webdav_url + ).path + + files = await get_files_from_folder(webdav, base_url_path, info_hash) + if not files: + return None + + exact_match = next((file for file in files if file["name"] == filename), None) + if exact_match: + return exact_match + + # Fuzzy matching as a fallback + for file in files: + file["fuzzy_ratio"] = fuzz.ratio(filename, file["name"]) + selected_file = max(files, key=lambda x: x["fuzzy_ratio"]) + + # If the fuzzy ratio is less than 50, then select the largest file + if selected_file["fuzzy_ratio"] < 50: + selected_file = max(files, key=lambda x: x["size"]) + + if "video" not in selected_file["content_type"]: + raise ProviderException( + "No matching file available for this torrent", "no_matching_file.mp4" + ) + + return selected_file + + +async def initialize_qbittorrent(user_data: UserData): + try: + qbittorrent = await create_client( + user_data.streaming_provider.qbittorrent_config.qbittorrent_url + + "/api/v2/", + username=user_data.streaming_provider.qbittorrent_config.qbittorrent_username, + password=user_data.streaming_provider.qbittorrent_config.qbittorrent_password, + ) + except LoginError: + raise ProviderException( + "Invalid qBittorrent credentials", "invalid_credentials.mp4" + ) + except ClientConnectorError: + raise ProviderException( + "Failed to connect to qBittorrent", "qbittorrent_error.mp4" + ) + return qbittorrent + + +async def initialize_webdav(user_data: UserData): + webdav_options = { + "webdav_hostname": user_data.streaming_provider.qbittorrent_config.webdav_url, + "webdav_login": user_data.streaming_provider.qbittorrent_config.webdav_username, + "webdav_password": user_data.streaming_provider.qbittorrent_config.webdav_password, + } + + webdav = WebDavClient(webdav_options) + + try: + if not await webdav.check(): + raise ProviderException( + "Invalid WebDAV credentials", "invalid_credentials.mp4" + ) + except NoConnection: + raise ProviderException( + "Failed to connect to the WebDAV server", "webdav_error.mp4" + ) + return webdav + + +async def handle_torrent_status( + qbittorrent: APIClient, + info_hash: str, + play_video_after: int, + max_retries: int, + retry_interval: int, +): + torrent = await check_torrent_status(qbittorrent, info_hash) + if not torrent: + return + + if torrent.progress * 100 >= play_video_after: + return torrent + + await wait_for_torrent_to_complete( + qbittorrent, info_hash, play_video_after, max_retries, retry_interval + ) + return torrent + + +async def retrieve_or_download_file( + qbittorrent: APIClient, + webdav: WebDavClient, + user_data: UserData, + play_video_after: int, + filename: str, + magnet_link: str, + info_hash: str, + max_retries: int, + retry_interval: int, +): + selected_file = await find_file_in_folder_tree( + webdav, user_data, info_hash, filename + ) + if not selected_file: + await add_magnet(qbittorrent, magnet_link, info_hash, user_data) + await wait_for_torrent_to_complete( + qbittorrent, info_hash, play_video_after, max_retries, retry_interval + ) + selected_file = await find_file_in_folder_tree( + webdav, user_data, info_hash, filename + ) + if selected_file is None: + raise ProviderException( + "No matching file available for this torrent", "no_matching_file.mp4" + ) + return selected_file + + +def generate_webdav_url(user_data: UserData, selected_file: dict) -> str: + webdav_url = user_data.streaming_provider.qbittorrent_config.webdav_url + webdav_username = user_data.streaming_provider.qbittorrent_config.webdav_username + webdav_password = user_data.streaming_provider.qbittorrent_config.webdav_password + + # Check if credentials are provided + if webdav_username and webdav_password: + # URL encode the username and password + webdav_username = quote(webdav_username, safe="") + webdav_password = quote(webdav_password, safe="") + parsed_url = urlparse(webdav_url) + netloc = f"{webdav_username}:{webdav_password}@{parsed_url.netloc}" + base_url = parsed_url._replace(netloc=netloc).geturl() + else: + base_url = webdav_url + + return urljoin( + base_url, + quote(selected_file["path"]), # Ensure the path is URL encoded + ) + + +async def get_direct_link_from_qbittorrent( + info_hash: str, + magnet_link: str, + user_data: UserData, + stream: TorrentStreams, + filename: str, + max_retries=5, + retry_interval=5, +) -> str: + """Get the direct link from the qBittorrent server.""" + qbittorrent = webdav = None + play_video_after = user_data.streaming_provider.qbittorrent_config.play_video_after + try: + qbittorrent = await initialize_qbittorrent(user_data) + webdav = await initialize_webdav(user_data) + await handle_torrent_status( + qbittorrent, + info_hash, + play_video_after, + max_retries, + retry_interval, + ) + + selected_file = await retrieve_or_download_file( + qbittorrent, + webdav, + user_data, + play_video_after, + filename or stream.torrent_name, + magnet_link, + info_hash, + max_retries, + retry_interval, + ) + + return generate_webdav_url(user_data, selected_file) + finally: + if qbittorrent: + await qbittorrent.close() + if webdav: + await webdav.close() + + +async def update_qbittorrent_cache_status( + streams: list[TorrentStreams], user_data: UserData +): + """Updates the cache status of streams based on qBittorrent's instant availability.""" + qbittorrent = None + try: + qbittorrent = await initialize_qbittorrent(user_data) + torrents_info = await qbittorrent.torrents.info( + hashes=[stream.id for stream in streams] + ) + torrents_dict = {torrent.hash: torrent.progress for torrent in torrents_info} + except ProviderException: + return + finally: + if qbittorrent: + await qbittorrent.close() + + for stream in streams: + stream.cached = torrents_dict.get(stream.id, 0) == 1 + + +async def fetch_info_hashes_from_webdav( + user_data: UserData, +) -> list[str]: + """Fetches the info_hashes from directories in the WebDAV server that are named after the torrent's info hashes.""" + webdav = None + try: + webdav = await initialize_webdav(user_data) + directories = await webdav.list() + + # Filter out directory names that match the length of an info hash (40 chars) plus the trailing slash + info_hashes = [ + dirname.removesuffix("/") + for dirname in directories + if len(dirname) == 41 and dirname.endswith("/") + ] + + return info_hashes + except ProviderException: + return [] + finally: + if webdav: + await webdav.close() diff --git a/streaming_providers/routes.py b/streaming_providers/routes.py index 4240a07..43ca694 100644 --- a/streaming_providers/routes.py +++ b/streaming_providers/routes.py @@ -23,6 +23,7 @@ from streaming_providers.realdebrid.utils import get_direct_link_from_realdebrid from streaming_providers.seedr.api import router as seedr_router from streaming_providers.seedr.utils import get_direct_link_from_seedr from streaming_providers.torbox.utils import get_direct_link_from_torbox +from streaming_providers.qbittorrent.utils import get_direct_link_from_qbittorrent from utils import crypto, torrent, wrappers, const router = APIRouter() @@ -85,6 +86,10 @@ async def streaming_provider_endpoint( video_url = get_direct_link_from_premiumize( info_hash, magnet_link, user_data, stream.torrent_name, filename, 1, 0 ) + elif user_data.streaming_provider.service == "qbittorrent": + video_url = await get_direct_link_from_qbittorrent( + info_hash, magnet_link, user_data, stream, filename, 1, 0 + ) else: video_url = get_direct_link_from_debridlink( info_hash, magnet_link, user_data, filename, stream.file_index, 1, 0 diff --git a/utils/parser.py b/utils/parser.py index 497c3fd..7c2f5fc 100644 --- a/utils/parser.py +++ b/utils/parser.py @@ -28,6 +28,10 @@ from streaming_providers.premiumize.utils import ( update_pm_cache_status, fetch_downloaded_info_hashes_from_premiumize, ) +from streaming_providers.qbittorrent.utils import ( + update_qbittorrent_cache_status, + fetch_info_hashes_from_webdav, +) from streaming_providers.realdebrid.utils import ( update_rd_cache_status, fetch_downloaded_info_hashes_from_rd, @@ -57,6 +61,7 @@ CACHE_UPDATE_FUNCTIONS = { "seedr": update_seedr_cache_status, "torbox": update_torbox_cache_status, "premiumize": update_pm_cache_status, + "qbittorrent": update_qbittorrent_cache_status, } # Define provider-specific downloaded info hashes fetch functions @@ -69,6 +74,7 @@ FETCH_DOWNLOADED_INFO_HASHES_FUNCTIONS = { "seedr": fetch_downloaded_info_hashes_from_seedr, "torbox": fetch_downloaded_info_hashes_from_torbox, "premiumize": fetch_downloaded_info_hashes_from_premiumize, + "qbittorrent": fetch_info_hashes_from_webdav, }