Added qBittorrent - WebDav support

This commit is contained in:
mhdzumair
2024-03-04 01:18:13 +05:30
parent bcbe51035a
commit 2452b4ef8f
11 changed files with 689 additions and 22 deletions
+2
View File
@@ -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 = "*"
Generated
+222 -19
View File
@@ -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": [
+27 -3
View File
@@ -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:
Binary file not shown.
Binary file not shown.
+69
View File
@@ -39,6 +39,7 @@
<option value="debridlink" {% if user_data.streaming_provider.service=='debridlink' %}selected{% endif %}>Debrid-Link (Premium)</option>
<option value="premiumize" {% if user_data.streaming_provider.service=='premiumize' %}selected{% endif %}>Premiumize (Premium)</option>
<option value="alldebrid" {% if user_data.streaming_provider.service=='alldebrid' %}selected{% endif %}>AllDebrid - Local Only - (Premium)</option>
<option value="qbittorrent" {% if user_data.streaming_provider.service=='qbittorrent' %}selected{% endif %}>qBittorrent - WebDav (Free/Premium)</option>
</select>
<!-- Affiliate Signup Links -->
@@ -100,6 +101,74 @@
</div>
</div>
<!-- qBittorrent Configuration -->
<div id="qbittorrent_config" class="mt-3" style="display:none">
<h5 class="mt-3">qBittorrent Configuration</h5>
<div class="mb-3">
<label for="qbittorrent_url">qBittorrent URL:</label>
<input class="form-control" type="text" id="qbittorrent_url" name="qbittorrent_url" placeholder="Enter qBittorrent URL"
value="{{ user_data.streaming_provider.qbittorrent_config.qbittorrent_url if user_data.streaming_provider and user_data.streaming_provider.qbittorrent_config else '' }}">
</div>
<div class="mb-3">
<label for="qbittorrent_username">qBittorrent Username: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
title="Enter the qBittorrent username for the qBittorrent server. if Authentication not required, leave it blank."></span></label>
<input class="form-control" type="text" id="qbittorrent_username" name="qbittorrent_username" placeholder="Enter Username"
value="{{ user_data.streaming_provider.qbittorrent_config.qbittorrent_username if user_data.streaming_provider.qbittorrent_config else '' }}">
</div>
<div class="mb-3">
<label for="qbittorrent_password">qBittorrent Password: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
title="Enter the qBittorrent password for the qBittorrent server. if Authentication not required, leave it blank."></span></label>
<div class="input-group">
<input class="form-control" type="password" id="qbittorrent_password" name="qbittorrent_password" placeholder="Enter qBittorrent Password">
<button class="btn btn-outline-secondary" type="button" id="toggleQbittorrentPassword">
<span id="toggleQbittorrentPasswordIcon" class="bi bi-eye"></span>
</button>
</div>
</div>
<div class="mb-3">
<label for="seeding_time_limit">Seeding Time Limit (minutes): <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
title="Set the seeding time limit for torrents in minutes."></span></label>
<input class="form-control" type="number" id="seeding_time_limit" name="seeding_time_limit" placeholder="Enter Seeding Time Limit"
value="{{ user_data.streaming_provider.qbittorrent_config.seeding_time_limit if user_data.streaming_provider.qbittorrent_config else 1440 }}">
</div>
<div class="mb-3">
<label for="seeding_ratio_limit">Seeding Ratio Limit: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
title="Set the seeding ratio limit for torrents."></span></label>
<input class="form-control" type="text" id="seeding_ratio_limit" name="seeding_ratio_limit" placeholder="Enter Seeding Ratio Limit"
value="{{ user_data.streaming_provider.qbittorrent_config.seeding_ratio_limit if user_data.streaming_provider.qbittorrent_config else 1.0 }}">
</div>
<div class="mb-3">
<label for="play_video_after_download">Play Video After Download: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
title="Enter the percentage of the video to be downloaded before playing."></span></label>
<input class="form-control" type="number" id="play_video_after_download" name="play_video_after_download" placeholder="Enter Play Video After Download"
value="{{ user_data.streaming_provider.qbittorrent_config.play_video_after_download if user_data.streaming_provider.qbittorrent_config else 100 }}">
<!-- WebDAV Configuration -->
<h5 class="mt-3">WebDAV Configuration</h5>
<div class="mb-3">
<label for="webdav_url">WebDAV URL: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
title="Enter the WebDAV URL for the qBittorrent WebDAV server with the format: http{s}://host/path."></span></label>
<input class="form-control" type="text" id="webdav_url" name="webdav_url" placeholder="Enter WebDAV URL"
value="{{ user_data.streaming_provider.qbittorrent_config.webdav_url if user_data.streaming_provider.qbittorrent_config else '' }}">
</div>
<div class="mb-3">
<label for="webdav_username">WebDAV Username: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
title="Enter the WebDAV username for the WebDAV server. if Authentication not required, leave it blank."></span></label>
<input class="form-control" type="text" id="webdav_username" name="webdav_username" placeholder="Enter WebDAV Username"
value="{{ user_data.streaming_provider.qbittorrent_config.webdav_username if user_data.streaming_provider.qbittorrent_config else '' }}">
</div>
<div class="mb-3">
<label for="webdav_password">WebDAV Password: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
title="Enter the WebDAV password for the WebDAV server. if Authentication not required, leave it blank."></span></label>
<div class="input-group">
<input class="form-control" type="password" id="webdav_password" name="webdav_password" placeholder="Enter WebDAV Password">
<button class="btn btn-outline-secondary" type="button" id="toggleWebdavPassword">
<span id="toggleWebdavPasswordIcon" class="bi bi-eye"></span>
</button>
</div>
</div>
</div>
</div>
<!-- Catalog Configuration -->
+26
View File
@@ -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';
+332
View File
@@ -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()
+5
View File
@@ -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
+6
View File
@@ -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,
}