Add support for scraping movie streams from torrentio

This commit is contained in:
mhdzumair
2024-01-25 12:36:57 +05:30
parent 2268df8b04
commit f5189f3038
20 changed files with 694 additions and 148 deletions
+1
View File
@@ -31,6 +31,7 @@ playwright-stealth = "*"
pikpakapi = {git = "git+https://github.com/mhdzumair/PikPakAPI.git"}
parse-torrent-title = {git = "git+https://github.com/mhdzumair/parse-torrent-title"}
diskcache = "*"
demagnetize = "*"
[dev-packages]
pysocks = "*"
Generated
+224 -1
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "7c6de99d98df9c4bedf75371ac04dcf1b6073812835279f87784d5f80fe931da"
"sha256": "b956a956a444c18919c0d7c01e8bb9cf50a3a54c987d0069f7b99d6d46afbe60"
},
"pipfile-spec": 6,
"requires": {
@@ -41,6 +41,14 @@
"markers": "python_version >= '3.6'",
"version": "==3.10.4"
},
"attrs": {
"hashes": [
"sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
"sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
],
"markers": "python_version >= '3.7'",
"version": "==23.2.0"
},
"beanie": {
"hashes": [
"sha256:2328f0f745ea8b4626a818c79ed71021a5c4362fc2c0adc48a59e6b40c5ce54b",
@@ -183,6 +191,14 @@
"markers": "python_version >= '3.7'",
"version": "==8.1.7"
},
"click-loglevel": {
"hashes": [
"sha256:897070fd4bf5b503edb5a1ecc0551448647901f4fac83a01c9f00aa28ad86d60",
"sha256:fcc98a136a96479b4768494df25017114ba1cb525dac0bed619209b3578fd4f3"
],
"markers": "python_version >= '3.7'",
"version": "==0.5.0"
},
"cloudscraper": {
"hashes": [
"sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3",
@@ -191,6 +207,23 @@
"index": "pypi",
"version": "==1.2.71"
},
"colorlog": {
"hashes": [
"sha256:4ed23b05a1154294ac99f511fabe8c1d6d4364ec1f7fc989c7fb515ccc29d375",
"sha256:fbb6fdf9d5685f2517f388fb29bb27d54e8654dd31f58bc2a3b217e967a95ca6"
],
"markers": "python_version >= '3.6'",
"version": "==6.8.0"
},
"demagnetize": {
"hashes": [
"sha256:a3ac825bb8d949d3d1ea17fb67b48d41c22a22bf0eae6c70817042aa7db9f01c",
"sha256:ab023ff72d03cc2ed369e94aa1d6d513e5774735c3adee9bc66e274e8207af4e"
],
"index": "pypi",
"markers": "python_version >= '3.10'",
"version": "==0.3.0"
},
"diskcache": {
"hashes": [
"sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc",
@@ -218,6 +251,12 @@
"markers": "python_version >= '3.8'",
"version": "==0.109.0"
},
"flatbencode": {
"hashes": [
"sha256:77397d5d0108835404f6f7cad640d403c7025a11812afc7e1df4b0f92589ba77"
],
"version": "==0.2.1"
},
"greenlet": {
"hashes": [
"sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a",
@@ -538,6 +577,86 @@
"markers": "python_version >= '3.7'",
"version": "==3.3.2"
},
"multidict": {
"hashes": [
"sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9",
"sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8",
"sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03",
"sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710",
"sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161",
"sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664",
"sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569",
"sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067",
"sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313",
"sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706",
"sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2",
"sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636",
"sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49",
"sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93",
"sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603",
"sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0",
"sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60",
"sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4",
"sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e",
"sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1",
"sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60",
"sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951",
"sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc",
"sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe",
"sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95",
"sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d",
"sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8",
"sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed",
"sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2",
"sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775",
"sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87",
"sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c",
"sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2",
"sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98",
"sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3",
"sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe",
"sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78",
"sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660",
"sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176",
"sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e",
"sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988",
"sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c",
"sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c",
"sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0",
"sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449",
"sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f",
"sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde",
"sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5",
"sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d",
"sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac",
"sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a",
"sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9",
"sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca",
"sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11",
"sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35",
"sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063",
"sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b",
"sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982",
"sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258",
"sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1",
"sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52",
"sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480",
"sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7",
"sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461",
"sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d",
"sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc",
"sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779",
"sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a",
"sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547",
"sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0",
"sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171",
"sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf",
"sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d",
"sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"
],
"markers": "python_version >= '3.7'",
"version": "==6.0.4"
},
"parse-torrent-title": {
"git": "git+https://github.com/mhdzumair/parse-torrent-title",
"ref": "c3002b767de54ffdcf633e60a16567b49cc7e0e6"
@@ -1225,6 +1344,14 @@
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"torf": {
"hashes": [
"sha256:e84ec73cd81cb2d56e36b9f4604806f16ee0c70aeafadfe2f31ca3266ca03a87",
"sha256:f846ef58b6b00bc3b6a5c25bf225c59149ee871738d87a20a8953f620b77671c"
],
"markers": "python_version >= '3.7'",
"version": "==4.2.4"
},
"typing-extensions": {
"hashes": [
"sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783",
@@ -1461,6 +1588,102 @@
"sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"
],
"version": "==12.0"
},
"yarl": {
"hashes": [
"sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51",
"sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce",
"sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559",
"sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0",
"sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81",
"sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc",
"sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4",
"sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c",
"sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130",
"sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136",
"sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e",
"sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec",
"sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7",
"sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1",
"sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455",
"sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099",
"sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129",
"sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10",
"sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142",
"sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98",
"sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa",
"sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7",
"sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525",
"sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c",
"sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9",
"sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c",
"sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8",
"sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b",
"sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf",
"sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23",
"sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd",
"sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27",
"sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f",
"sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece",
"sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434",
"sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec",
"sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff",
"sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78",
"sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d",
"sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863",
"sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53",
"sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31",
"sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15",
"sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5",
"sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b",
"sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57",
"sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3",
"sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1",
"sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f",
"sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad",
"sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c",
"sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7",
"sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2",
"sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b",
"sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2",
"sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b",
"sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9",
"sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be",
"sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e",
"sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984",
"sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4",
"sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074",
"sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2",
"sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392",
"sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91",
"sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541",
"sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf",
"sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572",
"sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66",
"sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575",
"sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14",
"sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5",
"sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1",
"sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e",
"sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551",
"sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17",
"sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead",
"sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0",
"sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe",
"sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234",
"sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0",
"sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7",
"sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34",
"sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42",
"sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385",
"sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78",
"sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be",
"sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958",
"sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749",
"sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"
],
"markers": "python_version >= '3.7'",
"version": "==1.9.4"
}
},
"develop": {
+19 -5
View File
@@ -5,7 +5,15 @@ from typing import Literal
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from diskcache import Cache
from fastapi import FastAPI, Request, Response, Depends, HTTPException, status
from fastapi import (
FastAPI,
Request,
Response,
Depends,
HTTPException,
status,
BackgroundTasks,
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse, FileResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
@@ -58,8 +66,9 @@ cache = Cache(
@app.on_event("startup")
async def init_db():
async def init_server():
await database.init()
await torrent.init_best_trackers()
@app.on_event("startup")
@@ -325,6 +334,7 @@ async def get_streams(
catalog_type: Literal["movie", "series", "tv"],
video_id: str,
response: Response,
background_tasks: BackgroundTasks,
secret_str: str = None,
season: int = None,
episode: int = None,
@@ -333,7 +343,9 @@ async def get_streams(
response.headers.update(headers)
if catalog_type == "movie":
fetched_streams = await crud.get_movie_streams(user_data, secret_str, video_id)
fetched_streams = await crud.get_movie_streams(
user_data, secret_str, video_id, background_tasks
)
elif catalog_type == "series":
fetched_streams = await crud.get_series_streams(
user_data, secret_str, video_id, season, episode
@@ -370,7 +382,9 @@ async def streaming_provider_endpoint(
if not stream:
raise HTTPException(status_code=400, detail="Stream not found.")
magnet_link = torrent.convert_info_hash_to_magnet(info_hash, stream.announce_list)
magnet_link = await torrent.convert_info_hash_to_magnet(
info_hash, stream.announce_list
)
episode_data = stream.get_episode(season, episode)
filename = episode_data.filename if episode_data else stream.filename
@@ -382,7 +396,7 @@ async def streaming_provider_endpoint(
)
elif user_data.streaming_provider.service == "realdebrid":
video_url = get_direct_link_from_realdebrid(
info_hash, magnet_link, user_data, filename, 1, 0
info_hash, magnet_link, user_data, filename, stream.file_index, 1, 0
)
elif user_data.streaming_provider.service == "alldebrid":
video_url = get_direct_link_from_alldebrid(
+44 -31
View File
@@ -1,22 +1,25 @@
import hashlib
import logging
from datetime import datetime, timedelta
from typing import Optional
from uuid import uuid4
from beanie import WriteRules
from beanie.operators import In
from fastapi import BackgroundTasks
from db import schemas, models
from db.config import settings
from db.models import (
MediaFusionMovieMetaData,
MediaFusionSeriesMetaData,
Streams,
TorrentStreams,
Season,
Episode,
MediaFusionTVMetaData,
)
from db.schemas import Stream, MetaIdProjection
from scrappers.torrentio import scrap_streams_from_torrentio
from utils.parser import (
parse_stream_data,
get_catalogs,
@@ -113,12 +116,28 @@ async def get_tv_data_by_id(
return tv_data
async def get_movie_streams(user_data, secret_str: str, video_id: str) -> list[Stream]:
movie_data = await get_movie_data_by_id(video_id, True)
if not movie_data:
return []
async def get_movie_streams(
user_data, secret_str: str, video_id: str, background_tasks: BackgroundTasks
) -> list[Stream]:
streams = (
await TorrentStreams.find({"meta_id": video_id})
.sort(-TorrentStreams.updated_at)
.to_list()
)
last_torrentio_stream = next(
(stream for stream in streams if stream.source == "Torrentio"), None
)
return await parse_stream_data(movie_data.streams, user_data, secret_str)
if video_id.startswith("tt") and "torrentio_streams" in user_data.selected_catalogs:
if (
last_torrentio_stream is None
or last_torrentio_stream.updated_at < datetime.now() - timedelta(days=1)
):
streams.extend(
await scrap_streams_from_torrentio(video_id, "movie", background_tasks)
)
return await parse_stream_data(streams, user_data, secret_str)
async def get_series_streams(
@@ -181,7 +200,7 @@ async def get_series_meta(meta_id: str):
# Loop through streams to populate the videos list
for stream in series_data.streams:
stream: Streams
stream: TorrentStreams
if stream.season: # Ensure the stream has season data
for episode in stream.season.episodes:
stream_id = (
@@ -256,9 +275,7 @@ async def save_movie_metadata(metadata: dict):
meta_id = existing_movie.id
# Determine file index for the main movie file (largest file)
largest_file = max(
metadata["torrent_metadata"]["file_data"], key=lambda x: x["size"]
)
largest_file = max(metadata["file_data"], key=lambda x: x["size"])
if "language" in metadata:
languages = (
@@ -270,11 +287,11 @@ async def save_movie_metadata(metadata: dict):
languages = [metadata["scrap_language"]]
# Create the stream object
new_stream = Streams(
id=metadata["torrent_metadata"]["info_hash"],
torrent_name=metadata["torrent_metadata"]["torrent_name"],
announce_list=metadata["torrent_metadata"]["announce_list"],
size=metadata["torrent_metadata"]["total_size"],
new_stream = TorrentStreams(
id=metadata["info_hash"],
torrent_name=metadata["torrent_name"],
announce_list=metadata["announce_list"],
size=metadata["total_size"],
filename=largest_file["filename"],
file_index=largest_file["index"],
languages=languages,
@@ -286,6 +303,7 @@ async def save_movie_metadata(metadata: dict):
source=metadata["source"],
catalog=get_catalogs(metadata["catalog"], languages),
created_at=metadata["created_at"],
meta_id=meta_id,
)
if existing_movie:
@@ -349,11 +367,7 @@ async def save_series_metadata(metadata: dict):
logging.info("Added series %s", series.title)
existing_stream = next(
(
s
for s in series.streams
if s.id == metadata["torrent_metadata"]["info_hash"]
),
(s for s in series.streams if s.id == metadata["info_hash"]),
None,
)
if existing_stream:
@@ -361,8 +375,6 @@ async def save_series_metadata(metadata: dict):
logging.info("Stream already exists for series %s", series.title)
return
season_number = metadata["season"]
# Extract episodes
episodes = [
Episode(
@@ -371,7 +383,7 @@ async def save_series_metadata(metadata: dict):
size=file["size"],
file_index=file["index"],
)
for file in metadata["torrent_metadata"]["file_data"]
for file in metadata["file_data"]
if file["episode"]
]
@@ -386,11 +398,11 @@ async def save_series_metadata(metadata: dict):
languages = [metadata["scrap_language"]]
# Create the stream
stream = Streams(
id=metadata["torrent_metadata"]["info_hash"],
torrent_name=metadata["torrent_metadata"]["torrent_name"],
announce_list=metadata["torrent_metadata"]["announce_list"],
size=metadata["torrent_metadata"]["total_size"],
stream = TorrentStreams(
id=metadata["info_hash"],
torrent_name=metadata["torrent_name"],
announce_list=metadata["announce_list"],
size=metadata["total_size"],
languages=languages,
resolution=metadata.get("resolution"),
codec=metadata.get("codec"),
@@ -400,7 +412,8 @@ async def save_series_metadata(metadata: dict):
source=metadata["source"],
catalog=get_catalogs(metadata["catalog"], languages),
created_at=metadata["created_at"],
season=Season(season_number=season_number, episodes=episodes),
season=Season(season_number=metadata["season"], episodes=episodes),
meta_id=series.id,
)
# Add the stream to the series
@@ -443,8 +456,8 @@ async def process_search_query(search_query: str, catalog_type: str) -> dict:
return {"metas": metas}
async def get_stream_by_info_hash(info_hash: str) -> Streams | None:
stream = await Streams.get(info_hash)
async def get_stream_by_info_hash(info_hash: str) -> TorrentStreams | None:
stream = await TorrentStreams.get(info_hash)
return stream
+2 -2
View File
@@ -5,7 +5,7 @@ from db.config import settings
from db.models import (
MediaFusionSeriesMetaData,
MediaFusionMovieMetaData,
Streams,
TorrentStreams,
TVStreams,
MediaFusionTVMetaData,
)
@@ -21,7 +21,7 @@ async def init():
document_models=[
MediaFusionMovieMetaData,
MediaFusionSeriesMetaData,
Streams,
TorrentStreams,
TVStreams,
MediaFusionTVMetaData,
],
+7 -5
View File
@@ -1,10 +1,9 @@
import hashlib
from datetime import datetime
from typing import Optional, Any
import pymongo
from beanie import Document, Link
from pydantic import BaseModel, Field, computed_field
from beanie import Document, Link, BackLink
from pydantic import BaseModel, Field
from pymongo import IndexModel, ASCENDING
@@ -20,7 +19,7 @@ class Season(BaseModel):
episodes: list[Episode]
class Streams(Document):
class TorrentStreams(Document):
id: str
torrent_name: str
size: int
@@ -32,6 +31,7 @@ class Streams(Document):
source: str
catalog: list[str]
created_at: datetime = Field(default_factory=datetime.now)
updated_at: Optional[datetime] = None
resolution: Optional[str]
codec: Optional[str]
quality: Optional[str]
@@ -39,6 +39,7 @@ class Streams(Document):
encoder: Optional[str]
seeders: Optional[int] = None
cached: Optional[bool] = None
meta_id: Optional[str] = None
def get_episode(self, season_number: int, episode_number: int) -> Optional[Episode]:
"""
@@ -58,6 +59,7 @@ class TVStreams(Document):
source: str
behaviorHints: dict[str, Any] | None = None
created_at: datetime = Field(default_factory=datetime.now)
meta_id: Optional[str] = None
class MediaFusionMetaData(Document):
@@ -67,7 +69,7 @@ class MediaFusionMetaData(Document):
poster: str
is_poster_working: Optional[bool] = True
background: Optional[str] = None
streams: list[Link[Streams]]
streams: list[Link[TorrentStreams]]
type: str
class Settings:
Binary file not shown.
+3 -3
View File
@@ -1,10 +1,10 @@
{
"id": "mhdzumair.addons.mediafusion",
"version": "3.6.5",
"version": "3.6.6",
"name": "Media Fusion",
"contactEmail": "mhdzumair@gmail",
"description": "Watch Tamil, Hindi, Malayalam, Kannada, English, and dubbed movies, series & live TV with the Media Fusion add-on. Source: https://github.com/mhdzumair/MediaFusion",
"logo": "https://882b9915d0fe-mediafusion.baby-beamup.club/static/images/mediafusion_logo.png",
"description": "Universal Stremio Add-on for Movies, Series & Live TV. Source: https://github.com/mhdzumair/MediaFusion",
"logo": "https://mediafusion.fun/static/images/mediafusion_logo.png",
"behaviorHints": {
"configurable": true,
"configurationRequired": false
+1 -2
View File
@@ -75,8 +75,7 @@ async def download_and_save_torrent(
logging.error(f"Info hash not found for {torrent_link}")
return False
parsed_data = PTN.parse(torrent_metadata["torrent_name"])
metadata.update({"torrent_metadata": torrent_metadata, **parsed_data})
metadata.update(torrent_metadata)
if not metadata.get("year"):
logging.error(f"Year not found for {page_link}")
+162
View File
@@ -0,0 +1,162 @@
import re
from datetime import datetime
import PTN
import httpx
from fastapi import BackgroundTasks
from db.models import TorrentStreams
from utils.parser import convert_size_to_bytes
from utils.torrent import info_hashes_to_torrent_metadata
async def fetch_stream_data(url: str) -> dict:
"""Fetch stream data asynchronously."""
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=10)
response.raise_for_status() # Will raise an exception for 4xx/5xx responses
return response.json()
async def scrap_streams_from_torrentio(
video_id: str, catalog_type: str, background_tasks: BackgroundTasks
) -> list[TorrentStreams]:
"""
Get streams by IMDb ID from torrentio stremio addon.
"""
url = f"https://torrentio.strem.fun/stream/{catalog_type}/{video_id}.json"
try:
stream_data = await fetch_stream_data(url)
return await store_and_parse_stream_data(
video_id, stream_data.get("streams", []), background_tasks
)
except (httpx.HTTPError, httpx.TimeoutException):
return [] # Return empty list in case of HTTP errors or timeouts
def parse_stream_title(stream: dict) -> dict:
"""Parse the stream title for metadata and other details."""
torrent_name = stream["title"].splitlines()[0]
metadata = PTN.parse(torrent_name)
# Initialize fields
details = stream["title"]
size = convert_size_to_bytes(extract_size_string(details))
seeders = extract_seeders(details)
languages = extract_languages(metadata, details)
return {
"torrent_name": torrent_name,
"size": size,
"seeders": seeders,
"languages": languages,
"metadata": metadata,
}
async def store_and_parse_stream_data(
video_id: str, stream_data: list, background_tasks: BackgroundTasks
) -> list[TorrentStreams]:
streams = []
info_hashes = []
for stream in stream_data:
parsed_data = parse_stream_title(stream)
if not parsed_data["seeders"]:
continue
torrent_stream = await TorrentStreams.get(stream["infoHash"])
if torrent_stream:
# Update existing stream
torrent_stream.seeders = parsed_data["seeders"]
torrent_stream.updated_at = datetime.now()
await torrent_stream.save()
else:
# Create new stream
torrent_stream = TorrentStreams(
id=stream["infoHash"],
torrent_name=parsed_data["torrent_name"],
announce_list=[],
size=parsed_data["size"],
filename=None,
file_index=stream.get("fileIdx"),
languages=parsed_data["languages"],
resolution=parsed_data["metadata"].get("resolution"),
codec=parsed_data["metadata"].get("codec"),
quality=parsed_data["metadata"].get("quality"),
audio=parsed_data["metadata"].get("audio"),
encoder=parsed_data["metadata"].get("encoder"),
source="Torrentio",
catalog=["torrentio_streams"],
updated_at=datetime.now(),
seeders=parsed_data["seeders"],
meta_id=video_id,
)
await torrent_stream.save()
streams.append(torrent_stream)
if torrent_stream.filename is None:
info_hashes.append(stream["infoHash"])
background_tasks.add_task(update_torrent_streams_metadata, info_hashes)
return streams
def extract_seeders(details: str) -> int:
"""Extract seeders from details string."""
seeders_match = re.search(r"👤 (\d+)", details)
return int(seeders_match.group(1)) if seeders_match else None
def extract_languages_from_title(title: str) -> list:
"""Extract languages and country flags from the title string."""
languages = []
if "Multi Audio" in title or "Multi Language" in title:
languages.append("Multi Language")
elif "Dual Audio" in title or "Dual Language" in title:
languages.append("Dual Language")
# Regex to match country flag emojis
flag_emojis = re.findall(r"[\U0001F1E6-\U0001F1FF]{2}", title)
if flag_emojis:
languages.extend(flag_emojis)
return languages
def extract_languages(metadata: dict, title: str) -> list:
"""Extract languages from metadata or title."""
language = metadata.get("language")
if language:
if isinstance(language, str):
return [language]
elif isinstance(language, list):
return language
return extract_languages_from_title(title)
def extract_size_string(details: str) -> str:
"""Extract the size string from the details."""
size_match = re.search(r"💾 (\d+(?:\.\d+)?\s*(GB|MB))", details, re.IGNORECASE)
return size_match.group(1) if size_match else ""
async def update_torrent_streams_metadata(info_hashes: list[str]):
"""Update torrent streams metadata."""
streams_metadata = await info_hashes_to_torrent_metadata(info_hashes, [])
for stream_metadata in streams_metadata:
if not stream_metadata:
continue
torrent_stream = await TorrentStreams.get(stream_metadata["info_hash"])
if torrent_stream:
torrent_stream.torrent_name = stream_metadata["torrent_name"]
torrent_stream.size = stream_metadata["total_size"]
largest_file = max(stream_metadata["file_data"], key=lambda x: x["size"])
torrent_stream.filename = largest_file["filename"]
torrent_stream.file_index = largest_file["index"]
torrent_stream.updated_at = datetime.now()
await torrent_stream.save()
+2 -2
View File
@@ -1,6 +1,6 @@
from typing import Any
from db.models import Streams
from db.models import TorrentStreams
from db.schemas import UserData
from streaming_providers.alldebrid.client import AllDebrid
from streaming_providers.exceptions import ProviderException
@@ -46,7 +46,7 @@ def get_direct_link_from_alldebrid(
return response["data"]["link"]
def update_ad_cache_status(streams: list[Streams], user_data: UserData):
def update_ad_cache_status(streams: list[TorrentStreams], user_data: UserData):
"""Updates the cache status of streams based on AllDebrid's instant availability."""
try:
+3 -3
View File
@@ -2,7 +2,7 @@ import time
import PTN
from db.models import Streams, Episode
from db.models import TorrentStreams, Episode
from db.schemas import UserData
from streaming_providers.debridlink.client import DebridLink
from streaming_providers.exceptions import ProviderException
@@ -12,7 +12,7 @@ def get_direct_link_from_debridlink(
info_hash: str,
magnet_link: str,
user_data: UserData,
stream: Streams,
stream: TorrentStreams,
episode_data: Episode = None,
max_retries=5,
retry_interval=5,
@@ -128,7 +128,7 @@ def select_episode_file(torrent_files: list, episode: int, file_name_key: str) -
)
def update_dl_cache_status(streams: list[Streams], user_data: UserData):
def update_dl_cache_status(streams: list[TorrentStreams], user_data: UserData):
"""Updates the cache status of streams based on DebridLink's instant availability."""
try:
+2 -2
View File
@@ -1,4 +1,4 @@
from db.models import Streams
from db.models import TorrentStreams
from db.schemas import UserData
from streaming_providers.exceptions import ProviderException
from streaming_providers.offcloud.client import OffCloud
@@ -38,7 +38,7 @@ def get_direct_link_from_offcloud(
return oc_client.create_download_link(request_id, torrent_info, filename)
def update_oc_cache_status(streams: list[Streams], user_data: UserData):
def update_oc_cache_status(streams: list[TorrentStreams], user_data: UserData):
"""Updates the cache status of streams based on OffCloud's instant availability."""
try:
+19 -6
View File
@@ -5,7 +5,7 @@ import httpx
from pikpakapi import PikPakApi, PikpakException
from thefuzz import fuzz
from db.models import Streams
from db.models import TorrentStreams
from db.schemas import UserData
from streaming_providers.exceptions import ProviderException
@@ -101,7 +101,18 @@ async def find_file_in_folder_tree(
# Fuzzy matching as a fallback
for file in files:
file["fuzzy_ratio"] = fuzz.ratio(filename, file["name"])
return sorted(files, key=lambda x: x["fuzzy_ratio"], reverse=True)[0]
selected_file = max(files, key=lambda x: x["fuzzy_ratio"])
# If the fuzzy ratio is less than 50, then select the largest file
if selected_file["fuzzy_ratio"] < 50:
selected_file = max(files, key=lambda x: x["size"])
if "video" not in selected_file["mime_type"]:
raise ProviderException(
"No matching file available for this torrent", "no_matching_file.mp4"
)
return selected_file
async def initialize_pikpak(user_data: UserData):
@@ -154,7 +165,7 @@ async def retrieve_or_download_file(
filename: str,
magnet_link: str,
info_hash: str,
stream: Streams,
stream: TorrentStreams,
max_retries: int,
retry_interval: int,
):
@@ -206,7 +217,7 @@ async def get_direct_link_from_pikpak(
info_hash: str,
magnet_link: str,
user_data: UserData,
stream: Streams,
stream: TorrentStreams,
filename: str,
max_retries=5,
retry_interval=5,
@@ -220,7 +231,7 @@ async def get_direct_link_from_pikpak(
selected_file = await retrieve_or_download_file(
pikpak,
folder_id,
filename,
filename or stream.torrent_name,
magnet_link,
info_hash,
stream,
@@ -232,7 +243,9 @@ async def get_direct_link_from_pikpak(
return file_data["web_content_link"]
async def update_pikpak_cache_status(streams: list[Streams], user_data: UserData):
async def update_pikpak_cache_status(
streams: list[TorrentStreams], user_data: UserData
):
"""Updates the cache status of streams based on PikPak's instant availability."""
try:
pikpak = await initialize_pikpak(user_data)
+12 -11
View File
@@ -1,14 +1,14 @@
from typing import Any
from db.models import Streams
from db.models import TorrentStreams
from db.schemas import UserData
from streaming_providers.exceptions import ProviderException
from streaming_providers.realdebrid.client import RealDebrid
def create_download_link(rd_client, torrent_id, filename):
def create_download_link(rd_client, torrent_id, filename, file_index):
torrent_info = rd_client.get_torrent_info(torrent_id)
file_index = select_file_index_from_torrent(torrent_info, filename)
file_index = select_file_index_from_torrent(torrent_info, filename, file_index)
response = rd_client.create_download_link(torrent_info["links"][file_index])
return response.get("download")
@@ -18,6 +18,7 @@ def get_direct_link_from_realdebrid(
magnet_link: str,
user_data: UserData,
filename: str,
file_index: int,
max_retries=5,
retry_interval=5,
) -> str:
@@ -28,12 +29,12 @@ def get_direct_link_from_realdebrid(
if torrent_info:
torrent_id = torrent_info.get("id")
if torrent_info["status"] == "downloaded":
return create_download_link(rd_client, torrent_id, filename)
return create_download_link(rd_client, torrent_id, filename, file_index)
elif torrent_info["status"] == "downloading":
rd_client.wait_for_status(
torrent_id, "downloaded", max_retries, retry_interval
)
return create_download_link(rd_client, torrent_id, filename)
return create_download_link(rd_client, torrent_id, filename, file_index)
elif torrent_info["status"] == "magnet_error":
rd_client.delete_torrent(torrent_id)
raise ProviderException(
@@ -57,10 +58,10 @@ def get_direct_link_from_realdebrid(
# Wait for download completion and get the direct link
rd_client.wait_for_status(torrent_id, "downloaded", max_retries, retry_interval)
return create_download_link(rd_client, torrent_id, filename)
return create_download_link(rd_client, torrent_id, filename, file_index)
def update_rd_cache_status(streams: list[Streams], user_data: UserData):
def update_rd_cache_status(streams: list[TorrentStreams], user_data: UserData):
"""Updates the cache status of streams based on RealDebrid's instant availability."""
try:
@@ -75,15 +76,15 @@ def update_rd_cache_status(streams: list[Streams], user_data: UserData):
pass
def select_file_index_from_torrent(torrent_info: dict[str, Any], filename: str) -> int:
def select_file_index_from_torrent(
torrent_info: dict[str, Any], filename: str, file_index: int
) -> int:
"""Select the file index from the torrent info."""
selected_files = [file for file in torrent_info["files"] if file["selected"] == 1]
for index, file in enumerate(selected_files):
if file["path"] == "/" + filename:
return index
raise ProviderException(
"No matching file available for this torrent", "api_error.mp4"
)
return file_index
def fetch_downloaded_info_hashes_from_rd(user_data: UserData) -> list[str]:
+27 -10
View File
@@ -5,7 +5,7 @@ from datetime import datetime
from seedrcc import Seedr
from thefuzz import fuzz
from db.models import Streams
from db.models import TorrentStreams
from db.schemas import UserData
from streaming_providers.exceptions import ProviderException
@@ -91,13 +91,18 @@ def get_file_details_from_folder(seedr, folder_id: int, filename: str):
# If the file is not found with exact match, try to find it by fuzzy ratio
for file in folder_content["files"]:
# rename file to remove any special characters
file["name"] = seedr_clean_name(file["name"])
seedr.renameFile(file["folder_file_id"], file["name"])
file["fuzzy_ratio"] = fuzz.ratio(filename, file["name"])
return sorted(
folder_content["files"], key=lambda x: x["fuzzy_ratio"], reverse=True
)[0]
selected_file = max(folder_content["files"], key=lambda x: x["fuzzy_ratio"])
# If the fuzzy ratio is less than 50, then select the largest file
if selected_file["fuzzy_ratio"] < 50:
selected_file = max(folder_content["files"], key=lambda x: x["size"])
if selected_file["play_video"] is False:
raise ProviderException(
"No matching file available for this torrent", "no_matching_file.mp4"
)
return selected_file
def seedr_clean_name(name: str, replace: str = "") -> str:
@@ -118,11 +123,20 @@ def get_seedr_client(user_data: UserData) -> Seedr:
return seedr
def rename_seedr_files(seedr, folder_id):
"""rename filenames to fix stremio android client bug."""
folder_content = seedr.listContents(folder_id)
for file in folder_content["files"]:
# rename file to remove any special characters
if file["name"] != seedr_clean_name(file["name"]):
seedr.renameFile(file["folder_file_id"], seedr_clean_name(file["name"]))
async def get_direct_link_from_seedr(
info_hash: str,
magnet_link: str,
user_data: UserData,
stream: Streams,
stream: TorrentStreams,
filename: str,
max_retries=5,
retry_interval=5,
@@ -144,10 +158,13 @@ async def get_direct_link_from_seedr(
folder = check_folder_status(seedr, info_hash)
folder_id = folder["id"]
# rename filenames to fix stremio android client bug.
rename_seedr_files(seedr, folder_id)
selected_file = get_file_details_from_folder(
seedr,
folder_id,
seedr_clean_name(filename),
seedr_clean_name(filename or stream.torrent_name),
)
video_link = seedr.fetchFile(selected_file["folder_file_id"])["url"]
@@ -183,7 +200,7 @@ def free_up_space(seedr, required_space):
available_space += folder["size"]
def update_seedr_cache_status(streams: list[Streams], user_data: UserData):
def update_seedr_cache_status(streams: list[TorrentStreams], user_data: UserData):
"""Updates the cache status of the streams based on the user's Seedr account."""
try:
seedr = get_seedr_client(user_data)
+15 -10
View File
@@ -1,27 +1,32 @@
from typing import Any
from db.models import Streams
from db.models import TorrentStreams
from db.schemas import UserData
from streaming_providers.exceptions import ProviderException
from streaming_providers.torbox.client import Torbox
def get_direct_link_from_torbox(
info_hash: str,
magnet_link: str,
user_data: UserData,
filename: str,
max_retries=5,
retry_interval=5,
info_hash: str,
magnet_link: str,
user_data: UserData,
filename: str,
max_retries=5,
retry_interval=5,
) -> str:
torbox_client = Torbox(token=user_data.streaming_provider.token)
# Check if the torrent already exists
torrent_info = torbox_client.get_available_torrent(info_hash)
if torrent_info:
if torrent_info["download_finished"] is True and torrent_info["download_present"] is True:
if (
torrent_info["download_finished"] is True
and torrent_info["download_present"] is True
):
file_id = select_file_id_from_torrent(torrent_info, filename)
response = torbox_client.create_download_link(torrent_info.get("id"), file_id)
response = torbox_client.create_download_link(
torrent_info.get("id"), file_id
)
return response["data"]
else:
# If torrent doesn't exist, add it
@@ -34,7 +39,7 @@ def get_direct_link_from_torbox(
)
def update_torbox_cache_status(streams: list[Streams], user_data: UserData):
def update_torbox_cache_status(streams: list[TorrentStreams], user_data: UserData):
"""Updates the cache status of streams based on Torbox's instant availability."""
try:
+2
View File
@@ -31,6 +31,7 @@ CATALOG_ID_DATA = [
"english_series",
"live_tv",
"mediafusion_search_tv",
"torrentio_streams",
]
CATALOG_NAME_DATA = [
@@ -66,6 +67,7 @@ CATALOG_NAME_DATA = [
"English Series",
"Live TV",
"MediaFusion Search TV",
"Torrentio Streams",
]
RESOLUTIONS = [
+19 -5
View File
@@ -1,11 +1,12 @@
import asyncio
import math
import re
import requests
from imdb import Cinemagoer
from db.config import settings
from db.models import Streams, MediaFusionTVMetaData
from db.models import TorrentStreams, MediaFusionTVMetaData
from db.schemas import Stream, UserData
from streaming_providers.alldebrid.utils import (
update_ad_cache_status,
@@ -40,8 +41,8 @@ ia = Cinemagoer()
async def filter_and_sort_streams(
streams: list[Streams], user_data: UserData
) -> list[Streams]:
streams: list[TorrentStreams], user_data: UserData
) -> list[TorrentStreams]:
# Filter streams by selected catalogs and resolutions
filtered_streams = [
stream
@@ -81,7 +82,7 @@ async def filter_and_sort_streams(
async def parse_stream_data(
streams: list[Streams],
streams: list[TorrentStreams],
user_data: UserData,
secret_str: str,
season: int = None,
@@ -115,11 +116,14 @@ async def parse_stream_data(
else:
streaming_provider = "Torrent ⏳"
seeders = f"👤 {stream_data.seeders}" if stream_data.seeders else None
description_parts = [
quality_detail,
convert_bytes_to_readable(
episode_data.size if episode_data else stream_data.size
),
seeders,
" + ".join(stream_data.languages),
stream_data.source,
]
@@ -159,7 +163,17 @@ def convert_bytes_to_readable(size_bytes: int) -> str:
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return f"{s} {size_name[i]}"
return f"💾 {s} {size_name[i]}"
def convert_size_to_bytes(size_str: str) -> int:
"""Convert size string to bytes."""
match = re.match(r"(\d+(?:\.\d+)?)\s*(GB|MB)", size_str, re.IGNORECASE)
if match:
size, unit = match.groups()
size = float(size)
return int(size * 1024**3) if "GB" in unit.upper() else int(size * 1024**2)
return 0
def get_catalogs(catalog: str, languages: list[str]) -> list[str]:
+130 -50
View File
@@ -1,88 +1,168 @@
import hashlib
import io
import logging
from contextlib import AsyncExitStack, asynccontextmanager
from typing import Awaitable, Iterable, AsyncIterator, Optional, TypeVar
from urllib.parse import quote
import PTN
import bencodepy
import anyio
import httpx
from anyio import (
create_task_group,
create_memory_object_stream,
CapacityLimiter,
)
from anyio.streams.memory import MemoryObjectSendStream
from demagnetize.core import Demagnetizer
from torf import Magnet, Torrent
TRACKERS = [
"http://tracker3.itzmx.com:8080/announce",
"udp://9.rarbg.me:2710/announce",
"udp://9.rarbg.to:2710/announce",
"udp://exodus.desync.com:6969/announce",
"udp://ipv4.tracker.harry.lu:80/announce",
"udp://tracker.coppersurfer.tk:6969/announce",
"udp://tracker.internetwarriors.net:1337/announce",
"udp://tracker.leechers-paradise.org:6969/announce",
"udp://tracker.openbittorrent.com:80/announce",
"udp://tracker.opentrackr.org:1337/announce",
"udp://tracker.pomf.se:80/announce",
"udp://exodus.desync.com:6969/announce",
"udp://tracker.internetwarriors.net:1337/announce",
"udp://tracker.tiny-vps.com:6969/announce",
"udp://tracker.coppersurfer.tk:6969/announce",
"udp://tracker.leechers-paradise.org:6969/announce",
"udp://9.rarbg.to:2710/announce",
"udp://9.rarbg.me:2710/announce",
"http://tracker3.itzmx.com:8080/announce",
"udp://ipv4.tracker.harry.lu:80/announce",
]
def extract_torrent_metadata(content) -> dict:
def response_content_to_torrent(content) -> Torrent | None:
try:
torrent_data = bencodepy.decode(content)
return Torrent.read_stream(io.BytesIO(content))
except Exception as e:
logging.error(f"Error occurred: {e}")
return
info = torrent_data[b"info"]
info_encoded = bencodepy.encode(info)
m = hashlib.sha1()
m.update(info_encoded)
info_hash = m.hexdigest()
def extract_torrent_metadata(content: Torrent | bytes) -> dict:
if isinstance(content, bytes):
torrent = response_content_to_torrent(content)
if not torrent:
return {}
else:
torrent = content
# Extract file size, file list, and announce list
if b"files" in info:
total_size = sum(file[b"length"] for file in info[b"files"])
file_data = []
for idx, file in enumerate(info[b"files"]):
filename = file[b"path"][0].decode()
parsed_data = PTN.parse(filename)
file_data.append(
{
"filename": filename,
"size": file[b"length"],
"index": idx,
"season": parsed_data.get("season"),
"episode": parsed_data.get("episode"),
}
)
else:
total_size = info[b"length"]
filename = info[b"name"].decode()
try:
info_hash = torrent.infohash
total_size = torrent.size
file_data = []
for idx, file in enumerate(
torrent.files if torrent.mode == "multifile" else [torrent.name]
):
filename = str(file).split("/")[-1]
parsed_data = PTN.parse(filename)
file_data = [
file_data.append(
{
"filename": filename,
"size": total_size,
"index": 0,
"size": file.size if torrent.mode == "multifile" else total_size,
"index": idx,
"season": parsed_data.get("season"),
"episode": parsed_data.get("episode"),
}
]
announce_list = [
tracker[0].decode() for tracker in torrent_data.get(b"announce-list", [])
]
torrent_name = info.get(b"name", b"").decode() or file_data[0]["filename"]
)
return {
**PTN.parse(torrent.name),
"info_hash": info_hash,
"announce_list": announce_list,
"announce_list": sorted(list(torrent.trackers.flat)),
"total_size": total_size,
"file_data": file_data,
"torrent_name": torrent_name,
"torrent_name": torrent.name,
}
except Exception as e:
logging.error(f"Error occurred: {e}")
return {}
def convert_info_hash_to_magnet(info_hash: str, trackers: list[str]) -> str:
async def convert_info_hash_to_magnet(info_hash: str, trackers: list[str]) -> str:
magnet_link = f"magnet:?xt=urn:btih:{info_hash}"
for tracker in trackers or TRACKERS:
for tracker in set(trackers) or TRACKERS:
encoded_tracker = quote(tracker, safe="")
magnet_link += f"&tr={encoded_tracker}"
return magnet_link
T = TypeVar("T")
@asynccontextmanager
async def acollect(
coros: Iterable[Awaitable[T]],
limit: Optional[CapacityLimiter] = None,
timeout: int = 30,
) -> AsyncIterator[AsyncIterator[T]]:
async with create_task_group() as tg:
sender, receiver = create_memory_object_stream[T]()
async with sender:
for c in coros:
tg.start_soon(_acollect_pipe, c, limit, sender.clone(), timeout)
async with receiver:
yield receiver
async def _acollect_pipe(
coro: Awaitable[T],
limit: Optional[CapacityLimiter],
sender: MemoryObjectSendStream[T],
timeout: int,
) -> None:
async with AsyncExitStack() as stack:
if limit is not None:
await stack.enter_async_context(limit)
await stack.enter_async_context(sender)
try:
with anyio.fail_after(timeout):
value = await coro
await sender.send(value)
except Exception as e:
# Send the exception instead of the value
await sender.send(e)
async def info_hashes_to_torrent_metadata(
info_hashes: list[str], trackers: list[str]
) -> list[dict]:
magnets = [
Magnet(xt=info_hash, tr=trackers or TRACKERS) for info_hash in info_hashes
]
torrents_data = []
demagnetizer = Demagnetizer()
async with acollect(
[demagnetizer.demagnetize(magnet) for magnet in magnets], timeout=60
) as async_iterator:
async for torrent_result in async_iterator:
try:
if isinstance(torrent_result, Exception):
pass
else:
torrents_data.append(extract_torrent_metadata(torrent_result))
except Exception as e:
logging.error(f"Error processing torrent: {e}")
return torrents_data
async def init_best_trackers():
# get the best trackers from https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt
global TRACKERS
async with httpx.AsyncClient() as client:
response = await client.get(
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt"
)
if response.status_code == 200:
trackers = [tracker for tracker in response.text.split("\n") if tracker]
TRACKERS.extend(trackers)
TRACKERS = list(set(TRACKERS))
logging.info(f"Loaded {len(trackers)} trackers. Total: {len(TRACKERS)}")
else:
logging.error(f"Failed to load trackers: {response.status_code}")