diff --git a/Dockerfile b/Dockerfile index a7adc0c..2bcc30a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,11 @@ ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' TZ='Asia/Kolkata ENV DEBIAN_FRONTEND='noninteractive' RUN apt-get update && apt-get upgrade -y && \ apt-get install -y libcrypto++-dev libfreeimage-dev && \ + apt-get install -y software-properties-common && \ + add-apt-repository ppa:qbittorrent-team/qbittorrent-stable && \ + apt-get install -y qbittorrent-nox && \ + apt-get purge -y software-properties-common && \ + apt-get autoremove -y && \ apt-get clean && rm -rf /var/lib/apt/lists/* RUN pip3 install --no-cache-dir /root/mega-sdk/bindings/python/dist/megasdk-*.whl WORKDIR /usr/src/app diff --git a/Pipfile b/Pipfile index 164a51d..1d3659c 100644 --- a/Pipfile +++ b/Pipfile @@ -11,6 +11,8 @@ google-auth-oauthlib = "==0.4.4" psutil = "==5.8.0" python-magic = "==0.4.24" python-telegram-bot = "==13.5" +qbittorrent-api = "==2021.8.23" +torrentool = "==1.1.1" youtube_dl = "==2021.6.6" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index dd32fcf..6591bf5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bb71303345265a6ced1d3002a7f1be6e1a9f69aad5c9116895db167bc0bf8157" + "sha256": "6e3dadabbe124dd4569f9cdfad3ea9905ebcabf9e6a2763a5e79f9dfff6452ee" }, "pipfile-spec": 6, "requires": { @@ -62,34 +62,34 @@ }, "cachetools": { "hashes": [ - "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", - "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" + "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693", + "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1" ], "markers": "python_version ~= '3.5'", - "version": "==4.2.2" + "version": "==4.2.4" }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], - "version": "==2021.5.30" + "version": "==2021.10.8" }, "charset-normalizer": { "hashes": [ - "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", - "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" ], "markers": "python_version >= '3'", - "version": "==2.0.4" + "version": "==2.0.9" }, "google-api-core": { "hashes": [ - "sha256:108cf94336aed7e614eafc53933ef02adf63b9f0fd87e8f8212acaa09eaca456", - "sha256:1d63e2b28057d79d64795c9a70abcecb5b7e96da732d011abf09606a39b48701" + "sha256:c77ffc8b4981b44efdb9d68431fd96d21dbd39545c29552bbe79b9b7dd2c3689", + "sha256:ed59c6a695a81f601e4ba0f37ca9dbde3c43b3309e161a1a8946f266db4a0c4e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.31.1" + "version": "==1.31.4" }, "google-api-python-client": { "hashes": [ @@ -101,11 +101,11 @@ }, "google-auth": { "hashes": [ - "sha256:bd6aa5916970a823e76ffb3d5c3ad3f0bedafca0a7fa53bc15149ab21cb71e05", - "sha256:f1094088bae046fb06f3d1a3d7df14717e8d959e9105b79c57725bd4e17597a2" + "sha256:997516b42ecb5b63e8d80f5632c1a61dddf41d2a4c2748057837e06e00014258", + "sha256:b7033be9028c188ee30200b204ea00ed82ea1162e8ac1df4aa6ded19a191d88e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.34.0" + "version": "==1.35.0" }, "google-auth-httplib2": { "hashes": [ @@ -133,18 +133,19 @@ }, "httplib2": { "hashes": [ - "sha256:0b12617eeca7433d4c396a100eaecfa4b08ee99aa881e6df6e257a7aad5d533d", - "sha256:2ad195faf9faf079723f6714926e9a9061f694d07724b846658ce08d40f522b4" + "sha256:6b937120e7d786482881b44b8eec230c1ee1c5c1d06bce8cc865f25abbbf713b", + "sha256:e404681d2fbcec7506bcb52c503f2b021e95bee0ef7d01e5c221468a2406d8dc" ], - "version": "==0.19.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.20.2" }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], "markers": "python_version >= '3'", - "version": "==3.2" + "version": "==3.3" }, "loguru": { "hashes": [ @@ -164,43 +165,41 @@ }, "packaging": { "hashes": [ - "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", - "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], "markers": "python_version >= '3.6'", - "version": "==21.0" + "version": "==21.3" }, "protobuf": { "hashes": [ - "sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637", - "sha256:145ce0af55c4259ca74993ddab3479c78af064002ec8227beb3d944405123c71", - "sha256:14c1c9377a7ffbeaccd4722ab0aa900091f52b516ad89c4b0c3bb0a4af903ba5", - "sha256:1556a1049ccec58c7855a78d27e5c6e70e95103b32de9142bae0576e9200a1b0", - "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539", - "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87", - "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e", - "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde", - "sha256:59e5cf6b737c3a376932fbfb869043415f7c16a0cf176ab30a5bbc419cd709c1", - "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d", - "sha256:6ce4d8bf0321e7b2d4395e253f8002a1a5ffbcfd7bcc0a6ba46712c07d47d0b4", - "sha256:6d847c59963c03fd7a0cd7c488cadfa10cda4fff34d8bc8cba92935a91b7a037", - "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b", - "sha256:7a4c97961e9e5b03a56f9a6c82742ed55375c4a25f2692b625d4087d02ed31b9", - "sha256:85d6303e4adade2827e43c2b54114d9a6ea547b671cb63fafd5011dc47d0e13d", - "sha256:8727ee027157516e2c311f218ebf2260a18088ffb2d29473e82add217d196b1c", - "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1", - "sha256:9b7a5c1022e0fa0dbde7fd03682d07d14624ad870ae52054849d8960f04bc764", - "sha256:a22b3a0dbac6544dacbafd4c5f6a29e389a50e3b193e2c70dae6bbf7930f651d", - "sha256:a38bac25f51c93e4be4092c88b2568b9f407c27217d3dd23c7a57fa522a17554", - "sha256:a981222367fb4210a10a929ad5983ae93bd5a050a0824fc35d6371c07b78caf6", - "sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8", - "sha256:c56c050a947186ba51de4f94ab441d7f04fcd44c56df6e922369cc2e1a92d683", - "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47", - "sha256:ebcb546f10069b56dc2e3da35e003a02076aaa377caf8530fe9789570984a8d2", - "sha256:f0e59430ee953184a703a324b8ec52f571c6c4259d496a19d1cabcdc19dabc62", - "sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b" - ], - "version": "==3.17.3" + "sha256:038daf4fa38a7e818dd61f51f22588d61755160a98db087a046f80d66b855942", + "sha256:28ccea56d4dc38d35cd70c43c2da2f40ac0be0a355ef882242e8586c6d66666f", + "sha256:36d90676d6f426718463fe382ec6274909337ca6319d375eebd2044e6c6ac560", + "sha256:3cd0458870ea7d1c58e948ac8078f6ba8a7ecc44a57e03032ed066c5bb318089", + "sha256:5935c8ce02e3d89c7900140a8a42b35bc037ec07a6aeb61cc108be8d3c9438a6", + "sha256:615b426a177780ce381ecd212edc1e0f70db8557ed72560b82096bd36b01bc04", + "sha256:62a8e4baa9cb9e064eb62d1002eca820857ab2138440cb4b3ea4243830f94ca7", + "sha256:655264ed0d0efe47a523e2255fc1106a22f6faab7cc46cfe99b5bae085c2a13e", + "sha256:6e8ea9173403219239cdfd8d946ed101f2ab6ecc025b0fda0c6c713c35c9981d", + "sha256:71b0250b0cfb738442d60cab68abc166de43411f2a4f791d31378590bfb71bd7", + "sha256:74f33edeb4f3b7ed13d567881da8e5a92a72b36495d57d696c2ea1ae0cfee80c", + "sha256:77d2fadcf369b3f22859ab25bd12bb8e98fb11e05d9ff9b7cd45b711c719c002", + "sha256:8b30a7de128c46b5ecb343917d9fa737612a6e8280f440874e5cc2ba0d79b8f6", + "sha256:8e51561d72efd5bd5c91490af1f13e32bcba8dab4643761eb7de3ce18e64a853", + "sha256:a529e7df52204565bcd33738a7a5f288f3d2d37d86caa5d78c458fa5fabbd54d", + "sha256:b691d996c6d0984947c4cf8b7ae2fe372d99b32821d0584f0b90277aa36982d3", + "sha256:d80f80eb175bf5f1169139c2e0c5ada98b1c098e2b3c3736667f28cbbea39fc8", + "sha256:d83e1ef8cb74009bebee3e61cc84b1c9cd04935b72bca0cbc83217d140424995", + "sha256:d8919368410110633717c406ab5c97e8df5ce93020cfcf3012834f28b1fab1ea", + "sha256:db3532d9f7a6ebbe2392041350437953b6d7a792de10e629c1e4f5a6b1fe1ac6", + "sha256:e7b24c11df36ee8e0c085e5b0dc560289e4b58804746fb487287dda51410f1e2", + "sha256:e7e8d2c20921f8da0dea277dfefc6abac05903ceac8e72839b2da519db69206b", + "sha256:e813b1c9006b6399308e917ac5d298f345d95bb31f46f02b60cd92970a9afa17", + "sha256:fd390367fc211cc0ffcf3a9e149dfeca78fecc62adb911371db0cec5c8b7472d" + ], + "markers": "python_version >= '3.1'", + "version": "==3.19.1" }, "psutil": { "hashes": [ @@ -274,11 +273,11 @@ }, "pyparsing": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" + "markers": "python_version >= '3.1'", + "version": "==3.0.6" }, "python-magic": { "hashes": [ @@ -298,10 +297,26 @@ }, "pytz": { "hashes": [ - "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", - "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", + "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" + ], + "version": "==2021.3" + }, + "pytz-deprecation-shim": { + "hashes": [ + "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6", + "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==0.1.0.post0" + }, + "qbittorrent-api": { + "hashes": [ + "sha256:470f9deb539853f952707372ad840306c88b65c4ae761a7566500018e863fc94", + "sha256:596ad4ee47e0a9f2be8af17fba62caf2ee666b41f256b26155aca0487e10df11" ], - "version": "==2021.1" + "index": "pypi", + "version": "==2021.8.23" }, "requests": { "hashes": [ @@ -321,11 +336,19 @@ }, "rsa": { "hashes": [ - "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2", - "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9" + "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", + "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb" + ], + "markers": "python_version >= '3.6'", + "version": "==4.8" + }, + "setuptools": { + "hashes": [ + "sha256:b4c634615a0cf5b02cf83c7bedffc8da0ca439f00e79452699454da6fbd4153d", + "sha256:feb5ff19b354cde9efd2344ef6d5e79880ce4be643037641b49508bbb850d060" ], "markers": "python_version >= '3.6'", - "version": "==4.7.2" + "version": "==59.4.0" }, "six": { "hashes": [ @@ -390,13 +413,29 @@ "markers": "python_version >= '3.5'", "version": "==6.1" }, + "torrentool": { + "hashes": [ + "sha256:11e647c4f40c14b7e9d6087602b2a91b54743a6ad0e6cc41904b196e975f0d56", + "sha256:2d892e8a02749fa9ea57fb2dcf69c1794305c069e973f4726a0a51dc66283e9c" + ], + "index": "pypi", + "version": "==1.1.1" + }, + "tzdata": { + "hashes": [ + "sha256:3eee491e22ebfe1e5cfcc97a4137cd70f092ce59144d81f8924a844de05ba8f5", + "sha256:68dbe41afd01b867894bbdfd54fa03f468cfa4f0086bfb4adcd8de8f24f3ee21" + ], + "markers": "python_version >= '3.6'", + "version": "==2021.5" + }, "tzlocal": { "hashes": [ - "sha256:c736f2540713deb5938d789ca7c3fc25391e9a20803f05b60ec64987cf086559", - "sha256:f4e6e36db50499e0d92f79b67361041f048e2609d166e93456b50746dc4aef12" + "sha256:0f28015ac68a5c067210400a9197fc5d36ba9bc3f8eaf1da3cbd59acdfed9e09", + "sha256:28ba8d9fcb6c9a782d6e0078b4f6627af1ea26aeaa32b4eab5324abc7df4149f" ], "markers": "python_version >= '3.6'", - "version": "==3.0" + "version": "==4.1" }, "uritemplate": { "hashes": [ @@ -408,11 +447,11 @@ }, "urllib3": { "hashes": [ - "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", - "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", - "version": "==1.26.6" + "version": "==1.26.7" }, "websocket-client": { "hashes": [ @@ -434,84 +473,89 @@ "develop": { "bleach": { "hashes": [ - "sha256:c1685a132e6a9a38bf93752e5faab33a9517a6c0bb2f37b785e47bf253bdb51d", - "sha256:ffa9221c6ac29399cc50fcc33473366edd0cf8d5e2cbbbb63296dc327fb67cc8" + "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", + "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" ], "markers": "python_version >= '3.6'", - "version": "==4.0.0" + "version": "==4.1.0" }, "build": { "hashes": [ - "sha256:32290592c8ccf70ce84107962f6129407abf52cedaa752af28c0c95d99dfa2e7", - "sha256:d8d8417caff47888274d677f984de509554637dd1ea952d467b027849b06d83b" + "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f", + "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8" ], "index": "pypi", - "version": "==0.6.0.post1" + "version": "==0.7.0" }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], - "version": "==2021.5.30" + "version": "==2021.10.8" }, "cffi": { "hashes": [ - "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", - "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", - "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", - "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", - "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", - "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", - "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", - "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", - "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", - "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", - "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", - "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", - "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", - "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", - "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", - "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", - "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", - "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", - "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", - "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", - "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", - "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", - "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", - "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", - "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", - "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", - "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", - "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", - "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", - "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", - "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", - "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", - "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", - "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", - "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", - "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", - "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", - "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", - "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", - "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", - "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", - "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", - "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", - "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", - "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" - ], - "version": "==1.14.6" + "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", + "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", + "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", + "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", + "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", + "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", + "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", + "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", + "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", + "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", + "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", + "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", + "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", + "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", + "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", + "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", + "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", + "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", + "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", + "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", + "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", + "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", + "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", + "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", + "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", + "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", + "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", + "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", + "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", + "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", + "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", + "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", + "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", + "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", + "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", + "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", + "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", + "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", + "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", + "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", + "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", + "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", + "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", + "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", + "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", + "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", + "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", + "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", + "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", + "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" + ], + "version": "==1.15.0" }, "charset-normalizer": { "hashes": [ - "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", - "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" ], "markers": "python_version >= '3'", - "version": "==2.0.4" + "version": "==2.0.9" }, "colorama": { "hashes": [ @@ -523,105 +567,107 @@ }, "coverage": { "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", + "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", + "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", + "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", + "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", + "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", + "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", + "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", + "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", + "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", + "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", + "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", + "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", + "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", + "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", + "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", + "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", + "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", + "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", + "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", + "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", + "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", + "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", + "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", + "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", + "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", + "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", + "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", + "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", + "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", + "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", + "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", + "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", + "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", + "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", + "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", + "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", + "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", + "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", + "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", + "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", + "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", + "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", + "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", + "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", + "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", + "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" ], "index": "pypi", - "version": "==5.5" + "version": "==6.2" }, "cryptography": { "hashes": [ - "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", - "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", - "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", - "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", - "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", - "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", - "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", - "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", - "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", - "sha256:b01fd6f2737816cb1e08ed4807ae194404790eac7ad030b34f2ce72b332f5586", - "sha256:bf40af59ca2465b24e54f671b2de2c59257ddc4f7e5706dbd6930e26823668d3", - "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", - "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", - "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" + "sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681", + "sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed", + "sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4", + "sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568", + "sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e", + "sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f", + "sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f", + "sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712", + "sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e", + "sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58", + "sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44", + "sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6", + "sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d", + "sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636", + "sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba", + "sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120", + "sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3", + "sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d", + "sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b", + "sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81", + "sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8" ], "markers": "python_version >= '3.6'", - "version": "==3.4.7" + "version": "==36.0.0" }, "docutils": { "hashes": [ - "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", - "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" + "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", + "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.17.1" + "version": "==0.18.1" }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], "markers": "python_version >= '3'", - "version": "==3.2" + "version": "==3.3" }, "importlib-metadata": { "hashes": [ - "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f", - "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5" + "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", + "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" ], "markers": "python_version >= '3.6'", - "version": "==4.6.4" + "version": "==4.8.2" }, "jeepney": { "hashes": [ @@ -633,41 +679,41 @@ }, "keyring": { "hashes": [ - "sha256:b32397fd7e7063f8dd74a26db910c9862fc2109285fa16e3b5208bcb42a3e579", - "sha256:b7e0156667f5dcc73c1f63a518005cd18a4eb23fe77321194fefcc03748b21a4" + "sha256:3dc0f66062a4f8f6f2ce30d6a516e6e623e6c3c2e76864204ceaf64695408f07", + "sha256:88f206024295e3c6fb16bb0a60fb4bb7ec1185629dc5a729f12aa7c236d01387" ], "markers": "python_version >= '3.6'", - "version": "==23.1.0" + "version": "==23.4.0" }, "packaging": { "hashes": [ - "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", - "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], "markers": "python_version >= '3.6'", - "version": "==21.0" + "version": "==21.3" }, "pep517": { "hashes": [ - "sha256:3fa6b85b9def7ba4de99fb7f96fe3f02e2d630df8aa2720a5cf3b183f087a738", - "sha256:e1ba5dffa3a131387979a68ff3e391ac7d645be409216b961bc2efe6468ab0b2" + "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0", + "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161" ], - "version": "==0.11.0" + "version": "==0.12.0" }, "pkginfo": { "hashes": [ - "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779", - "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd" + "sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff", + "sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc" ], - "version": "==1.7.1" + "version": "==1.8.2" }, "pycparser": { "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20" + "version": "==2.21" }, "pygments": { "hashes": [ @@ -679,18 +725,18 @@ }, "pyparsing": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" + "markers": "python_version >= '3.1'", + "version": "==3.0.6" }, "readme-renderer": { "hashes": [ - "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", - "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" + "sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc", + "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8" ], - "version": "==29.0" + "version": "==30.0" }, "requests": { "hashes": [ @@ -732,35 +778,35 @@ }, "tomli": { "hashes": [ - "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", - "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" + "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", + "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" ], "markers": "python_version >= '3.6'", - "version": "==1.2.1" + "version": "==1.2.2" }, "tqdm": { "hashes": [ - "sha256:07856e19a1fe4d2d9621b539d3f072fa88c9c1ef1f3b7dd4d4953383134c3164", - "sha256:35540feeaca9ac40c304e916729e6b78045cbbeccd3e941b2868f09306798ac9" + "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c", + "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.62.1" + "version": "==4.62.3" }, "twine": { "hashes": [ - "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218", - "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936" + "sha256:5a3e3fb52b926827c99e050f0c1e5d8ae599848f3eb27764f19b886c09134590", + "sha256:8d6a0ad895576c97e9ad4a5da2d6adea37fd5434ecabace0054013d537ddbc6c" ], "index": "pypi", - "version": "==3.4.2" + "version": "==3.7.0" }, "urllib3": { "hashes": [ - "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", - "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", - "version": "==1.26.6" + "version": "==1.26.7" }, "webencodings": { "hashes": [ @@ -771,11 +817,11 @@ }, "zipp": { "hashes": [ - "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", - "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", + "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" ], "markers": "python_version >= '3.6'", - "version": "==3.5.0" + "version": "==3.6.0" } } } diff --git a/deploy b/deploy index 375eb5f..73be93f 160000 --- a/deploy +++ b/deploy @@ -1 +1 @@ -Subproject commit 375eb5fa5236742cf72b965e3d72c17340e44aac +Subproject commit 73be93f2251ea6735a96c795da8542dd45a24691 diff --git a/requirements.txt b/requirements.txt index 248b10b..cd60c1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,6 @@ google-auth-oauthlib==0.4.4 psutil==5.8.0 python-magic==0.4.24 python-telegram-bot==13.5 +qbittorrent-api==2021.8.23 +torrentool==1.1.1 youtube_dl==2021.6.6 diff --git a/tgmb/__init__.py b/tgmb/__init__.py index 48c5fc9..3caad46 100644 --- a/tgmb/__init__.py +++ b/tgmb/__init__.py @@ -1,11 +1,13 @@ # TODO: add sufficient documentation to the functions and classes in this module -# TODO: Code for Upload to Mega -# TODO: Code for user filters -# TODO: Add and Handle Exceptions -# TODO: Code for direct link generation +# TODO: code for Upload to Mega +# TODO: code for user filters +# TODO: add and Handle Exceptions +# TODO: code for direct link generation +# TODO: add hard-restart (restart all subprocesses) +# TODO: decide between confDefaults and optVals for AriaHelper, QbitTorrentHelper +# TODO: remove redundant 'apt-get clean' in Dockerfile import aria2p import asyncio -import copy import googleapiclient.discovery import googleapiclient.errors import googleapiclient.http @@ -20,6 +22,7 @@ import mega import os import psutil +import qbittorrentapi import random import re import requests @@ -36,6 +39,7 @@ import tornado.httpserver import tornado.ioloop import tornado.web +import torrentool.api import typing import warnings import youtube_dl @@ -75,12 +79,14 @@ def __init__(self): self.loggingHelper = LoggingHelper(self) self.mirrorHelper = MirrorHelper(self) self.statusHelper = StatusHelper(self) + self.subprocessHelper = SubprocessHelper(self) self.threadingHelper = ThreadingHelper(self) self.commandHelper = CommandHelper(self) self.conversationHelper = ConversationHelper(self) self.ariaHelper = AriaHelper(self) self.googleDriveHelper = GoogleDriveHelper(self) self.megaHelper = MegaHelper(self) + self.qbitTorrentHelper = QbitTorrentHelper(self) self.telegramHelper = TelegramHelper(self) self.youTubeHelper = YouTubeHelper(self) self.compressionHelper = CompressionHelper(self) @@ -88,7 +94,10 @@ def __init__(self): super().__init__(self) def initHelper(self) -> None: - self.envVars: typing.Dict[str, typing.Union[bool, str]] = {'currWorkDir': os.getcwd()} + self.envVars: typing.Dict[str, typing.Union[bool, str]] = \ + { + 'currWorkDir': os.getcwd() + } self.restartJsonFile = 'restart.json' self.restartMsgInfo: typing.Dict[str, int] = {} self.restartVars = (self.configHelper.jsonFileLoad(self.restartJsonFile) if os.path.exists(self.restartJsonFile) else {}) @@ -102,10 +111,12 @@ def initHelper(self) -> None: self.dispatcher = self.updater.dispatcher self.bot = self.updater.bot self.envVars['dlRootDirPath'] = os.path.join(self.envVars['currWorkDir'], self.configHelper.configVars[self.configHelper.optVars[2]]) + self.torrentFileMimeType = 'application/x-bittorrent' def initSubHelpers(self): self.loggingHelper.initHelper() self.getHelper.initHelper() + self.subprocessHelper.initHelper() self.threadingHelper.initHelper() self.configHelper.initHelper() self.mirrorHelper.initHelper() @@ -114,6 +125,7 @@ def initSubHelpers(self): self.ariaHelper.initHelper() self.googleDriveHelper.initHelper() self.megaHelper.initHelper() + self.qbitTorrentHelper.initHelper() self.telegramHelper.initHelper() self.youTubeHelper.initHelper() self.compressionHelper.initHelper() @@ -131,11 +143,23 @@ def addAllHandlers(self) -> None: self.dispatcher.add_handler(unknownHandler) def botRestart(self) -> None: - self.ariaHelper.api.remove_all(force=True) + self.ariaHelper.removeAllDownloads() + self.qbitTorrentHelper.removeAllDownloads() + self.ariaHelper.stopListener() + self.qbitTorrentHelper.unauthorizeApi() + self.megaHelper.unauthorizeApi() self.cleanDlRootDir() + self.listenerHelper.webhookServerStop() + self.statusHelper.updaterStop() self.logger.info('Restarting the Bot...') - restartJsonDict = {'restartMsgInfo': self.restartMsgInfo, 'ariaRpcSecret': self.ariaHelper.rpcSecret, - 'ariaDaemonPid': self.ariaHelper.daemonPid, 'botApiServerPid': self.telegramHelper.apiServerPid} + restartJsonDict = \ + { + 'restartMsgInfo': self.restartMsgInfo, + 'ariaRpcSecret': self.ariaHelper.rpcSecret, + 'ariaDaemonPid': self.ariaHelper.daemonPid, + 'botApiServerPid': self.telegramHelper.apiServerPid, + 'qbitDaemonPid': self.qbitTorrentHelper.daemonPid + } self.configHelper.jsonFileWrite(self.restartJsonFile, restartJsonDict) os.execl(sys.executable, sys.executable, '-m', 'tgmb') @@ -143,20 +167,25 @@ def botStart(self) -> None: self.cleanDlRootDir() self.loggingHelper.checkLogLevel() self.loggingHelper.delLogFiles() + self.ariaHelper.makeConf() + self.qbitTorrentHelper.makeConf() self.ariaHelper.daemonStart() + self.qbitTorrentHelper.daemonStart() self.telegramHelper.apiServerStart() self.ariaHelper.daemonCheck() + self.qbitTorrentHelper.daemonCheck() self.telegramHelper.apiServerCheck() - self.ariaHelper.dlTrackersList() - self.ariaHelper.globalOptsGet() - self.ariaHelper.globalOptsSet() + self.ariaHelper.getTrackersList() self.ariaHelper.startListener() + self.qbitTorrentHelper.authorizeApi() + self.qbitTorrentHelper.setTrackersList() self.megaHelper.addListener() self.googleDriveHelper.authorizeApi() self.megaHelper.authorizeApi() self.addAllHandlers() self.updaterStart() - self.listenerHelper.startWebhookServer() + self.listenerHelper.webhookServerStart() + self.statusHelper.updaterStart() self.logger.info("Bot Started !") def botIdle(self) -> None: @@ -166,11 +195,18 @@ def botIdle(self) -> None: self.updaterIdle() def botStop(self) -> None: + self.ariaHelper.removeAllDownloads() + self.qbitTorrentHelper.removeAllDownloads() + self.ariaHelper.stopListener() + self.qbitTorrentHelper.unauthorizeApi() self.megaHelper.unauthorizeApi() self.telegramHelper.apiServerStop() + self.qbitTorrentHelper.daemonStop() self.ariaHelper.daemonStop() + self.cleanDlRootDir() self.loggingHelper.delLogFiles() - self.listenerHelper.stopWebhookServer() + self.listenerHelper.webhookServerStop() + self.statusHelper.updaterStop() self.logger.info("Bot Stopped !") def ifUpdateRestartMsg(self) -> None: @@ -203,18 +239,116 @@ def initHelper(self) -> None: self.configJsonBakFile = self.configJsonFile + '.bak' self.dynamicJsonFile = 'dynamic.json' self.fileidJsonFile = 'fileid.json' - self.configFiles: [str] = [self.configJsonFile, self.configJsonBakFile] + self.configFiles: typing.List[str] = \ + [ + self.configJsonFile, + self.configJsonBakFile + ] self.configVars: typing.Dict = {} - self.reqVars: [str] = ['botToken', 'botOwnerId', 'telegramApiId', 'telegramApiHash', - 'googleDriveAuth', 'googleDriveUploadFolderIds'] - self.optVars: typing.List[str] = ['ariaGlobalOpts', 'authorizedChats', 'dlRootDir', 'logLevel', - 'megaAuth', 'statusUpdateInterval', 'trackersListUrl', 'ytdlFormat'] + self.reqVars: typing.List[str] = \ + [ + 'botToken', + 'botOwnerId', + 'telegramApiId', + 'telegramApiHash', + 'googleDriveAuth', + 'googleDriveUploadFolderIds' + ] + self.optVars: typing.List[str] = \ + [ + 'ariaConf', + 'authorizedChats', + 'dlRootDir', + 'logLevel', + 'megaAuth', + 'qbitTorrentConf', + 'statusUpdateInterval', + 'trackersListUrl', + 'ytdlFormat' + ] self.optVals: typing.List[typing.Union[str, typing.Dict]] = \ - [{'allow-overwrite': 'true', 'bt-max-peers': '0', 'follow-torrent': 'mem', - 'max-connection-per-server': '8', 'max-overall-upload-limit': '1K', - 'min-split-size': '10M', 'seed-time': '0.01', 'split': '10'}, - {}, 'dl', 'INFO', {}, '5', 'https://trackerslist.com/all_aria2.txt', 'best/bestvideo+bestaudio'] - self.emptyVals: typing.List[typing.Union[str, typing.Dict]] = ['', ' ', {}] + [ + { + 'allow-overwrite': 'true', + 'follow-torrent': 'false', + 'max-connection-per-server': '8', + 'min-split-size': '8M', + 'split': '8' + }, + {}, + 'dl', + 'INFO', + { + 'apiKey': '', + 'emailId': '', + 'passPhrase': '' + }, + { + 'BitTorrent': { + 'Session': { + 'AsyncIOThreadsCount': '8', + 'MultiConnectionsPerIp': 'true', + 'SlowTorrentsDownloadRate': '100', + 'SlowTorrentsInactivityTimer': '600' + } + }, + 'LegalNotice': { + '': { + 'Accepted': 'true' + } + }, + 'Preferences': { + 'Advanced': { + 'AnnounceToAllTrackers': 'true', + 'AnonymousMode': 'false', + 'IgnoreLimitsLAN': 'true', + 'RecheckOnCompletion': 'true', + 'LtTrackerExchange': 'true' + }, + 'Bittorrent': { + 'AddTrackers': 'false', + 'MaxConnecs': '-1', + 'MaxConnecsPerTorrent': '-1', + 'MaxUploads': '-1', + 'MaxUploadsPerTorrent': '-1', + 'DHT': 'true', + 'DHTPort': '6881', + 'PeX': 'true', + 'LSD': 'true', + 'sameDHTPortAsBT': 'true' + }, + 'Downloads': { + 'DiskWriteCacheSize': '32', + 'PreAllocation': 'true', + 'UseIncompleteExtension': 'true' + }, + 'General': { + 'PreventFromSuspendWhenDownloading': 'true' + }, + 'Queueing': { + 'IgnoreSlowTorrents': 'true', + 'MaxActiveDownloads': '100', + 'MaxActiveTorrents': '50', + 'MaxActiveUploads': '50', + 'QueueingEnabled': 'false' + }, + 'WebUI': { + 'Enabled': 'true', + 'Port': '8400', + 'LocalHostAuth': 'false' + } + } + }, + '5', + 'https://trackerslist.com/all.txt', + 'best/bestvideo+bestaudio' + ] + self.emptyVals: typing.List[typing.Union[str, typing.Dict]] = \ + [ + '', + ' ', + {} + ] self.isFixConfigJson: bool = False self.configVarsLoad() self.configVarsCheck() @@ -256,10 +390,18 @@ def configVarsLoad(self) -> None: if os.path.exists(self.dynamicJsonFile): self.botHelper.envVars['dynamicConfig'] = True self.logger.info('Using Dynamic Config...') - self.botHelper.envVars = {**self.botHelper.envVars, **self.jsonFileLoad(self.dynamicJsonFile)} + self.botHelper.envVars = \ + { + **self.botHelper.envVars, + **self.jsonFileLoad(self.dynamicJsonFile) + } self.configFileDl(self.fileidJsonFile) self.configFileCheck(self.fileidJsonFile) - self.botHelper.envVars = {**self.botHelper.envVars, **self.jsonFileLoad(self.fileidJsonFile)} + self.botHelper.envVars = \ + { + **self.botHelper.envVars, + **self.jsonFileLoad(self.fileidJsonFile) + } for configFile in self.configFiles: fileHashInDict = self.botHelper.envVars[self.botHelper.getHelper.fileHashKey(configFile)] if not (os.path.exists(configFile) and fileHashInDict == self.botHelper.getHelper.fileHash(configFile)): @@ -314,7 +456,11 @@ def jsonFileWrite(jsonFileName: str, jsonDict: dict) -> None: def updateAuthorizedChats(self, chatId: int, chatName: str, chatType: str, auth: bool = None, unauth: bool = None) -> None: if auth: - self.configVars[self.optVars[1]][str(chatId)] = {"chatType": chatType, "chatName": chatName} + self.configVars[self.optVars[1]][str(chatId)] = \ + { + "chatType": chatType, + "chatName": chatName + } if unauth: self.configVars[self.optVars[1]].pop(str(chatId)) self.updateConfigJson() @@ -351,8 +497,26 @@ def initHelper(self) -> None: super().initHelper() self.keySuffixId: str = 'Id' self.keySuffixHash: str = 'Hash' - self.sizeUnits: [str] = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] - self.progressUnits: typing.List[str] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'] + self.sizeUnits: [str] = \ + [ + 'B', + 'KB', + 'MB', + 'GB', + 'TB', + 'PB' + ] + self.progressUnits: typing.List[str] = \ + [ + '▏', + '▎', + '▍', + '▌', + '▋', + '▊', + '▉', + '█' + ] @staticmethod def chatDetails(update: telegram.Update) -> (int, str, str): @@ -395,6 +559,14 @@ def folderSize(folderPath: str) -> int: size += os.path.getsize(os.path.join(path, file)) return size + @staticmethod + def hashFromMagnet(magnetUrl: str) -> str: + return re.search(r'(?<=xt=urn:btih:)[a-zA-Z0-9]+', magnetUrl).group(0).lower() + + @staticmethod + def magnetFromTorrentFile(torrentFile: str) -> str: + return torrentool.api.Torrent.from_file(torrentFile).magnet_link + def progressBar(self, progress: float) -> str: progressRounded = round(progress) numFull = progressRounded // 8 @@ -470,51 +642,59 @@ def initHelper(self) -> None: self.uploadQueueSize: int = 3 self.uploadQueueActive: int = 0 self.uploadQueue: typing.List[str] = [] - self.statusCallBacks: typing.Dict[str, typing.Callable] \ - = {MirrorStatus.addMirror: self.onAddMirror, - MirrorStatus.cancelMirror: self.onCancelMirror, - MirrorStatus.completeMirror: self.onCompleteMirror, - MirrorStatus.downloadQueue: self.onDownloadQueue, - MirrorStatus.downloadStart: self.onDownloadStart, - MirrorStatus.downloadProgress: self.onDownloadProgress, - MirrorStatus.downloadComplete: self.onDownloadComplete, - MirrorStatus.downloadError: self.onDownloadError, - MirrorStatus.compressionQueue: self.onCompressionQueue, - MirrorStatus.compressionStart: self.onCompressionStart, - MirrorStatus.compressionProgress: self.onCompressionProgress, - MirrorStatus.compressionComplete: self.onCompressionComplete, - MirrorStatus.compressionError: self.onCompressionError, - MirrorStatus.decompressionQueue: self.onDecompressionQueue, - MirrorStatus.decompressionStart: self.onDecompressionStart, - MirrorStatus.decompressionProgress: self.onDecompressionProgress, - MirrorStatus.decompressionComplete: self.onDecompressionComplete, - MirrorStatus.decompressionError: self.onDecompressionError, - MirrorStatus.uploadQueue: self.onUploadQueue, - MirrorStatus.uploadStart: self.onUploadStart, - MirrorStatus.uploadProgress: self.onUploadProgress, - MirrorStatus.uploadComplete: self.onUploadComplete, - MirrorStatus.uploadError: self.onUploadError} - - def startWebhookServer(self, ready=None, forceEventLoop=False) -> None: + self.statusCallBacks: typing.Dict[str, typing.Callable] = \ + { + MirrorStatus.addMirror: self.onAddMirror, + MirrorStatus.cancelMirror: self.onCancelMirror, + MirrorStatus.completeMirror: self.onCompleteMirror, + MirrorStatus.downloadQueue: self.onDownloadQueue, + MirrorStatus.downloadStart: self.onDownloadStart, + MirrorStatus.downloadProgress: self.onDownloadProgress, + MirrorStatus.downloadComplete: self.onDownloadComplete, + MirrorStatus.downloadError: self.onDownloadError, + MirrorStatus.compressionQueue: self.onCompressionQueue, + MirrorStatus.compressionStart: self.onCompressionStart, + MirrorStatus.compressionProgress: self.onCompressionProgress, + MirrorStatus.compressionComplete: self.onCompressionComplete, + MirrorStatus.compressionError: self.onCompressionError, + MirrorStatus.decompressionQueue: self.onDecompressionQueue, + MirrorStatus.decompressionStart: self.onDecompressionStart, + MirrorStatus.decompressionProgress: self.onDecompressionProgress, + MirrorStatus.decompressionComplete: self.onDecompressionComplete, + MirrorStatus.decompressionError: self.onDecompressionError, + MirrorStatus.uploadQueue: self.onUploadQueue, + MirrorStatus.uploadStart: self.onUploadStart, + MirrorStatus.uploadProgress: self.onUploadProgress, + MirrorStatus.uploadComplete: self.onUploadComplete, + MirrorStatus.uploadError: self.onUploadError + } + + def webhookServerStart(self, ready=None, forceEventLoop=False) -> None: self.webhookServer = WebhookServer(self.botHelper) self.botHelper.threadingHelper.initThread(target=self.webhookServer.serveForever, name='ListenerHelper.webhookServer', forceEventLoop=forceEventLoop, ready=ready) - def stopWebhookServer(self) -> None: + def webhookServerStop(self) -> None: if self.webhookServer: self.webhookServer.shutdown() self.webhookServer = None def updateStatus(self, uid: str, mirrorStatus: str) -> None: - self.botHelper.mirrorHelper.mirrorInfos[uid].status = mirrorStatus - data = {'mirrorUid': uid, 'mirrorStatus': mirrorStatus} - headers = {'Content-Type': 'application/json'} - requests.post(url=self.webhookServer.webhookUrl, data=json.dumps(data), headers=headers) + self.botHelper.mirrorHelper.mirrorInfos[uid].updateStatus(mirrorStatus) + payloadData: typing.Dict[str, str] = \ + { + 'mirrorUid': uid, + 'mirrorStatus': mirrorStatus} + headers: typing.Dict[str, str] = \ + { + 'Content-Type': 'application/json' + } + requests.post(url=self.webhookServer.webhookUrl, data=json.dumps(payloadData), headers=headers) def updateStatusCallback(self, uid: str) -> None: mirrorInfo: MirrorInfo = self.botHelper.mirrorHelper.mirrorInfos[uid] - self.logger.info(f'{mirrorInfo.uid} : {mirrorInfo.status}') - self.statusCallBacks[mirrorInfo.status](mirrorInfo) + self.logger.info(f'{mirrorInfo.uid} : {mirrorInfo.currentStatus}') + self.statusCallBacks[mirrorInfo.currentStatus](mirrorInfo) def onAddMirror(self, mirrorInfo: 'MirrorInfo') -> None: self.downloadQueue.append(mirrorInfo.uid) @@ -522,9 +702,103 @@ def onAddMirror(self, mirrorInfo: 'MirrorInfo') -> None: # TODO: improve method and maybe not use onCancelMirror callback in operationErrors and improve onOperationErrors def onCancelMirror(self, mirrorInfo: 'MirrorInfo') -> None: - # TODO: implement cancel callbacks for various download and upload types - shutil.rmtree(mirrorInfo.path) + if mirrorInfo.previousStatus in [ + MirrorStatus.downloadQueue, + MirrorStatus.uploadQueue, + MirrorStatus.compressionQueue, + MirrorStatus.decompressionQueue + ]: + if mirrorInfo.previousStatus == MirrorStatus.downloadQueue: + self.removeDownloadQueue(mirrorInfo.uid) + if mirrorInfo.previousStatus == MirrorStatus.uploadQueue: + self.removeUploadQueue(mirrorInfo.uid) + if mirrorInfo.previousStatus == MirrorStatus.compressionQueue: + self.removeCompressionQueue(mirrorInfo.uid) + if mirrorInfo.previousStatus == MirrorStatus.decompressionQueue: + self.removeDecompressionQueue(mirrorInfo.uid) + if mirrorInfo.previousStatus in [ + MirrorStatus.downloadStart, + MirrorStatus.uploadStart, + MirrorStatus.compressionStart, + MirrorStatus.decompressionStart + ]: + while self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].previousStatus in [ + MirrorStatus.downloadStart, + MirrorStatus.uploadStart, + MirrorStatus.compressionStart, + MirrorStatus.decompressionStart + ]: + time.sleep(1.0) + self.updateStatus(mirrorInfo.uid, MirrorStatus.cancelMirror) + return + if mirrorInfo.previousStatus in [ + MirrorStatus.downloadProgress + ]: + if mirrorInfo.isAriaDownload: + self.botHelper.ariaHelper.cancelDownload(mirrorInfo) + if mirrorInfo.isGoogleDriveDownload: + self.botHelper.googleDriveHelper.cancelDownload(mirrorInfo) + if mirrorInfo.isMegaDownload: + self.botHelper.megaHelper.cancelDownload(mirrorInfo) + if mirrorInfo.isQbitTorrentDownload: + self.botHelper.qbitTorrentHelper.cancelDownload(mirrorInfo) + if mirrorInfo.isTelegramDownload: + self.botHelper.telegramHelper.cancelDownload(mirrorInfo) + if mirrorInfo.isYouTubeDownload: + self.botHelper.youTubeHelper.cancelDownload(mirrorInfo) + self.downloadQueue.remove(mirrorInfo.uid) + self.downloadQueueActive -= 1 + self.checkDownloadQueue() + if mirrorInfo.previousStatus in [ + MirrorStatus.uploadProgress + ]: + if mirrorInfo.isGoogleDriveUpload: + self.botHelper.googleDriveHelper.cancelUpload(mirrorInfo) + if mirrorInfo.isMegaUpload: + self.botHelper.megaHelper.cancelUpload(mirrorInfo) + if mirrorInfo.isTelegramUpload: + self.botHelper.telegramHelper.cancelUpload(mirrorInfo) + self.uploadQueue.remove(mirrorInfo.uid) + self.uploadQueueActive -= 1 + self.checkUploadQueue() + if mirrorInfo.previousStatus in [ + MirrorStatus.compressionProgress, + MirrorStatus.decompressionProgress + ]: + while self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].previousStatus in [ + MirrorStatus.compressionProgress, + MirrorStatus.decompressionProgress + ]: + time.sleep(1.0) + self.updateStatus(mirrorInfo.uid, MirrorStatus.cancelMirror) + return + if mirrorInfo.previousStatus in [ + MirrorStatus.downloadComplete, + MirrorStatus.uploadComplete, + MirrorStatus.compressionComplete, + MirrorStatus.decompressionComplete + ]: + while self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].previousStatus in [ + MirrorStatus.downloadComplete, + MirrorStatus.uploadComplete, + MirrorStatus.compressionComplete, + MirrorStatus.decompressionComplete + ]: + time.sleep(1.0) + self.updateStatus(mirrorInfo.uid, MirrorStatus.cancelMirror) + return + if mirrorInfo.previousStatus in [ + MirrorStatus.downloadError, + MirrorStatus.uploadError, + MirrorStatus.compressionError, + MirrorStatus.decompressionError + ]: + pass + if os.path.exists(mirrorInfo.path): + shutil.rmtree(mirrorInfo.path) self.botHelper.mirrorHelper.mirrorInfos.pop(mirrorInfo.uid) + self.botHelper.bot.sendMessage(text=f'Cancelled: [{mirrorInfo.uid}]', parse_mode='HTML', + chat_id=mirrorInfo.chatId, reply_to_message_id=mirrorInfo.msgId) def onCompleteMirror(self, mirrorInfo: 'MirrorInfo') -> None: shutil.rmtree(mirrorInfo.path) @@ -543,6 +817,10 @@ def checkDownloadQueue(self) -> None: self.downloadQueueActive += 1 self.checkDownloadQueue() + def removeDownloadQueue(self, uid: str) -> None: + self.downloadQueue.remove(uid) + self.checkDownloadQueue() + def onDownloadStart(self, mirrorInfo: 'MirrorInfo') -> None: os.mkdir(mirrorInfo.path) if mirrorInfo.isAriaDownload: @@ -554,6 +832,9 @@ def onDownloadStart(self, mirrorInfo: 'MirrorInfo') -> None: if mirrorInfo.isMegaDownload: self.botHelper.threadingHelper.initThread(target=self.botHelper.megaHelper.addDownload, name=f'{mirrorInfo.uid}-MegaDownload', mirrorInfo=mirrorInfo) + if mirrorInfo.isQbitTorrentDownload: + self.botHelper.threadingHelper.initThread(target=self.botHelper.qbitTorrentHelper.addDownload, + name=f'{mirrorInfo.uid}-QbitTorrentDownload', mirrorInfo=mirrorInfo) if mirrorInfo.isTelegramDownload: self.botHelper.threadingHelper.initThread(target=self.botHelper.telegramHelper.addDownload, name=f'{mirrorInfo.uid}-TelegramDownload', mirrorInfo=mirrorInfo) @@ -593,6 +874,10 @@ def checkCompressionQueue(self) -> None: self.compressionQueueActive += 1 self.checkCompressionQueue() + def removeCompressionQueue(self, uid: str) -> None: + self.compressionQueue.remove(uid) + self.checkCompressionQueue() + def onCompressionStart(self, mirrorInfo: 'MirrorInfo') -> None: self.botHelper.threadingHelper.initThread(target=self.botHelper.compressionHelper.addCompression, name=f'{mirrorInfo.uid}-Compression', mirrorInfo=mirrorInfo) @@ -629,6 +914,10 @@ def checkDecompressionQueue(self) -> None: self.decompressionQueueActive += 1 self.checkDecompressionQueue() + def removeDecompressionQueue(self, uid: str) -> None: + self.decompressionQueue.remove(uid) + self.checkDecompressionQueue() + def onDecompressionStart(self, mirrorInfo: 'MirrorInfo') -> None: self.botHelper.threadingHelper.initThread(target=self.botHelper.decompressionHelper.addDecompression, name=f'{mirrorInfo.uid}-Decompression', mirrorInfo=mirrorInfo) @@ -660,6 +949,10 @@ def checkUploadQueue(self) -> None: self.uploadQueueActive += 1 self.checkUploadQueue() + def removeUploadQueue(self, uid: str) -> None: + self.uploadQueue.remove(uid) + self.checkUploadQueue() + def onUploadStart(self, mirrorInfo: 'MirrorInfo') -> None: if mirrorInfo.isGoogleDriveUpload: self.botHelper.threadingHelper.initThread(target=self.botHelper.googleDriveHelper.addUpload, @@ -693,12 +986,14 @@ def resetMirrorProgress(self, uid: str) -> None: class LoggingHelper(BaseHelper): LogFormats: typing.Dict[str, str] = \ - {'DEFAULT': '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | ' - '{name}:{function}:{line} - {message}', - 'INFO': '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <6} | {message}', - 'DEBUG': '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | ' - '{name}:{extra[classname]}:{function}():{line} | ' - '{message}'} + { + 'DEFAULT': '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | ' + '{name}:{function}:{line} - {message}', + 'INFO': '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <6} | {message}', + 'DEBUG': '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | ' + '{name}:{extra[classname]}:{function}():{line} | ' + '{message}' + } def __init__(self, botHelper: BotHelper): super().__init__(botHelper) @@ -706,12 +1001,22 @@ def __init__(self, botHelper: BotHelper): def initHelper(self) -> None: self.logDebugFile = 'log.debug' self.isChangeLogLevel: bool = False - self.logFiles: typing.List[str] = ['bot.log', 'botApiServer.log', 'ariaDaemon.log', - 'tqueue.binlog', 'webhooks_db.binlog'] + self.logFiles: typing.List[str] = \ + [ + 'bot.log', + 'botApiServer.log', + 'ariaDaemon.log', + 'tqueue.binlog', + 'webhooks_db.binlog' + ] if os.path.exists(self.logFiles[0]): os.remove(self.logFiles[0]) self.logLevel = (list(self.LogFormats.keys())[2] if os.path.exists(self.logDebugFile) else list(self.LogFormats.keys())[1]) - self.logDisableModules: typing.List[str] = ['apscheduler', 'telegram.vendor.ptb_urllib3.urllib3.connectionpool'] + self.logDisableModules: typing.List[str] = \ + [ + 'apscheduler', + 'telegram.vendor.ptb_urllib3.urllib3.connectionpool' + ] self.logger = loguru.logger self.logger.remove() self.logger.add(sys.stderr, level=self.logLevel, format=self.LogFormats[self.logLevel]) @@ -747,6 +1052,94 @@ def delLogFiles(self) -> None: self.logger.debug(f"Deleted: '{logFile}'") +class MirrorHelper(BaseHelper): + def __init__(self, botHelper: BotHelper): + super().__init__(botHelper) + + def initHelper(self) -> None: + super().initHelper() + self.mirrorInfos: typing.Dict[str, MirrorInfo] = {} + self.supportedArchiveFormats: typing.Dict[str, str] = \ + { + 'zip': '.zip', + 'tar': '.tar', + 'bztar': '.tar.bz2', + 'gztar': '.tar.gz', + 'xztar': '.tar.xz' + } + + def addMirror(self, mirrorInfo: 'MirrorInfo') -> None: + self.logger.debug(vars(mirrorInfo)) + self.mirrorInfos[mirrorInfo.uid] = mirrorInfo + self.mirrorInfos[mirrorInfo.uid].timeStart = time.time() + self.botHelper.listenerHelper.updateStatus(mirrorInfo.uid, MirrorStatus.addMirror) + self.botHelper.threadingHelper.initThread(target=self.botHelper.statusHelper.addStatus, name=f'{mirrorInfo.uid}-addStatus', + chatId=mirrorInfo.chatId, msgId=mirrorInfo.msgId) + + def cancelMirror(self, msg: telegram.Message) -> None: + if self.mirrorInfos == {}: + self.logger.info('No Active Downloads !') + return + uids: typing.List[str] = [] + try: + msgTxt = msg.text.split(' ')[1].strip() + if msgTxt == 'all': + uids = list(self.mirrorInfos.keys()) + if msgTxt in self.mirrorInfos.keys(): + uids.append(msgTxt) + except IndexError: + replyTo = msg.reply_to_message + if replyTo: + msgId = replyTo.message_id + for mirrorInfo in self.mirrorInfos.values(): + if msgId == mirrorInfo.msgId: + uids.append(mirrorInfo.uid) + break + if len(uids) == 0: + self.logger.info('No Valid Mirror Found !') + return + for uid in uids: + self.botHelper.listenerHelper.updateStatus(uid, MirrorStatus.cancelMirror) + + def genMirrorInfo(self, msg: telegram.Message) -> (bool, 'MirrorInfo'): + mirrorInfo = MirrorInfo(msg, self.botHelper) + isValidDl: bool = True + try: + mirrorInfo.downloadUrl = msg.text.split(' ')[1].strip() + mirrorInfo.tag = msg.from_user.username + if re.findall(UrlRegex.googleDrive, mirrorInfo.downloadUrl): + mirrorInfo.isGoogleDriveDownload = True + elif re.findall(UrlRegex.mega, mirrorInfo.downloadUrl): + mirrorInfo.isMegaDownload = True + elif re.findall(UrlRegex.youTube, mirrorInfo.downloadUrl): + mirrorInfo.isYouTubeDownload = True + elif re.findall(UrlRegex.bittorrentMagnet, mirrorInfo.downloadUrl): + mirrorInfo.isQbitTorrentDownload = True + elif re.findall(UrlRegex.generalUrl, mirrorInfo.downloadUrl): + mirrorInfo.isAriaDownload = True + else: + isValidDl = False + except IndexError: + replyTo = msg.reply_to_message + if replyTo: + mirrorInfo.tag = replyTo.from_user.username + for media in [replyTo.document, replyTo.audio, replyTo.video]: + if media: + if media.mime_type == self.botHelper.torrentFileMimeType: + mirrorInfo.isQbitTorrentDownload = True + torrentFile = media.get_file().file_path + mirrorInfo.downloadUrl = self.botHelper.getHelper.magnetFromTorrentFile(torrentFile) + os.remove(torrentFile) + else: + mirrorInfo.isTelegramDownload = True + break + else: + isValidDl = False + if not isValidDl: + self.logger.info('No Valid Link Provided !') + return isValidDl, mirrorInfo + + class StatusHelper(BaseHelper): def __init__(self, botHelper: BotHelper): super().__init__(botHelper) @@ -754,9 +1147,9 @@ def __init__(self, botHelper: BotHelper): def initHelper(self) -> None: super().initHelper() self.updaterLock = threading.Lock() - self.isInitThread: bool = False - self.isUpdateStatus: bool = False - self.statusUpdateInterval: int = int(self.botHelper.configHelper.configVars[self.botHelper.configHelper.optVars[5]]) + self.isContinue: bool = False + self.isUpdate: bool = False + self.statusUpdateInterval: float = float(self.botHelper.configHelper.configVars[self.botHelper.configHelper.optVars[6]]) self.msgId: int = 0 self.chatId: int = 0 self.lastStatusMsgId: int = 0 @@ -764,35 +1157,29 @@ def initHelper(self) -> None: def addStatus(self, chatId: int, msgId: int) -> None: with self.updaterLock: - if self.botHelper.mirrorHelper.mirrorInfos != {}: - self.isUpdateStatus = True - else: - self.isUpdateStatus = False - if self.lastStatusMsgId == 0: - self.isInitThread = True if self.lastStatusMsgId != 0: self.botHelper.bot.deleteMessage(chat_id=self.chatId, message_id=self.lastStatusMsgId) self.chatId = chatId self.msgId = msgId self.lastStatusMsgId = self.botHelper.bot.sendMessage(text='...', parse_mode='HTML', chat_id=self.chatId, reply_to_message_id=self.msgId).message_id - if self.isInitThread: - self.isInitThread = False - self.botHelper.threadingHelper.initThread(target=self.updateStatusMsg, name='statusUpdaterStart') + self.isUpdate = True def getStatusMsgTxt(self) -> str: statusMsgTxt = '' for uid in self.botHelper.mirrorHelper.mirrorInfos.keys(): mirrorInfo: MirrorInfo = self.botHelper.mirrorHelper.mirrorInfos[uid] - statusMsgTxt += f'{mirrorInfo.uid} | {mirrorInfo.status}\n' - if mirrorInfo.status in [MirrorStatus.downloadProgress, MirrorStatus.uploadProgress]: - if mirrorInfo.status == MirrorStatus.downloadProgress and mirrorInfo.isAriaDownload: - self.botHelper.ariaHelper.updateProgress(mirrorInfo.uid) + statusMsgTxt += f'{mirrorInfo.uid} | {mirrorInfo.currentStatus}\n' + if mirrorInfo.currentStatus in [MirrorStatus.downloadProgress, MirrorStatus.uploadProgress]: + if mirrorInfo.currentStatus == MirrorStatus.downloadProgress: + if mirrorInfo.isAriaDownload: + self.botHelper.ariaHelper.updateProgress(mirrorInfo.uid) + if mirrorInfo.isQbitTorrentDownload: + self.botHelper.qbitTorrentHelper.updateProgress(mirrorInfo.uid) statusMsgTxt += f'S: {self.botHelper.getHelper.readableSize(mirrorInfo.sizeCurrent)} | ' \ f'{self.botHelper.getHelper.readableSize(mirrorInfo.sizeTotal)} | ' \ f'{self.botHelper.getHelper.readableSize(mirrorInfo.sizeTotal - mirrorInfo.sizeCurrent)}\n' \ - f'P: {self.botHelper.getHelper.progressBar(mirrorInfo.progressPercent)} | ' \ - f'{mirrorInfo.progressPercent}% | ' \ + f'P: {self.botHelper.getHelper.progressBar(mirrorInfo.progressPercent)} | {mirrorInfo.progressPercent}% | ' \ f'{self.botHelper.getHelper.readableSize(mirrorInfo.speedCurrent)}/s\n' \ f'T: {self.botHelper.getHelper.readableTime(int(mirrorInfo.timeCurrent - mirrorInfo.timeStart))} | ' \ f'{self.botHelper.getHelper.readableTime(int(mirrorInfo.timeEnd - mirrorInfo.timeCurrent))}\n' @@ -800,35 +1187,54 @@ def getStatusMsgTxt(self) -> str: return statusMsgTxt def updateStatusMsg(self) -> None: - with self.updaterLock: - if self.isUpdateStatus: - if self.botHelper.mirrorHelper.mirrorInfos: - statusMsgTxt = self.getStatusMsgTxt() - if statusMsgTxt != self.lastStatusMsgTxt: - self.botHelper.bot.editMessageText(text=statusMsgTxt, parse_mode='HTML', chat_id=self.chatId, - message_id=self.lastStatusMsgId) - self.lastStatusMsgTxt = statusMsgTxt - time.sleep(self.statusUpdateInterval - 1) - time.sleep(1) - self.botHelper.threadingHelper.initThread(target=self.updateStatusMsg, name='statusUpdaterContinue') - return - if not self.botHelper.mirrorHelper.mirrorInfos: - self.isUpdateStatus = False - self.botHelper.threadingHelper.initThread(target=self.updateStatusMsg, name='statusUpdaterEnd') - return - if not self.isUpdateStatus: - self.botHelper.bot.editMessageText(text='No Active Downloads !', parse_mode='HTML', - chat_id=self.chatId, message_id=self.lastStatusMsgId) - self.resetAllDat() + while self.isContinue: + with self.updaterLock: + if self.isUpdate: + if self.botHelper.mirrorHelper.mirrorInfos: + statusMsgTxt = self.getStatusMsgTxt() + if statusMsgTxt != self.lastStatusMsgTxt: + self.botHelper.bot.editMessageText(text=statusMsgTxt, parse_mode='HTML', + chat_id=self.chatId, message_id=self.lastStatusMsgId) + self.lastStatusMsgTxt = statusMsgTxt + time.sleep(self.statusUpdateInterval - 1.0) + if not self.botHelper.mirrorHelper.mirrorInfos: + self.botHelper.bot.editMessageText(text='No Active Downloads !', parse_mode='HTML', + chat_id=self.chatId, message_id=self.lastStatusMsgId) + self.resetAllDat() + time.sleep(1.0) def resetAllDat(self) -> None: - self.isInitThread = False - self.isUpdateStatus = False + self.isUpdate = False self.msgId = 0 self.chatId = 0 self.lastStatusMsgId = 0 self.lastStatusMsgTxt = '' + def updaterStart(self) -> None: + self.isContinue = True + self.botHelper.threadingHelper.initThread(target=self.updateStatusMsg, name='statusUpdater') + + def updaterStop(self) -> None: + self.isContinue = False + self.botHelper.threadingHelper.runningThreads[0].join() + + +class SubprocessHelper(BaseHelper): + def __init__(self, botHelper: BotHelper): + super().__init__(botHelper) + + def initHelper(self) -> None: + super().initHelper() + + @staticmethod + def procInit(procStartCmd: typing.List[str]) -> subprocess.Popen: + return subprocess.Popen(procStartCmd, start_new_session=True, + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + @staticmethod + def procTerm(procPid: int) -> None: + return os.kill(procPid, signal.SIGTERM) + class ThreadingHelper(BaseHelper): def __init__(self, botHelper: BotHelper): @@ -902,10 +1308,20 @@ def initHelper(self) -> None: self.syncCmdHandler = telegram.ext.CommandHandler(command=self.SyncCmd.command, callback=self.syncCallBack, run_async=True) self.cmdHandlers: typing.List[telegram.ext.CommandHandler] = \ - [self.startCmdHandler, self.helpCmdHandler, self.statsCmdHandler, self.pingCmdHandler, - self.restartCmdHandler, self.statusCmdHandler, self.cancelCmdHandler, self.listCmdHandler, - self.deleteCmdHandler, self.authorizeCmdHandler, self.unauthorizeCmdHandler, - self.syncCmdHandler] + [ + self.startCmdHandler, + self.helpCmdHandler, + self.statsCmdHandler, + self.pingCmdHandler, + self.restartCmdHandler, + self.statusCmdHandler, + self.cancelCmdHandler, + self.listCmdHandler, + self.deleteCmdHandler, + self.authorizeCmdHandler, + self.unauthorizeCmdHandler, + self.syncCmdHandler + ] def startCallBack(self, update: telegram.Update, _: telegram.ext.CallbackContext): self.botHelper.bot.sendMessage(text=f'A Telegram Bot Written in Python to Mirror Files on the Internet to Google Drive.\n' @@ -1017,7 +1433,11 @@ def initHelper(self) -> None: super().initHelper() self.initSubHelpers() self.convHandlers: typing.List[telegram.ext.ConversationHandler] = \ - [self.configConvHelper.handler, self.logConvHelper.handler, self.mirrorConvHelper.handler] + [ + self.configConvHelper.handler, + self.logConvHelper.handler, + self.mirrorConvHelper.handler + ] def initSubHelpers(self): self.configConvHelper.initHelper() @@ -1044,20 +1464,30 @@ def initHelper(self) -> None: states={ # ZEROTH # Choose Environment Variable - self.FIRST: [telegram.ext.CallbackQueryHandler(self.stageOne)], + self.FIRST: [ + telegram.ext.CallbackQueryHandler(self.stageOne) + ], # Show Existing Value - self.SECOND: [telegram.ext.CallbackQueryHandler(self.stageTwo)], + self.SECOND: [ + telegram.ext.CallbackQueryHandler(self.stageTwo) + ], # Capture New Value for Environment Variable self.THIRD: [ telegram.ext.CallbackQueryHandler(self.stageThree), telegram.ext.MessageHandler(telegram.ext.Filters.text, self.newVal) ], # Verify New Value - self.FOURTH: [telegram.ext.CallbackQueryHandler(self.stageFour)], + self.FOURTH: [ + telegram.ext.CallbackQueryHandler(self.stageFour) + ], # Show All Changes and Proceed - self.FIFTH: [telegram.ext.CallbackQueryHandler(self.stageFive)], + self.FIFTH: [ + telegram.ext.CallbackQueryHandler(self.stageFive) + ], # Save or Discard Changes - self.SIXTH: [telegram.ext.CallbackQueryHandler(self.stageSix)] + self.SIXTH: [ + telegram.ext.CallbackQueryHandler(self.stageSix) + ] # Exit or Start Over }, conversation_timeout=120, run_async=True) @@ -1118,9 +1548,14 @@ def stageSix(self, update: telegram.Update, _: telegram.ext.CallbackContext) -> def loadConfigDict(self): self.resetAllDat() self.configVarsEditable = self.botHelper.configHelper.jsonFileLoad(self.botHelper.configHelper.configJsonFile) - for key in [self.botHelper.configHelper.reqVars[4], self.botHelper.configHelper.reqVars[5], - self.botHelper.configHelper.optVars[0], self.botHelper.configHelper.optVars[1], - self.botHelper.configHelper.optVars[4]]: + for key in [ + self.botHelper.configHelper.reqVars[4], + self.botHelper.configHelper.reqVars[5], + self.botHelper.configHelper.optVars[0], + self.botHelper.configHelper.optVars[1], + self.botHelper.configHelper.optVars[4], + self.botHelper.configHelper.optVars[5] + ]: if key in list(self.configVarsEditable.keys()): self.configVarsEditable.pop(key) @@ -1210,7 +1645,11 @@ def initHelper(self) -> None: # TODO: filter - restrict to user who sent LogCommand self.cmdHandler = telegram.ext.CommandHandler(self.botHelper.commandHelper.LogCmd.command, self.stageZero) self.handler = telegram.ext.ConversationHandler(entry_points=[self.cmdHandler], fallbacks=[self.cmdHandler], - states={self.FIRST: [telegram.ext.CallbackQueryHandler(self.stageOne)]}, + states={ + self.FIRST: [ + telegram.ext.CallbackQueryHandler(self.stageOne) + ] + }, conversation_timeout=120, run_async=True) def stageZero(self, update: telegram.Update, _: telegram.ext.CallbackContext) -> int: @@ -1258,15 +1697,25 @@ def initHelper(self) -> None: states={ # ZEROTH # Choose to Modify or Use Default Values - self.FIRST: [telegram.ext.CallbackQueryHandler(self.stageOne)], + self.FIRST: [ + telegram.ext.CallbackQueryHandler(self.stageOne) + ], # Choose Upload Location - self.SECOND: [telegram.ext.CallbackQueryHandler(self.stageTwo)], + self.SECOND: [ + telegram.ext.CallbackQueryHandler(self.stageTwo) + ], # Choose googleDriveUploadFolder - self.THIRD: [telegram.ext.CallbackQueryHandler(self.stageThree)], + self.THIRD: [ + telegram.ext.CallbackQueryHandler(self.stageThree) + ], # Choose Compress / Decompress - self.FOURTH: [telegram.ext.CallbackQueryHandler(self.stageFour)], + self.FOURTH: [ + telegram.ext.CallbackQueryHandler(self.stageFour) + ], # Confirm and Proceed / Cancel - self.FIFTH: [telegram.ext.CallbackQueryHandler(self.stageFive)] + self.FIFTH: [ + telegram.ext.CallbackQueryHandler(self.stageFive) + ] }, conversation_timeout=120, run_async=True) @@ -1356,6 +1805,8 @@ def getMirrorInfoStr(self): mirrorInfoStr += f'[isGoogleDriveDownload | True]\n' elif self.mirrorInfo.isMegaDownload: mirrorInfoStr += f'[isMegaDownload | True]\n' + elif self.mirrorInfo.isQbitTorrentDownload: + mirrorInfoStr += f'[isQbitTorrentDownload | True]\n' elif self.mirrorInfo.isTelegramDownload: mirrorInfoStr += f'[isTelegramDownload | True]\n' elif self.mirrorInfo.isYouTubeDownload: @@ -1375,110 +1826,42 @@ def getMirrorInfoStr(self): return mirrorInfoStr -class MirrorHelper(BaseHelper): - def __init__(self, botHelper: BotHelper): - super().__init__(botHelper) - - def initHelper(self) -> None: - super().initHelper() - self.mirrorInfos: typing.Dict[str, MirrorInfo] = {} - self.supportedArchiveFormats: typing.Dict[str, str] = {'zip': '.zip', 'tar': '.tar', 'bztar': '.tar.bz2', - 'gztar': '.tar.gz', 'xztar': '.tar.xz'} - - def addMirror(self, mirrorInfo: 'MirrorInfo') -> None: - self.logger.debug(vars(mirrorInfo)) - self.mirrorInfos[mirrorInfo.uid] = mirrorInfo - self.mirrorInfos[mirrorInfo.uid].timeStart = time.time() - self.botHelper.listenerHelper.updateStatus(mirrorInfo.uid, MirrorStatus.addMirror) - self.botHelper.threadingHelper.initThread(target=self.botHelper.statusHelper.addStatus, name=f'{mirrorInfo.uid}-addStatus', - chatId=mirrorInfo.chatId, msgId=mirrorInfo.msgId) - - def cancelMirror(self, msg: telegram.Message) -> None: - if self.mirrorInfos == {}: - self.logger.info('No Active Downloads !') - return - uids: typing.List[str] = [] - try: - msgTxt = msg.text.split(' ')[1].strip() - if msgTxt == 'all': - uids = list(self.mirrorInfos.keys()) - if msgTxt in self.mirrorInfos.keys(): - uids.append(msgTxt) - except IndexError: - replyTo = msg.reply_to_message - if replyTo: - msgId = replyTo.message_id - for mirrorInfo in self.mirrorInfos.values(): - if msgId == mirrorInfo.msgId: - uids.append(mirrorInfo.uid) - break - if len(uids) == 0: - self.logger.info('No Valid Mirror Found !') - return - for uid in uids: - self.botHelper.listenerHelper.updateStatus(uid, MirrorStatus.cancelMirror) - - def genMirrorInfo(self, msg: telegram.Message) -> (bool, 'MirrorInfo'): - mirrorInfo = MirrorInfo(msg, self.botHelper) - isValidDl: bool = True - try: - mirrorInfo.downloadUrl = msg.text.split(' ')[1].strip() - mirrorInfo.tag = msg.from_user.username - if re.findall(UrlRegex.googleDrive, mirrorInfo.downloadUrl): - mirrorInfo.isGoogleDriveDownload = True - elif re.findall(UrlRegex.mega, mirrorInfo.downloadUrl): - mirrorInfo.isMegaDownload = True - elif re.findall(UrlRegex.youTube, mirrorInfo.downloadUrl): - mirrorInfo.isYouTubeDownload = True - elif re.findall(UrlRegex.bittorrentMagnet, mirrorInfo.downloadUrl): - mirrorInfo.isAriaDownload = True - elif re.findall(UrlRegex.generalUrl, mirrorInfo.downloadUrl): - mirrorInfo.isAriaDownload = True - else: - isValidDl = False - except IndexError: - replyTo = msg.reply_to_message - if replyTo: - mirrorInfo.tag = replyTo.from_user.username - for media in [replyTo.document, replyTo.audio, replyTo.video]: - if media: - if media.mime_type == 'application/x-bittorrent': - mirrorInfo.isAriaDownload = True - mirrorInfo.downloadUrl = media.get_file().file_path - else: - mirrorInfo.isTelegramDownload = True - break - else: - isValidDl = False - if not isValidDl: - self.logger.info('No Valid Link Provided !') - return isValidDl, mirrorInfo - - class AriaHelper(BaseHelper): def __init__(self, botHelper: BotHelper): super().__init__(botHelper) def initHelper(self) -> None: super().initHelper() + self.rpcListenPort = 7200 self.rpcSecret = (self.botHelper.restartVars['ariaRpcSecret'] if self.botHelper.restartVars else self.botHelper.getHelper.randomString(8)) - self.api = aria2p.API(aria2p.Client(host="http://localhost", port=6800, secret=self.rpcSecret)) + self.api = aria2p.API(aria2p.Client(host="http://localhost", port=self.rpcListenPort, secret=self.rpcSecret)) + self.confFile = 'aria.conf' + self.confFileDir = 'aria/config' + self.confDefaults: typing.Dict[str, str] = \ + { + 'enable-rpc': 'true', + 'rpc-listen-port': self.rpcListenPort, + 'rpc-secret': self.rpcSecret, + 'rpc-max-request-size': '32M', + 'disable-ipv6': 'true', + 'log': self.botHelper.loggingHelper.logFiles[2] + } self.daemonPid: int = 0 - self.daemonStartCmd: typing.List[str] = ['aria2c', '--quiet', '--enable-rpc', f'--rpc-secret={self.rpcSecret}', - '--rpc-max-request-size=32M', f'--log={self.botHelper.loggingHelper.logFiles[2]}'] + self.daemonStartCmd: typing.List[str] = \ + [ + 'aria2c', + f'--conf-path={os.getcwd()}/{self.confFileDir}/{self.confFile}' + ] self.globalOpts: aria2p.Options self.trackersListFile = 'trackers.list' self.gids: typing.Dict[str, str] = {} def addDownload(self, mirrorInfo: 'MirrorInfo') -> None: - if re.findall(UrlRegex.bittorrentMagnet, mirrorInfo.downloadUrl): - self.gids[mirrorInfo.uid] = self.api.add_magnet(mirrorInfo.downloadUrl, options={'dir': mirrorInfo.path}).gid - if re.findall(UrlRegex.generalUrl, mirrorInfo.downloadUrl): - self.gids[mirrorInfo.uid] = self.api.add_uris([mirrorInfo.downloadUrl], options={'dir': mirrorInfo.path}).gid + self.gids[mirrorInfo.uid] = self.api.add_uris([mirrorInfo.downloadUrl], options={'dir': mirrorInfo.path}).gid - def cancelDownload(self, uid: str) -> None: - self.getDlObj(self.gids[uid]).remove(force=True, files=True) - self.gids.pop(uid) + def cancelDownload(self, mirrorInfo: 'MirrorInfo') -> None: + self.getDlObj(self.gids[mirrorInfo.uid]).remove(force=True) + self.gids.pop(mirrorInfo.uid) def getUid(self, gid: str) -> str: for uid in self.gids.keys(): @@ -1493,32 +1876,28 @@ def daemonStart(self) -> None: self.daemonPid = self.botHelper.restartVars['ariaDaemonPid'] self.logger.info(f'ariaDaemon Already Running (pid {self.daemonPid}) !') if not self.daemonPid: - self.daemonPid = subprocess.Popen(self.daemonStartCmd).pid - self.logger.info(f"ariaDaemon started (pid {self.daemonPid}) !") + self.daemonPid = self.botHelper.subprocessHelper.procInit(self.daemonStartCmd).pid + self.logger.info(f'ariaDaemon Started (pid {self.daemonPid}) !') # TODO: implement this method - def daemonCheck(self): + def daemonCheck(self) -> None: pass def daemonStop(self) -> None: - os.kill(self.daemonPid, signal.SIGTERM) - self.logger.info(f"ariaDaemon terminated (pid {self.daemonPid})") + self.botHelper.subprocessHelper.procTerm(self.daemonPid) + self.logger.info(f'ariaDaemon Terminated (pid {self.daemonPid}) !') - def globalOptsGet(self): + def globalOptsGet(self) -> None: self.globalOpts = self.api.get_global_options() - def globalOptsSet(self): - userOpts = copy.deepcopy(self.botHelper.configHelper.configVars[self.botHelper.configHelper.optVars[0]]) - userOpts['bt-tracker'] = open(self.trackersListFile, 'rt').read() - for optKey in list(userOpts.keys()): - optSetResponse = self.globalOpts.set(optKey, userOpts[optKey]) - self.logger.debug(f"(ariaGlobalOpts) ({optSetResponse}) ['{optKey}' : '{userOpts[optKey]}']") + def globalOptsSet(self, optKey: str, optVal: str) -> None: + self.logger.debug(f"(ariaGlobalOpts) ({self.globalOpts.set(optKey, optVal)}) ['{optKey}' : '{optVal}']") - def dlTrackersList(self): + def getTrackersList(self) -> None: if os.path.exists(self.trackersListFile): os.remove(self.trackersListFile) self.logger.debug(f"Downloading '{self.trackersListFile}' ...") - dlObj = self.api.add_uris(uris=[self.botHelper.configHelper.configVars[self.botHelper.configHelper.optVars[6]]], + dlObj = self.api.add_uris(uris=[self.botHelper.configHelper.configVars[self.botHelper.configHelper.optVars[7]]], options={'out': self.trackersListFile}) while dlObj.status == 'active': time.sleep(0.1) @@ -1527,28 +1906,47 @@ def dlTrackersList(self): self.logger.debug(f"Downloaded '{self.trackersListFile}' !") else: self.logger.debug(f"Download Failed - '{self.trackersListFile}' ! Retrying...") - self.dlTrackersList() + self.getTrackersList() + + def makeConf(self) -> None: + if os.path.exists(self.confFileDir.split('/')[0]): + shutil.rmtree(self.confFileDir.split('/')[0]) + os.mkdir(self.confFileDir.split('/')[0]) + os.mkdir(self.confFileDir) + confStr = '' + confData: typing.Dict = \ + { + **self.confDefaults, + **self.botHelper.configHelper.configVars['ariaConf'] + } + for confKey in confData.keys(): + confStr += f'{confKey}={confData[confKey]}\n' + open(f'{self.confFileDir}/{self.confFile}', 'wt').write(confStr) + + def removeAllDownloads(self) -> None: + self.api.remove_all(force=True) def startListener(self) -> None: - self.api.listen_to_notifications(threaded=True, + self.api.listen_to_notifications(threaded=True, handle_signals=False, on_download_start=self.onDownloadStart, on_download_pause=self.onDownloadPause, on_download_complete=self.onDownloadComplete, on_download_stop=self.onDownloadStop, on_download_error=self.onDownloadError) + def stopListener(self) -> None: + self.api.stop_listening() + def updateProgress(self, uid: str) -> None: if uid in self.gids.keys(): dlObj = self.getDlObj(self.gids[uid]) currVars: typing.Dict[str, typing.Union[int, float, str]] = \ - {MirrorInfo.updatableVars[0]: dlObj.total_length, - MirrorInfo.updatableVars[1]: dlObj.completed_length, - MirrorInfo.updatableVars[2]: dlObj.download_speed, - MirrorInfo.updatableVars[3]: time.time()} - if dlObj.is_torrent: - currVars[MirrorInfo.updatableVars[4]] = True - currVars[MirrorInfo.updatableVars[5]] = dlObj.num_seeders - currVars[MirrorInfo.updatableVars[6]] = dlObj.connections + { + MirrorInfo.UpdatableVars[0]: dlObj.total_length, + MirrorInfo.UpdatableVars[1]: dlObj.completed_length, + MirrorInfo.UpdatableVars[2]: dlObj.download_speed, + MirrorInfo.UpdatableVars[3]: time.time() + } self.botHelper.mirrorHelper.mirrorInfos[uid].updateVars(currVars) def onDownloadStart(self, _: aria2p.API, gid: str) -> None: @@ -1559,10 +1957,19 @@ def onDownloadPause(self, _: aria2p.API, gid: str) -> None: def onDownloadComplete(self, _: aria2p.API, gid: str) -> None: self.logger.debug(vars(self.getDlObj(gid))) - if self.getDlObj(gid).followed_by_ids: - self.gids[self.getUid(gid)] = self.getDlObj(gid).followed_by_ids[0] - return - self.botHelper.listenerHelper.updateStatus(self.getUid(gid), MirrorStatus.downloadComplete) + uid = self.getUid(gid) + mirrorStatus = MirrorStatus.downloadComplete + dlPath = self.botHelper.mirrorHelper.mirrorInfos[uid].path + dlContent = os.path.join(dlPath, os.listdir(dlPath)[0]) + if os.path.isfile(dlContent) and (magic.Magic(mime=True).from_file(dlContent) == self.botHelper.torrentFileMimeType): + self.botHelper.mirrorHelper.mirrorInfos[uid].isAriaDownload = False + self.botHelper.mirrorHelper.mirrorInfos[uid].isQbitTorrentDownload = True + self.botHelper.mirrorHelper.mirrorInfos[uid].downloadUrl = self.botHelper.getHelper.magnetFromTorrentFile(dlContent) + os.remove(dlContent) + os.rmdir(self.botHelper.mirrorHelper.mirrorInfos[uid].path) + mirrorStatus = MirrorStatus.downloadStart + self.botHelper.listenerHelper.updateStatus(uid, mirrorStatus) + self.gids.pop(uid) def onDownloadStop(self, _: aria2p.API, gid: str) -> None: self.logger.debug(vars(self.getDlObj(gid))) @@ -1577,14 +1984,21 @@ def __init__(self, botHelper: BotHelper): def initHelper(self) -> None: super().initHelper() - self.authInfos: typing.List[str] = ['saJson', 'tokenJson'] - self.authTypes: typing.List[str] = ['saAuth', 'userAuth'] + self.authInfos: typing.List[str] = \ + [ + 'saJson', + 'tokenJson' + ] + self.authTypes: typing.List[str] = \ + [ + 'saAuth', + 'userAuth' + ] self.oauthScopes: typing.List[str] = ['https://www.googleapis.com/auth/drive'] self.baseFileDownloadUrl: str = 'https://drive.google.com/uc?id={}&export=download' self.baseFolderDownloadUrl: str = 'https://drive.google.com/drive/folders/{}' self.googleDriveFolderMimeType: str = 'application/vnd.google-apps.folder' self.chunkSize: int = 32 * 1024 * 1024 - self.service: typing.Any = None if self.botHelper.configHelper.configVars[self.botHelper.configHelper.reqVars[4]]['authType'] == self.authTypes[0] and \ self.botHelper.configHelper.configVars[self.botHelper.configHelper.reqVars[4]]['authInfos'][self.authInfos[0]]: self.oauthCreds: google.oauth2.service_account.Credentials \ @@ -1601,7 +2015,7 @@ def initHelper(self) -> None: def addDownload(self, mirrorInfo: 'MirrorInfo') -> None: sourceId = self.getIdFromUrl(mirrorInfo.downloadUrl) - self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].updateVars({mirrorInfo.updatableVars[0]: self.getSizeById(sourceId)}) + self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].updateVars({mirrorInfo.UpdatableVars[0]: self.getSizeById(sourceId)}) isFolder = False if self.getMetadataById(sourceId, 'mimeType') == self.googleDriveFolderMimeType: isFolder = True @@ -1619,12 +2033,12 @@ def addDownload(self, mirrorInfo: 'MirrorInfo') -> None: self.downloadFile(sourceFileId=sourceId, dlPath=mirrorInfo.path, uid=mirrorInfo.uid) self.botHelper.listenerHelper.updateStatus(mirrorInfo.uid, MirrorStatus.downloadComplete) - def cancelDownload(self, uid: str) -> None: + def cancelDownload(self, mirrorInfo: 'MirrorInfo') -> None: raise NotImplementedError def addUpload(self, mirrorInfo: 'MirrorInfo') -> None: if not (mirrorInfo.isGoogleDriveDownload and not (mirrorInfo.isCompress or mirrorInfo.isDecompress)): - currVars = {MirrorInfo.updatableVars[0]: self.botHelper.getHelper.folderSize(mirrorInfo.path)} + currVars = {MirrorInfo.UpdatableVars[0]: self.botHelper.getHelper.folderSize(mirrorInfo.path)} self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].updateVars(currVars) uploadPath = os.path.join(mirrorInfo.path, os.listdir(mirrorInfo.path)[0]) if os.path.isdir(uploadPath): @@ -1637,35 +2051,31 @@ def addUpload(self, mirrorInfo: 'MirrorInfo') -> None: time.sleep(self.botHelper.statusHelper.statusUpdateInterval) self.botHelper.listenerHelper.updateStatus(mirrorInfo.uid, MirrorStatus.uploadComplete) - def cancelUpload(self, uid: str) -> None: + def cancelUpload(self, mirrorInfo: 'MirrorInfo') -> None: raise NotImplementedError def authorizeApi(self) -> None: if self.botHelper.configHelper.configVars[self.botHelper.configHelper.reqVars[4]]['authType'] == self.authTypes[0]: - self.buildService() + pass if self.botHelper.configHelper.configVars[self.botHelper.configHelper.reqVars[4]]['authType'] == self.authTypes[1]: if not self.oauthCreds.valid: if self.oauthCreds.expired and self.oauthCreds.refresh_token: self.oauthCreds.refresh(google.auth.transport.requests.Request()) self.logger.info('Google Drive API Token Refreshed !') self.botHelper.configHelper.configVars[self.botHelper.configHelper.reqVars[4]]['authInfos'][self.authInfos[1]] = json.loads(self.oauthCreds.to_json()) - self.buildService() self.botHelper.configHelper.updateConfigJson() else: self.logger.info('Google Drive API User Token Needs to Refreshed Manually ! Exiting...') exit(1) - else: - self.buildService() - def buildService(self) -> None: - self.service = googleapiclient.discovery.build(serviceName='drive', version='v3', credentials=self.oauthCreds, - cache_discovery=False) + def buildService(self) -> typing.Any: + return googleapiclient.discovery.build(serviceName='drive', version='v3', credentials=self.oauthCreds, cache_discovery=False) def uploadFile(self, filePath: str, parentFolderId: str, uid: str) -> str: upStatus: googleapiclient.http.MediaUploadProgress fileName, fileMimeType, fileMetadata, mediaBody = self.getUpData(filePath, isResumable=True) fileMetadata['parents'] = [parentFolderId] - fileOp = self.service.files().create(supportsAllDrives=True, body=fileMetadata, media_body=mediaBody) + fileOp = self.buildService().files().create(supportsAllDrives=True, body=fileMetadata, media_body=mediaBody) upResponse = None while not upResponse: upStatus, upResponse = fileOp.next_chunk() @@ -1688,7 +2098,7 @@ def uploadFolder(self, folderPath: str, parentFolderId: str, uid: str) -> str: def cloneFile(self, sourceFileId: str, parentFolderId: str, uid: str) -> str: fileMetadata = {'parents': [parentFolderId]} - fileOp = self.service.files().copy(supportsAllDrives=True, fileId=sourceFileId, body=fileMetadata).execute() + fileOp = self.buildService().files().copy(supportsAllDrives=True, fileId=sourceFileId, body=fileMetadata).execute() self.updateProgress(self.getSizeById(sourceFileId), uid) return fileOp['id'] @@ -1709,8 +2119,8 @@ def downloadFile(self, sourceFileId: str, dlPath: str, uid: str) -> None: filePath = os.path.join(dlPath, fileName) downStatus: googleapiclient.http.MediaDownloadProgress fileOp = googleapiclient.http.MediaIoBaseDownload(fd=open(filePath, 'wb'), chunksize=self.chunkSize, - request=self.service.files().get_media(fileId=sourceFileId, - supportsAllDrives=True)) + request=self.buildService().files().get_media(fileId=sourceFileId, + supportsAllDrives=True)) downResponse = None while not downResponse: downStatus, downResponse = fileOp.next_chunk() @@ -1733,13 +2143,13 @@ def downloadFolder(self, sourceFolderId: str, dlPath: str, uid: str) -> None: def createFolder(self, folderName: str, parentFolderId: str) -> str: folderMetadata = {'name': folderName, 'parents': [parentFolderId], 'mimeType': self.googleDriveFolderMimeType} - folderOp = self.service.files().create(supportsAllDrives=True, body=folderMetadata).execute() + folderOp = self.buildService().files().create(supportsAllDrives=True, body=folderMetadata).execute() return folderOp['id'] def deleteByUrl(self, url: str) -> str: contentId = self.getIdFromUrl(url) if contentId != '': - self.service.files().delete(fileId=contentId, supportsAllDrives=True).execute() + self.buildService().files().delete(fileId=contentId, supportsAllDrives=True).execute() return f'Deleted: [{url}]' return 'Not a Valid Google Drive Link !' @@ -1752,7 +2162,11 @@ def getIdFromUrl(url: str) -> str: def getUpData(self, filePath: str, isResumable: bool) -> (str, str, typing.Dict, googleapiclient.http.MediaIoBaseUpload): fileName = filePath.split('/')[-1] fileMimeType = magic.Magic(mime=True).from_file(filePath) - fileMetadata = {'name': fileName, 'mimeType': fileMimeType} + fileMetadata = \ + { + 'name': fileName, + 'mimeType': fileMimeType + } if isResumable: mediaBody = googleapiclient.http.MediaIoBaseUpload(fd=open(filePath, 'rb'), mimetype=fileMimeType, resumable=True, chunksize=self.chunkSize) @@ -1762,16 +2176,16 @@ def getUpData(self, filePath: str, isResumable: bool) -> (str, str, typing.Dict, return fileName, fileMimeType, fileMetadata, mediaBody def getMetadataById(self, sourceId: str, field: str) -> str: - return self.service.files().get(supportsAllDrives=True, fileId=sourceId, fields=field).execute().get(field) + return self.buildService().files().get(supportsAllDrives=True, fileId=sourceId, fields=field).execute().get(field) def getFolderContentsById(self, folderId: str) -> typing.List: query = f"'{folderId}' in parents" pageToken = None folderContents: typing.List = [] while True: - result = self.service.files().list(supportsAllDrives=True, includeTeamDriveItems=True, spaces='drive', - fields='nextPageToken, files(name, id, mimeType, size)', - q=query, pageSize=200, pageToken=pageToken).execute() + result = self.buildService().files().list(supportsAllDrives=True, includeTeamDriveItems=True, spaces='drive', + fields='nextPageToken, files(name, id, mimeType, size)', + q=query, pageSize=200, pageToken=pageToken).execute() for content in result.get('files', []): folderContents.append(content) pageToken = result.get('nextPageToken', None) @@ -1795,7 +2209,7 @@ def getSizeById(self, sourceId: str) -> int: def patchFile(self, filePath: str, fileId: str) -> str: fileName, fileMimeType, fileMetadata, mediaBody = self.getUpData(filePath, isResumable=False) - fileOp = self.service.files().update(fileId=fileId, body=fileMetadata, media_body=mediaBody).execute() + fileOp = self.buildService().files().update(fileId=fileId, body=fileMetadata, media_body=mediaBody).execute() return f"Patched: [{fileOp['id']}] [{fileName}] [{os.path.getsize(fileName)} bytes]" def updateProgress(self, sizeUpdate: int, uid: str): @@ -1806,9 +2220,9 @@ def updateProgress(self, sizeUpdate: int, uid: str): timeCurrent = time.time() timeDiff = timeCurrent - timeLast speedCurrent = (int(sizeUpdate / timeDiff) if timeDiff else speedLast) - self.botHelper.mirrorHelper.mirrorInfos[uid].updateVars({MirrorInfo.updatableVars[1]: sizeCurrent, - MirrorInfo.updatableVars[2]: speedCurrent, - MirrorInfo.updatableVars[3]: timeCurrent}) + self.botHelper.mirrorHelper.mirrorInfos[uid].updateVars({MirrorInfo.UpdatableVars[1]: sizeCurrent, + MirrorInfo.UpdatableVars[2]: speedCurrent, + MirrorInfo.UpdatableVars[3]: timeCurrent}) class MegaHelper(BaseHelper): @@ -1837,16 +2251,16 @@ def addDownload(self, mirrorInfo: 'MirrorInfo') -> None: self.dlNodes[mirrorInfo.uid] = self.apiWrapper.getFolderNode(mirrorInfo.downloadUrl) if 'file' in mirrorInfo.downloadUrl: self.dlNodes[mirrorInfo.uid] = self.apiWrapper.getFileNode(mirrorInfo.downloadUrl) - self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].updateVars({MirrorInfo.updatableVars[0]: self.dlNodes[mirrorInfo.uid].getSize()}) + self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].updateVars({MirrorInfo.UpdatableVars[0]: int(self.dlNodes[mirrorInfo.uid].getSize())}) self.apiWrapper.downloadNode(self.dlNodes[mirrorInfo.uid], mirrorInfo.path) - def cancelDownload(self, uid: str) -> None: + def cancelDownload(self, mirrorInfo: 'MirrorInfo') -> None: raise NotImplementedError def addUpload(self, mirrorInfo: 'MirrorInfo') -> None: raise NotImplementedError - def cancelUpload(self, uid: str) -> None: + def cancelUpload(self, mirrorInfo: 'MirrorInfo') -> None: raise NotImplementedError def getUid(self, nodeName: str) -> str: @@ -1855,6 +2269,129 @@ def getUid(self, nodeName: str) -> str: return uid +class QbitTorrentHelper(BaseHelper): + def __init__(self, botHelper: BotHelper): + super().__init__(botHelper) + + def initHelper(self) -> None: + super().initHelper() + self.webApiPort = 8400 + self.apiClient = qbittorrentapi.Client(host='http://localhost', port=self.webApiPort, username='admin', password='adminadmin') + self.confFile = 'qBittorrent.conf' + self.confFileDir = 'qBittorrent/config' + self.confDefaults: typing.Dict[str, str] = \ + {} + self.daemonPid: int = 0 + self.daemonStartCmd: typing.List[str] = \ + [ + 'qbittorrent-nox', + f'--profile={os.getcwd()}' + ] + self.torrentHashes: typing.Dict[str, str] = {} + + def addDownload(self, mirrorInfo: 'MirrorInfo') -> None: + self.apiClient.torrents_add(urls=[mirrorInfo.downloadUrl], save_path=mirrorInfo.path) + self.torrentHashes[mirrorInfo.uid] = self.botHelper.getHelper.hashFromMagnet(mirrorInfo.downloadUrl) + + def cancelDownload(self, mirrorInfo: 'MirrorInfo') -> None: + self.pauseAndRemoveTorrent(mirrorInfo.uid) + + def daemonStart(self) -> None: + if self.botHelper.restartVars and self.botHelper.restartVars['qbitDaemonPid']: + self.daemonPid = self.botHelper.restartVars['qbitDaemonPid'] + self.logger.info(f'qbitDaemon Already Running (pid {self.daemonPid}) !') + if not self.daemonPid: + self.daemonPid = self.botHelper.subprocessHelper.procInit(self.daemonStartCmd).pid + self.logger.info(f'qbitDaemon Started (pid {self.daemonPid}) !') + + # TODO: implement this method + def daemonCheck(self) -> None: + pass + + def daemonStop(self) -> None: + self.botHelper.subprocessHelper.procTerm(self.daemonPid) + self.logger.info(f'qbitDaemon Terminated (pid {self.daemonPid}) !') + + def makeConf(self) -> None: + if os.path.exists(self.confFileDir.split('/')[0]): + shutil.rmtree(self.confFileDir.split('/')[0]) + os.mkdir(self.confFileDir.split('/')[0]) + os.mkdir(self.confFileDir) + confStr = '' + lvlZeroData: typing.Dict = \ + { + **self.confDefaults, + **self.botHelper.configHelper.configVars['qbitTorrentConf'] + } + for lvlOneKey in lvlZeroData.keys(): + lvlOneData: typing.Dict = lvlZeroData[lvlOneKey] + confStr += f'[{lvlOneKey}]\n' + for lvlTwoKey in lvlOneData.keys(): + lvlTwoData: typing.Dict = lvlOneData[lvlTwoKey] + for lvlThreeKey in lvlTwoData.keys(): + lvlThreeData: str = lvlTwoData[lvlThreeKey] + confStr += f'{lvlTwoKey}\\{lvlThreeKey}={lvlThreeData}\n' + open(f'{self.confFileDir}/{self.confFile}', 'wt').write(confStr.replace('\n\\', '\n')) + + def removeAllDownloads(self) -> None: + self.apiClient.torrents_delete(torrent_hashes='all', delete_files=True) + + def setTrackersList(self) -> None: + self.apiClient.app.setPreferences({'add_trackers': open(self.botHelper.ariaHelper.trackersListFile, 'rt').read()}) + + def authorizeApi(self) -> None: + self.apiClient.auth_log_in() + + def unauthorizeApi(self) -> None: + self.apiClient.auth_log_out() + + def updateProgress(self, uid: str) -> None: + torrentInfo = self.getTorrentInfos([self.torrentHashes[uid]])[0] + self.logger.debug(torrentInfo) + currVars: typing.Dict[str, typing.Union[int, float, str]] = \ + { + MirrorInfo.UpdatableVars[0]: int(torrentInfo.size), + MirrorInfo.UpdatableVars[1]: int(torrentInfo.downloaded), + MirrorInfo.UpdatableVars[2]: int(torrentInfo.dlspeed), + MirrorInfo.UpdatableVars[3]: time.time(), + MirrorInfo.UpdatableVars[4]: True, + MirrorInfo.UpdatableVars[5]: int(torrentInfo.num_seeds), + MirrorInfo.UpdatableVars[6]: int(torrentInfo.num_leechs) + } + self.botHelper.mirrorHelper.mirrorInfos[uid].updateVars(currVars) + self.checkState(torrentInfo) + + def checkState(self, torrentInfo: typing.Any) -> None: + torrentState = qbittorrentapi.TorrentStates(torrentInfo.state) + if torrentState in [ + qbittorrentapi.TorrentStates.DOWNLOADING + ]: + return + mirrorInfo = self.botHelper.mirrorHelper.mirrorInfos[self.getUid(torrentInfo.hash)] + if torrentState in [ + qbittorrentapi.TorrentStates.QUEUED_UPLOAD, + qbittorrentapi.TorrentStates.STALLED_UPLOAD, + qbittorrentapi.TorrentStates.FORCED_UPLOAD, + qbittorrentapi.TorrentStates.PAUSED_UPLOAD, + qbittorrentapi.TorrentStates.UPLOADING + ]: + self.pauseAndRemoveTorrent(mirrorInfo.uid) + self.botHelper.listenerHelper.updateStatus(mirrorInfo.uid, MirrorStatus.downloadComplete) + + def getTorrentInfos(self, torrentHashes: typing.List[str]) -> typing.List[typing.Any]: + return self.apiClient.torrents_info(torrent_hashes=torrentHashes) + + def getUid(self, torrentHash: str) -> str: + for uid in self.torrentHashes.keys(): + if torrentHash == self.torrentHashes[uid]: + return uid + + def pauseAndRemoveTorrent(self, uid: str) -> None: + self.apiClient.torrents_pause(torrent_hashes=self.torrentHashes[uid]) + self.apiClient.torrents_delete(torrent_hashes=self.torrentHashes[uid]) + self.torrentHashes.pop(uid) + + class TelegramHelper(BaseHelper): def __init__(self, botHelper: BotHelper): super().__init__(botHelper) @@ -1863,10 +2400,14 @@ def initHelper(self) -> None: super().initHelper() self.apiServerPid: int = 0 self.apiServerStartCmd: typing.List[str] = \ - ['telegram-bot-api', '--local', '--verbosity=9', - f'--api-id={self.botHelper.configHelper.configVars[self.botHelper.configHelper.reqVars[2]]}', - f'--api-hash={self.botHelper.configHelper.configVars[self.botHelper.configHelper.reqVars[3]]}', - f'--log={os.path.join(self.botHelper.envVars["currWorkDir"], self.botHelper.loggingHelper.logFiles[1])}'] + [ + 'telegram-bot-api', + '--local', + '--verbosity=9', + f'--api-id={self.botHelper.configHelper.configVars[self.botHelper.configHelper.reqVars[2]]}', + f'--api-hash={self.botHelper.configHelper.configVars[self.botHelper.configHelper.reqVars[3]]}', + f'--log={os.path.join(self.botHelper.envVars["currWorkDir"], self.botHelper.loggingHelper.logFiles[1])}' + ] self.uploadMaxSize: int = 2 * 1024 * 1024 * 1024 self.maxTimeout: int = 24 * 60 * 60 @@ -1875,7 +2416,7 @@ def apiServerStart(self) -> None: self.apiServerPid = self.botHelper.restartVars['botApiServerPid'] self.logger.info(f'botApiServer Already Running (pid {self.apiServerPid}) !') if not self.apiServerPid: - self.apiServerPid = subprocess.Popen(self.apiServerStartCmd).pid + self.apiServerPid = self.botHelper.subprocessHelper.procInit(self.apiServerStartCmd).pid self.logger.info(f'botApiServer Started (pid {self.apiServerPid}) !') def apiServerCheck(self) -> None: @@ -1889,23 +2430,23 @@ def apiServerCheck(self) -> None: continue def apiServerStop(self) -> None: - os.kill(self.apiServerPid, signal.SIGTERM) - self.logger.info(f"botApiServer terminated (pid {self.apiServerPid})") + self.botHelper.subprocessHelper.procTerm(self.apiServerPid) + self.logger.info(f'botApiServer Terminated (pid {self.apiServerPid}) !') def addDownload(self, mirrorInfo: 'MirrorInfo') -> None: replyTo = mirrorInfo.msg.reply_to_message for media in [replyTo.document, replyTo.audio, replyTo.video]: if media: - self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].updateVars({mirrorInfo.updatableVars[0]: media.file_size}) + self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].updateVars({mirrorInfo.UpdatableVars[0]: media.file_size}) self.downloadMedia(media, mirrorInfo.path) break self.botHelper.listenerHelper.updateStatus(mirrorInfo.uid, MirrorStatus.downloadComplete) - def cancelDownload(self, uid: str) -> None: + def cancelDownload(self, mirrorInfo: 'MirrorInfo') -> None: raise NotImplementedError def addUpload(self, mirrorInfo: 'MirrorInfo') -> None: - currVars = {MirrorInfo.updatableVars[0]: self.botHelper.getHelper.folderSize(mirrorInfo.path)} + currVars = {MirrorInfo.UpdatableVars[0]: self.botHelper.getHelper.folderSize(mirrorInfo.path)} self.botHelper.mirrorHelper.mirrorInfos[mirrorInfo.uid].updateVars(currVars) uploadPath = os.path.join(mirrorInfo.path, os.listdir(mirrorInfo.path)[0]) upResponse: bool = True @@ -1922,7 +2463,7 @@ def addUpload(self, mirrorInfo: 'MirrorInfo') -> None: if not upResponse: self.botHelper.listenerHelper.updateStatus(mirrorInfo.uid, MirrorStatus.cancelMirror) - def cancelUpload(self, uid: str) -> None: + def cancelUpload(self, mirrorInfo: 'MirrorInfo') -> None: raise NotImplementedError def downloadMedia(self, media: typing.Union[telegram.Document, telegram.Audio, telegram.Video], mirrorInfoPath: str) -> None: @@ -1964,11 +2505,18 @@ def initHelper(self) -> None: super().initHelper() def addDownload(self, mirrorInfo: 'MirrorInfo') -> None: - ytdlOpts: dict = {'quiet': True, 'format': mirrorInfo.ytdlFormat, 'progress_hooks': [self.progressHook], - 'outtmpl': f'{mirrorInfo.path}/%(title)s-%(id)s.f%(format_id)s.%(ext)s'} + ytdlOpts: typing.Dict = \ + { + 'quiet': True, + 'format': mirrorInfo.ytdlFormat, + 'progress_hooks': [ + self.progressHook + ], + 'outtmpl': f'{mirrorInfo.path}/%(title)s-%(id)s.f%(format_id)s.%(ext)s' + } self.downloadVideo(mirrorInfo.downloadUrl, ytdlOpts) - def cancelDownload(self, uid: str) -> None: + def cancelDownload(self, mirrorInfo: 'MirrorInfo') -> None: raise NotImplementedError @staticmethod @@ -1980,10 +2528,12 @@ def progressHook(self, progressUpdate: dict): uid = progressUpdate['filename'].replace(self.botHelper.envVars['dlRootDirPath'], '').split('/')[1] if progressUpdate['status'] == 'downloading': currVars: typing.Dict[str, typing.Union[int, float, str]] = \ - {MirrorInfo.updatableVars[0]: int((sizeTotal if (sizeTotal := progressUpdate['total_bytes']) else 0)), - MirrorInfo.updatableVars[1]: int((sizeCurrent if (sizeCurrent := progressUpdate['downloaded_bytes']) else 0)), - MirrorInfo.updatableVars[2]: int((speedCurrent if (speedCurrent := progressUpdate['speed']) else 0)), - MirrorInfo.updatableVars[3]: time.time()} + { + MirrorInfo.UpdatableVars[0]: int((sizeTotal if (sizeTotal := progressUpdate['total_bytes']) else 0)), + MirrorInfo.UpdatableVars[1]: int((sizeCurrent if (sizeCurrent := progressUpdate['downloaded_bytes']) else 0)), + MirrorInfo.UpdatableVars[2]: int((speedCurrent if (speedCurrent := progressUpdate['speed']) else 0)), + MirrorInfo.UpdatableVars[3]: time.time() + } self.botHelper.mirrorHelper.mirrorInfos[uid].updateVars(currVars) if progressUpdate['status'] == 'finished': self.botHelper.listenerHelper.updateStatus(uid, MirrorStatus.downloadComplete) @@ -2147,11 +2697,13 @@ def onTransferStart(self, api: mega.MegaApi, transfer: mega.MegaTransfer): def onTransferUpdate(self, api: mega.MegaApi, transfer: mega.MegaTransfer): if transfer.getFileName() in [dlNode.getName() for dlNode in list(self.megaHelper.dlNodes.values())]: uid = self.megaHelper.getUid(transfer.getFileName()) - currVars: typing.Dict[str, typing.Union[int, float, str]] = \ - {MirrorInfo.updatableVars[0]: transfer.getTotalBytes(), - MirrorInfo.updatableVars[1]: transfer.getTransferredBytes(), - MirrorInfo.updatableVars[2]: transfer.getSpeed(), - MirrorInfo.updatableVars[3]: time.time()} + currVars: typing.Dict[str, typing.Union[int, float]] = \ + { + MirrorInfo.UpdatableVars[0]: int(transfer.getTotalBytes()), + MirrorInfo.UpdatableVars[1]: int(transfer.getTransferredBytes()), + MirrorInfo.UpdatableVars[2]: int(transfer.getSpeed()), + MirrorInfo.UpdatableVars[3]: time.time() + } self.megaHelper.botHelper.mirrorHelper.mirrorInfos[uid].updateVars(currVars) self.logger.debug(f'Transfer Update ({transfer} {transfer.getFileName()}); ' f'Progress: {transfer.getTransferredBytes() / 1024} KB of {transfer.getTotalBytes() / 1024} KB, ' @@ -2171,8 +2723,16 @@ def onNodesUpdate(self, api: mega.MegaApi, nodes: mega.MegaNodeList): class MirrorInfo: - updatableVars: typing.List[str] = ['sizeTotal', 'sizeCurrent', 'speedCurrent', 'timeCurrent', - 'isTorrent', 'numSeeders', 'numLeechers'] + UpdatableVars: typing.List[str] = \ + [ + 'sizeTotal', + 'sizeCurrent', + 'speedCurrent', + 'timeCurrent', + 'isTorrent', + 'numSeeders', + 'numLeechers' + ] def __init__(self, msg: telegram.Message, botHelper: BotHelper): self.msg = msg @@ -2180,10 +2740,11 @@ def __init__(self, msg: telegram.Message, botHelper: BotHelper): self.chatId = msg.chat.id self.uid: str = botHelper.getHelper.randomString(8) self.path: str = os.path.join(botHelper.envVars['dlRootDirPath'], self.uid) - self.status: str = '' + self.currentStatus: str = '' + self.previousStatus: str = '' self.tag: str = '' self.downloadUrl: str = '' - self.ytdlFormat: str = botHelper.configHelper.configVars[botHelper.configHelper.optVars[7]] + self.ytdlFormat: str = botHelper.configHelper.configVars[botHelper.configHelper.optVars[8]] self.sizeTotal: int = 0 self.sizeCurrent: int = 0 self.timeStart: float = 0.0 @@ -2199,6 +2760,7 @@ def __init__(self, msg: telegram.Message, botHelper: BotHelper): self.isAriaDownload: bool = False self.isGoogleDriveDownload: bool = False self.isMegaDownload: bool = False + self.isQbitTorrentDownload: bool = False self.isTelegramDownload: bool = False self.isYouTubeDownload: bool = False self.isGoogleDriveUpload: bool = False @@ -2213,22 +2775,28 @@ def resetVars(self): self.speedCurrent = 0 self.progressPercent = 0.0 + def updateStatus(self, mirrorStatus: str) -> None: + if self.currentStatus == mirrorStatus: + return + self.previousStatus = self.currentStatus + self.currentStatus = mirrorStatus + def updateVars(self, currVars: typing.Dict[str, typing.Union[int, float, str]]) -> None: currVarsKeys = list(currVars.keys()) - if self.updatableVars[0] in currVarsKeys: - self.sizeTotal = currVars[self.updatableVars[0]] - if self.updatableVars[1] in currVarsKeys and self.updatableVars[2] in currVarsKeys: - self.sizeCurrent = currVars[self.updatableVars[1]] - self.speedCurrent = currVars[self.updatableVars[2]] - self.timeCurrent = currVars[self.updatableVars[3]] + if self.UpdatableVars[0] in currVarsKeys: + self.sizeTotal = currVars[self.UpdatableVars[0]] + if self.UpdatableVars[1] in currVarsKeys and self.UpdatableVars[2] in currVarsKeys: + self.sizeCurrent = currVars[self.UpdatableVars[1]] + self.speedCurrent = currVars[self.UpdatableVars[2]] + self.timeCurrent = currVars[self.UpdatableVars[3]] if self.sizeTotal != 0: self.progressPercent = round(((self.sizeCurrent / self.sizeTotal) * 100), ndigits=2) if self.speedCurrent != 0: self.timeEnd = self.timeCurrent + ((self.sizeTotal - self.sizeCurrent) / self.speedCurrent) - if self.updatableVars[4] in currVarsKeys: + if self.UpdatableVars[4] in currVarsKeys: self.isTorrent = True - self.numSeeders = currVars[self.updatableVars[5]] - self.numLeechers = currVars[self.updatableVars[6]] + self.numSeeders = currVars[self.UpdatableVars[5]] + self.numLeechers = currVars[self.UpdatableVars[6]] class MirrorStatus: @@ -2284,14 +2852,14 @@ def __init__(self, botHelper: BotHelper): def serveForever(self, forceEventLoop: bool = False, ready: threading.Event = None) -> None: with self.serverLock: self.isRunning = True - self.logger.debug('Webhook Server started.') + self.logger.debug('Webhook Server Started.') self.ensureEventLoop(forceEventLoop=forceEventLoop) self.loop = tornado.ioloop.IOLoop.current() self.httpServer.listen(self.listenPort, address=self.listenAddress) if ready is not None: ready.set() self.loop.start() - self.logger.debug('Webhook Server stopped.') + self.logger.debug('Webhook Server Stopped.') self.isRunning = False def shutdown(self) -> None: