Re-design the db structure & code flow for new add-on MediaFusion

This commit is contained in:
mhdzumair
2023-10-04 14:04:28 +05:30
parent b5662c899f
commit 14866569ea
28 changed files with 1695 additions and 898 deletions
+23
View File
@@ -0,0 +1,23 @@
FROM mcr.microsoft.com/playwright/python:v1.38.0-jammy
# Install python 3.11
RUN apt-get update && apt-get install -y python3.11 python3-pip
RUN pip install pipenv
WORKDIR /mediafusion
# Install Python dependencies
COPY Pipfile Pipfile.lock ./
RUN pipenv install
# Copy the rest of the code
COPY . .
ARG PORT=8000
ENV PORT=$PORT
# Run the server
CMD ["pipenv", "run", "uvicorn", "--host", "0.0.0.0", "--port", "${PORT}", "app.main:app"]
+3
View File
@@ -22,6 +22,9 @@ bencodepy = "*"
pydantic-settings = "*"
pycryptodome = "*"
seedrcc = "*"
parse-torrent-title = {editable = true, git = "https://github.com/mhdzumair/parse-torrent-title"}
playwright = "==1.38.0"
playwright-stealth = "*"
[dev-packages]
Generated
+335 -283
View File
@@ -1,11 +1,11 @@
{
"_meta": {
"hash": {
"sha256": "f650867c93dd3313074582e67745a241c23acffb4589187066d091dd9b8e5096"
"sha256": "0db3ee31c019d926ab8bc9af95a5d2af5aa5e01c0168273870becdc629067518"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.11"
"python_version": "3"
},
"sources": [
{
@@ -42,11 +42,11 @@
},
"beanie": {
"hashes": [
"sha256:180a6d5e854297be2da84689b03509e7dc4f40f00c58a894a17ad40a00630838",
"sha256:df1bcac1d5834730d3a1e461814fd0dfa173d91daca047b4d8ddcbf54343b4fd"
"sha256:2b03cfe43ff383526d8ec67a88d8f0e60e251f2631af180c30fb4ac1fb9d900b",
"sha256:9721e3b0dc435e5a42b64e58d4f88bb9a447eb94c489de7ceb75854f45ec2a19"
],
"index": "pypi",
"version": "==1.21.0"
"version": "==1.22.6"
},
"beautifulsoup4": {
"hashes": [
@@ -68,94 +68,109 @@
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
],
"markers": "python_full_version >= '3.6.0'",
"markers": "python_version >= '3.6'",
"version": "==2023.7.22"
},
"charset-normalizer": {
"hashes": [
"sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
"sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
"sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
"sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
"sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
"sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
"sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
"sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
"sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
"sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
"sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
"sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
"sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
"sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
"sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
"sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
"sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
"sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
"sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
"sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
"sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
"sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
"sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
"sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
"sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
"sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
"sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
"sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
"sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
"sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
"sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
"sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
"sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
"sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
"sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
"sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
"sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
"sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
"sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
"sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
"sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
"sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
"sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
"sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
"sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
"sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
"sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
"sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
"sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
"sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
"sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
"sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
"sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
"sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
"sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
"sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
"sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
"sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
"sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
"sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
"sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
"sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
"sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
"sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
"sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
"sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
"sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
"sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
"sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
"sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
"sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
"sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
"sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
"sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843",
"sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786",
"sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e",
"sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8",
"sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4",
"sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa",
"sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d",
"sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82",
"sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7",
"sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895",
"sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d",
"sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a",
"sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382",
"sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678",
"sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b",
"sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e",
"sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741",
"sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4",
"sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596",
"sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9",
"sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69",
"sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c",
"sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77",
"sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13",
"sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459",
"sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e",
"sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7",
"sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908",
"sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a",
"sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f",
"sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8",
"sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482",
"sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d",
"sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d",
"sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545",
"sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34",
"sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86",
"sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6",
"sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe",
"sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e",
"sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc",
"sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7",
"sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd",
"sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c",
"sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557",
"sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a",
"sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89",
"sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078",
"sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e",
"sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4",
"sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403",
"sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0",
"sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89",
"sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115",
"sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9",
"sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05",
"sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a",
"sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec",
"sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56",
"sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38",
"sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479",
"sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c",
"sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e",
"sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd",
"sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186",
"sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455",
"sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c",
"sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65",
"sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78",
"sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287",
"sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df",
"sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43",
"sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1",
"sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7",
"sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989",
"sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a",
"sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63",
"sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884",
"sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649",
"sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810",
"sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828",
"sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4",
"sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2",
"sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd",
"sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5",
"sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe",
"sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293",
"sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e",
"sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e",
"sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"
],
"markers": "python_version >= '3.7'",
"version": "==3.2.0"
"version": "==3.3.0"
},
"cinemagoer": {
"editable": true,
"git": "https://github.com/cinemagoer/cinemagoer",
"ref": "e4f39f42ce3d5873f2c9bdb48ff7355179a7c1e5"
"ref": "19f024d4b8333af3c23aa50cef26c5e565110cb6"
},
"click": {
"hashes": [
@@ -183,11 +198,11 @@
},
"fastapi": {
"hashes": [
"sha256:345844e6a82062f06a096684196aaf96c1198b25c06b72c1311b882aa2d8a35d",
"sha256:5e5f17e826dbd9e9b5a5145976c5cd90bcaa61f2bf9a69aca423f2bcebe44d83"
"sha256:3270de872f0fe9ec809d4bd3d4d890c6d5cc7b9611d721d6438f9dacc8c4ef2e",
"sha256:75a11f6bfb8fc4d2bec0bd710c2d5f2829659c0e8c0afd5560fdda6ce25ec653"
],
"index": "pypi",
"version": "==0.103.1"
"version": "==0.103.2"
},
"greenlet": {
"hashes": [
@@ -256,7 +271,7 @@
"sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1",
"sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"
],
"markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.0.2"
},
"h11": {
@@ -325,11 +340,11 @@
},
"lazy-model": {
"hashes": [
"sha256:74406bd54cf96a89b73570d8409b538c660ae4d0bfb4b30bbe46db2658a5937d",
"sha256:90dd0b26d3033fadc18f697ab9cdd66cdf8e55469f32f2d88efd92610468e2d6"
"sha256:57c0e91e171530c4fca7aebc3ac05a163a85cddd941bf7527cc46c0ddafca47c",
"sha256:5a3241775c253e36d9069d236be8378288a93d4fc53805211fd152e04cc9c342"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==0.1.0b0"
"version": "==0.2.0"
},
"lxml": {
"hashes": [
@@ -503,163 +518,193 @@
"index": "pypi",
"version": "==3.3.1"
},
"pycryptodome": {
"parse-torrent-title": {
"editable": true,
"git": "https://github.com/mhdzumair/parse-torrent-title",
"hashes": [
"sha256:01489bbdf709d993f3058e2996f8f40fee3f0ea4d995002e5968965fa2fe89fb",
"sha256:10da29526a2a927c7d64b8f34592f461d92ae55fc97981aab5bbcde8cb465bb6",
"sha256:12600268763e6fec3cefe4c2dcdf79bde08d0b6dc1813887e789e495cb9f3403",
"sha256:157c9b5ba5e21b375f052ca78152dd309a09ed04703fd3721dce3ff8ecced148",
"sha256:16bfd98dbe472c263ed2821284118d899c76968db1a6665ade0c46805e6b29a4",
"sha256:363dd6f21f848301c2dcdeb3c8ae5f0dee2286a5e952a0f04954b82076f23825",
"sha256:3811e31e1ac3069988f7a1c9ee7331b942e605dfc0f27330a9ea5997e965efb2",
"sha256:422c89fd8df8a3bee09fb8d52aaa1e996120eafa565437392b781abec2a56e14",
"sha256:4604816adebd4faf8810782f137f8426bf45fee97d8427fa8e1e49ea78a52e2c",
"sha256:4944defabe2ace4803f99543445c27dd1edbe86d7d4edb87b256476a91e9ffa4",
"sha256:51eae079ddb9c5f10376b4131be9589a6554f6fd84f7f655180937f611cd99a2",
"sha256:53aee6be8b9b6da25ccd9028caf17dcdce3604f2c7862f5167777b707fbfb6cb",
"sha256:62a1e8847fabb5213ccde38915563140a5b338f0d0a0d363f996b51e4a6165cf",
"sha256:6f4b967bb11baea9128ec88c3d02f55a3e338361f5e4934f5240afcb667fdaec",
"sha256:78d863476e6bad2a592645072cc489bb90320972115d8995bcfbee2f8b209918",
"sha256:795bd1e4258a2c689c0b1f13ce9684fa0dd4c0e08680dcf597cf9516ed6bc0f3",
"sha256:7a3d22c8ee63de22336679e021c7f2386f7fc465477d59675caa0e5706387944",
"sha256:83c75952dcf4a4cebaa850fa257d7a860644c70a7cd54262c237c9f2be26f76e",
"sha256:928078c530da78ff08e10eb6cada6e0dff386bf3d9fa9871b4bbc9fbc1efe024",
"sha256:957b221d062d5752716923d14e0926f47670e95fead9d240fa4d4862214b9b2f",
"sha256:9ad6f09f670c466aac94a40798e0e8d1ef2aa04589c29faa5b9b97566611d1d1",
"sha256:9c8eda4f260072f7dbe42f473906c659dcbadd5ae6159dfb49af4da1293ae380",
"sha256:b1d9701d10303eec8d0bd33fa54d44e67b8be74ab449052a8372f12a66f93fb9",
"sha256:b6a610f8bfe67eab980d6236fdc73bfcdae23c9ed5548192bb2d530e8a92780e",
"sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413",
"sha256:cb1be4d5af7f355e7d41d36d8eec156ef1382a88638e8032215c215b82a4b8ec",
"sha256:d1497a8cd4728db0e0da3c304856cb37c0c4e3d0b36fcbabcc1600f18504fc54",
"sha256:d20082bdac9218649f6abe0b885927be25a917e29ae0502eaf2b53f1233ce0c2",
"sha256:e8ad74044e5f5d2456c11ed4cfd3e34b8d4898c0cb201c4038fe41458a82ea27",
"sha256:f022a4fd2a5263a5c483a2bb165f9cb27f2be06f2f477113783efe3fe2ad887b",
"sha256:f21efb8438971aa16924790e1c3dba3a33164eb4000106a55baaed522c261acf",
"sha256:fc0a73f4db1e31d4a6d71b672a48f3af458f548059aa05e83022d5f61aac9c08"
"sha256:556830ea268f808a3cee3fc6e9380ab3293b13a8172567a5a39b5d858a5387af",
"sha256:9852cfba2b746683733d77d2019a24f57d5e8c2c4db395f6ff03035ea628a4bb"
],
"ref": "0ccb3aa7523063375ca8a6e3677a493ddba31e26"
},
"playwright": {
"hashes": [
"sha256:1c46a7ed7702b9f97b57737132f25e2052ef2e9541c3613d896e92739d2ea4ee",
"sha256:22e4a49d61a20a21d6a4a90891d4d08df5091f3719272d7a31c4c7f0ff436683",
"sha256:324e317c6ddc919a01e98ed182a54c88c0b6e775e91aea2996ed320b436c0f27",
"sha256:33d6500d94c5e4608d3a74372d6f50ecbebca55dc55eaee3f70b21eaf02b17aa",
"sha256:801029161725bd9a8c1ea2d29125074f7e54bfa7b0ef85c6dfb667023a0702c8",
"sha256:ce5c2d2c49c97ea856129ac895dc7277df3c877db4a998340bd08efc3696e7fb",
"sha256:d0288c8932d7f14bc231e4a6761ecf76fff879d1601cfa3b6f6aefd544468911"
],
"index": "pypi",
"version": "==3.18.0"
"version": "==1.38.0"
},
"playwright-stealth": {
"hashes": [
"sha256:b1b2bcf58eb6859aa53d42c49b91c4e27b74a6d13fc3d0c85eea513dd55efda3",
"sha256:b504d951d00fac755c7d13665a29611d415180510bd7d23f14ebc89439ba2043"
],
"index": "pypi",
"version": "==1.0.6"
},
"pycryptodome": {
"hashes": [
"sha256:0101f647d11a1aae5a8ce4f5fad6644ae1b22bb65d05accc7d322943c69a74a6",
"sha256:04dd31d3b33a6b22ac4d432b3274588917dcf850cc0c51c84eca1d8ed6933810",
"sha256:05e33267394aad6db6595c0ce9d427fe21552f5425e116a925455e099fdf759a",
"sha256:08ce3558af5106c632baf6d331d261f02367a6bc3733086ae43c0f988fe042db",
"sha256:139ae2c6161b9dd5d829c9645d781509a810ef50ea8b657e2257c25ca20efe33",
"sha256:17940dcf274fcae4a54ec6117a9ecfe52907ed5e2e438fe712fe7ca502672ed5",
"sha256:190c53f51e988dceb60472baddce3f289fa52b0ec38fbe5fd20dd1d0f795c551",
"sha256:22e0ae7c3a7f87dcdcf302db06ab76f20e83f09a6993c160b248d58274473bfa",
"sha256:3006c44c4946583b6de24fe0632091c2653d6256b99a02a3db71ca06472ea1e4",
"sha256:45430dfaf1f421cf462c0dd824984378bef32b22669f2635cb809357dbaab405",
"sha256:506c686a1eee6c00df70010be3b8e9e78f406af4f21b23162bbb6e9bdf5427bc",
"sha256:536f676963662603f1f2e6ab01080c54d8cd20f34ec333dcb195306fa7826997",
"sha256:542f99d5026ac5f0ef391ba0602f3d11beef8e65aae135fa5b762f5ebd9d3bfb",
"sha256:560591c0777f74a5da86718f70dfc8d781734cf559773b64072bbdda44b3fc3e",
"sha256:5b1986c761258a5b4332a7f94a83f631c1ffca8747d75ab8395bf2e1b93283d9",
"sha256:61bb3ccbf4bf32ad9af32da8badc24e888ae5231c617947e0f5401077f8b091f",
"sha256:7822f36d683f9ad7bc2145b2c2045014afdbbd1d9922a6d4ce1cbd6add79a01e",
"sha256:7919ccd096584b911f2a303c593280869ce1af9bf5d36214511f5e5a1bed8c34",
"sha256:7c760c8a0479a4042111a8dd2f067d3ae4573da286c53f13cf6f5c53a5c1f631",
"sha256:829b813b8ee00d9c8aba417621b94bc0b5efd18c928923802ad5ba4cf1ec709c",
"sha256:84c3e4fffad0c4988aef0d5591be3cad4e10aa7db264c65fadbc633318d20bde",
"sha256:8999316e57abcbd8085c91bc0ef75292c8618f41ca6d2b6132250a863a77d1e7",
"sha256:8c1601e04d32087591d78e0b81e1e520e57a92796089864b20e5f18c9564b3fa",
"sha256:a0ab84755f4539db086db9ba9e9f3868d2e3610a3948cbd2a55e332ad83b01b0",
"sha256:a9bcd5f3794879e91970f2bbd7d899780541d3ff439d8f2112441769c9f2ccea",
"sha256:bc35d463222cdb4dbebd35e0784155c81e161b9284e567e7e933d722e533331e",
"sha256:c1cc2f2ae451a676def1a73c1ae9120cd31af25db3f381893d45f75e77be2400",
"sha256:d033947e7fd3e2ba9a031cb2d267251620964705a013c5a461fa5233cc025270",
"sha256:d04f5f623a280fbd0ab1c1d8ecbd753193ab7154f09b6161b0f857a1a676c15f",
"sha256:d49a6c715d8cceffedabb6adb7e0cbf41ae1a2ff4adaeec9432074a80627dea1",
"sha256:e249a784cc98a29c77cea9df54284a44b40cafbfae57636dd2f8775b48af2434",
"sha256:fc7a79590e2b5d08530175823a242de6790abc73638cc6dc9d2684e7be2f5e49"
],
"index": "pypi",
"version": "==3.19.0"
},
"pydantic": {
"hashes": [
"sha256:1607cc106602284cd4a00882986570472f193fde9cb1259bceeaedb26aa79a6d",
"sha256:45b5e446c6dfaad9444819a293b921a40e1db1aa61ea08aede0522529ce90e81"
"sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7",
"sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"
],
"index": "pypi",
"version": "==2.3.0"
"version": "==2.4.2"
},
"pydantic-core": {
"hashes": [
"sha256:002d0ea50e17ed982c2d65b480bd975fc41086a5a2f9c924ef8fc54419d1dea3",
"sha256:02e1c385095efbd997311d85c6021d32369675c09bcbfff3b69d84e59dc103f6",
"sha256:046af9cfb5384f3684eeb3f58a48698ddab8dd870b4b3f67f825353a14441418",
"sha256:04fe5c0a43dec39aedba0ec9579001061d4653a9b53a1366b113aca4a3c05ca7",
"sha256:07a1aec07333bf5adebd8264047d3dc518563d92aca6f2f5b36f505132399efc",
"sha256:1480fa4682e8202b560dcdc9eeec1005f62a15742b813c88cdc01d44e85308e5",
"sha256:1508f37ba9e3ddc0189e6ff4e2228bd2d3c3a4641cbe8c07177162f76ed696c7",
"sha256:171a4718860790f66d6c2eda1d95dd1edf64f864d2e9f9115840840cf5b5713f",
"sha256:19e20f8baedd7d987bd3f8005c146e6bcbda7cdeefc36fad50c66adb2dd2da48",
"sha256:1a0ddaa723c48af27d19f27f1c73bdc615c73686d763388c8683fe34ae777bad",
"sha256:1aa712ba150d5105814e53cb141412217146fedc22621e9acff9236d77d2a5ef",
"sha256:1ac1750df1b4339b543531ce793b8fd5c16660a95d13aecaab26b44ce11775e9",
"sha256:1c721bfc575d57305dd922e6a40a8fe3f762905851d694245807a351ad255c58",
"sha256:1ce8c84051fa292a5dc54018a40e2a1926fd17980a9422c973e3ebea017aa8da",
"sha256:1fa1f6312fb84e8c281f32b39affe81984ccd484da6e9d65b3d18c202c666149",
"sha256:22134a4453bd59b7d1e895c455fe277af9d9d9fbbcb9dc3f4a97b8693e7e2c9b",
"sha256:23470a23614c701b37252618e7851e595060a96a23016f9a084f3f92f5ed5881",
"sha256:240a015102a0c0cc8114f1cba6444499a8a4d0333e178bc504a5c2196defd456",
"sha256:252851b38bad3bfda47b104ffd077d4f9604a10cb06fe09d020016a25107bf98",
"sha256:2a20c533cb80466c1d42a43a4521669ccad7cf2967830ac62c2c2f9cece63e7e",
"sha256:2dd50d6a1aef0426a1d0199190c6c43ec89812b1f409e7fe44cb0fbf6dfa733c",
"sha256:340e96c08de1069f3d022a85c2a8c63529fd88709468373b418f4cf2c949fb0e",
"sha256:3796a6152c545339d3b1652183e786df648ecdf7c4f9347e1d30e6750907f5bb",
"sha256:37a822f630712817b6ecc09ccc378192ef5ff12e2c9bae97eb5968a6cdf3b862",
"sha256:3a750a83b2728299ca12e003d73d1264ad0440f60f4fc9cee54acc489249b728",
"sha256:3c8945a105f1589ce8a693753b908815e0748f6279959a4530f6742e1994dcb6",
"sha256:3ccc13afee44b9006a73d2046068d4df96dc5b333bf3509d9a06d1b42db6d8bf",
"sha256:3f90e5e3afb11268628c89f378f7a1ea3f2fe502a28af4192e30a6cdea1e7d5e",
"sha256:4292ca56751aebbe63a84bbfc3b5717abb09b14d4b4442cc43fd7c49a1529efd",
"sha256:430ddd965ffd068dd70ef4e4d74f2c489c3a313adc28e829dd7262cc0d2dd1e8",
"sha256:439a0de139556745ae53f9cc9668c6c2053444af940d3ef3ecad95b079bc9987",
"sha256:44b4f937b992394a2e81a5c5ce716f3dcc1237281e81b80c748b2da6dd5cf29a",
"sha256:48c1ed8b02ffea4d5c9c220eda27af02b8149fe58526359b3c07eb391cb353a2",
"sha256:4ef724a059396751aef71e847178d66ad7fc3fc969a1a40c29f5aac1aa5f8784",
"sha256:50555ba3cb58f9861b7a48c493636b996a617db1a72c18da4d7f16d7b1b9952b",
"sha256:522a9c4a4d1924facce7270c84b5134c5cabcb01513213662a2e89cf28c1d309",
"sha256:5493a7027bfc6b108e17c3383959485087d5942e87eb62bbac69829eae9bc1f7",
"sha256:56ea80269077003eaa59723bac1d8bacd2cd15ae30456f2890811efc1e3d4413",
"sha256:5a2a3c9ef904dcdadb550eedf3291ec3f229431b0084666e2c2aa8ff99a103a2",
"sha256:5cfde4fab34dd1e3a3f7f3db38182ab6c95e4ea91cf322242ee0be5c2f7e3d2f",
"sha256:5e4a2cf8c4543f37f5dc881de6c190de08096c53986381daebb56a355be5dfe6",
"sha256:5e9c068f36b9f396399d43bfb6defd4cc99c36215f6ff33ac8b9c14ba15bdf6b",
"sha256:5ed7ceca6aba5331ece96c0e328cd52f0dcf942b8895a1ed2642de50800b79d3",
"sha256:5fa159b902d22b283b680ef52b532b29554ea2a7fc39bf354064751369e9dbd7",
"sha256:615a31b1629e12445c0e9fc8339b41aaa6cc60bd53bf802d5fe3d2c0cda2ae8d",
"sha256:621afe25cc2b3c4ba05fff53525156d5100eb35c6e5a7cf31d66cc9e1963e378",
"sha256:6656a0ae383d8cd7cc94e91de4e526407b3726049ce8d7939049cbfa426518c8",
"sha256:672174480a85386dd2e681cadd7d951471ad0bb028ed744c895f11f9d51b9ebe",
"sha256:692b4ff5c4e828a38716cfa92667661a39886e71136c97b7dac26edef18767f7",
"sha256:6bcc1ad776fffe25ea5c187a028991c031a00ff92d012ca1cc4714087e575973",
"sha256:6bf7d610ac8f0065a286002a23bcce241ea8248c71988bda538edcc90e0c39ad",
"sha256:75c0ebbebae71ed1e385f7dfd9b74c1cff09fed24a6df43d326dd7f12339ec34",
"sha256:788be9844a6e5c4612b74512a76b2153f1877cd845410d756841f6c3420230eb",
"sha256:7dc2ce039c7290b4ef64334ec7e6ca6494de6eecc81e21cb4f73b9b39991408c",
"sha256:813aab5bfb19c98ae370952b6f7190f1e28e565909bfc219a0909db168783465",
"sha256:8421cf496e746cf8d6b677502ed9a0d1e4e956586cd8b221e1312e0841c002d5",
"sha256:84e87c16f582f5c753b7f39a71bd6647255512191be2d2dbf49458c4ef024588",
"sha256:84f8bb34fe76c68c9d96b77c60cef093f5e660ef8e43a6cbfcd991017d375950",
"sha256:85cc4d105747d2aa3c5cf3e37dac50141bff779545ba59a095f4a96b0a460e70",
"sha256:883daa467865e5766931e07eb20f3e8152324f0adf52658f4d302242c12e2c32",
"sha256:8b2b1bfed698fa410ab81982f681f5b1996d3d994ae8073286515ac4d165c2e7",
"sha256:8ecbac050856eb6c3046dea655b39216597e373aa8e50e134c0e202f9c47efec",
"sha256:930bfe73e665ebce3f0da2c6d64455098aaa67e1a00323c74dc752627879fc67",
"sha256:9616567800bdc83ce136e5847d41008a1d602213d024207b0ff6cab6753fe645",
"sha256:9680dd23055dd874173a3a63a44e7f5a13885a4cfd7e84814be71be24fba83db",
"sha256:99faba727727b2e59129c59542284efebbddade4f0ae6a29c8b8d3e1f437beb7",
"sha256:9a718d56c4d55efcfc63f680f207c9f19c8376e5a8a67773535e6f7e80e93170",
"sha256:9b33bf9658cb29ac1a517c11e865112316d09687d767d7a0e4a63d5c640d1b17",
"sha256:9e8b374ef41ad5c461efb7a140ce4730661aadf85958b5c6a3e9cf4e040ff4bb",
"sha256:9e9b65a55bbabda7fccd3500192a79f6e474d8d36e78d1685496aad5f9dbd92c",
"sha256:a0b7486d85293f7f0bbc39b34e1d8aa26210b450bbd3d245ec3d732864009819",
"sha256:a53e3195f134bde03620d87a7e2b2f2046e0e5a8195e66d0f244d6d5b2f6d31b",
"sha256:a87c54e72aa2ef30189dc74427421e074ab4561cf2bf314589f6af5b37f45e6d",
"sha256:a892b5b1871b301ce20d40b037ffbe33d1407a39639c2b05356acfef5536d26a",
"sha256:a8acc9dedd304da161eb071cc7ff1326aa5b66aadec9622b2574ad3ffe225525",
"sha256:aaafc776e5edc72b3cad1ccedb5fd869cc5c9a591f1213aa9eba31a781be9ac1",
"sha256:acafc4368b289a9f291e204d2c4c75908557d4f36bd3ae937914d4529bf62a76",
"sha256:b0a5d7edb76c1c57b95df719af703e796fc8e796447a1da939f97bfa8a918d60",
"sha256:b25afe9d5c4f60dcbbe2b277a79be114e2e65a16598db8abee2a2dcde24f162b",
"sha256:b44c42edc07a50a081672e25dfe6022554b47f91e793066a7b601ca290f71e42",
"sha256:b594b64e8568cf09ee5c9501ede37066b9fc41d83d58f55b9952e32141256acd",
"sha256:b962700962f6e7a6bd77e5f37320cabac24b4c0f76afeac05e9f93cf0c620014",
"sha256:bb128c30cf1df0ab78166ded1ecf876620fb9aac84d2413e8ea1594b588c735d",
"sha256:bf9d42a71a4d7a7c1f14f629e5c30eac451a6fc81827d2beefd57d014c006c4a",
"sha256:c6595b0d8c8711e8e1dc389d52648b923b809f68ac1c6f0baa525c6440aa0daa",
"sha256:c8c6660089a25d45333cb9db56bb9e347241a6d7509838dbbd1931d0e19dbc7f",
"sha256:c9d469204abcca28926cbc28ce98f28e50e488767b084fb3fbdf21af11d3de26",
"sha256:d38bbcef58220f9c81e42c255ef0bf99735d8f11edef69ab0b499da77105158a",
"sha256:d4eb77df2964b64ba190eee00b2312a1fd7a862af8918ec70fc2d6308f76ac64",
"sha256:d63b7545d489422d417a0cae6f9898618669608750fc5e62156957e609e728a5",
"sha256:d7050899026e708fb185e174c63ebc2c4ee7a0c17b0a96ebc50e1f76a231c057",
"sha256:d79f1f2f7ebdb9b741296b69049ff44aedd95976bfee38eb4848820628a99b50",
"sha256:d85463560c67fc65cd86153a4975d0b720b6d7725cf7ee0b2d291288433fc21b",
"sha256:d9140ded382a5b04a1c030b593ed9bf3088243a0a8b7fa9f071a5736498c5483",
"sha256:d9b4916b21931b08096efed090327f8fe78e09ae8f5ad44e07f5c72a7eedb51b",
"sha256:df14f6332834444b4a37685810216cc8fe1fe91f447332cd56294c984ecbff1c",
"sha256:e49ce7dc9f925e1fb010fc3d555250139df61fa6e5a0a95ce356329602c11ea9",
"sha256:e61eae9b31799c32c5f9b7be906be3380e699e74b2db26c227c50a5fc7988698",
"sha256:ea053cefa008fda40f92aab937fb9f183cf8752e41dbc7bc68917884454c6362",
"sha256:f06e21ad0b504658a3a9edd3d8530e8cea5723f6ea5d280e8db8efc625b47e49",
"sha256:f14546403c2a1d11a130b537dda28f07eb6c1805a43dae4617448074fd49c282",
"sha256:f1a5d8f18877474c80b7711d870db0eeef9442691fcdb00adabfc97e183ee0b0",
"sha256:f2969e8f72c6236c51f91fbb79c33821d12a811e2a94b7aa59c65f8dbdfad34a",
"sha256:f468d520f47807d1eb5d27648393519655eadc578d5dd862d06873cce04c4d1b",
"sha256:f70dc00a91311a1aea124e5f64569ea44c011b58433981313202c46bccbec0e1",
"sha256:f93255b3e4d64785554e544c1c76cd32f4a354fa79e2eeca5d16ac2e7fdd57aa"
"sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e",
"sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33",
"sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7",
"sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7",
"sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea",
"sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4",
"sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0",
"sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7",
"sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94",
"sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff",
"sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82",
"sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd",
"sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893",
"sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e",
"sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d",
"sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901",
"sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9",
"sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c",
"sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7",
"sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891",
"sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f",
"sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a",
"sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9",
"sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5",
"sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e",
"sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a",
"sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c",
"sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f",
"sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514",
"sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b",
"sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302",
"sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096",
"sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0",
"sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27",
"sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884",
"sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a",
"sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357",
"sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430",
"sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221",
"sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325",
"sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4",
"sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05",
"sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55",
"sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875",
"sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970",
"sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc",
"sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6",
"sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f",
"sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b",
"sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d",
"sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15",
"sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118",
"sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee",
"sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e",
"sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6",
"sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208",
"sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede",
"sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3",
"sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e",
"sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada",
"sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175",
"sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a",
"sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c",
"sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f",
"sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58",
"sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f",
"sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a",
"sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a",
"sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921",
"sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e",
"sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904",
"sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776",
"sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52",
"sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf",
"sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8",
"sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f",
"sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b",
"sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63",
"sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c",
"sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f",
"sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468",
"sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e",
"sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab",
"sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2",
"sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb",
"sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb",
"sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132",
"sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b",
"sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607",
"sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934",
"sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698",
"sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e",
"sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561",
"sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de",
"sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b",
"sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a",
"sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595",
"sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402",
"sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881",
"sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429",
"sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5",
"sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7",
"sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c",
"sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531",
"sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6",
"sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"
],
"markers": "python_version >= '3.7'",
"version": "==2.6.3"
"version": "==2.10.1"
},
"pydantic-settings": {
"hashes": [
@@ -669,6 +714,13 @@
"index": "pypi",
"version": "==2.0.3"
},
"pyee": {
"hashes": [
"sha256:2770c4928abc721f46b705e6a72b0c59480c4a69c9a83ca0b00bb994f1ea4b32",
"sha256:9f066570130c554e9cc12de5a9d86f57c7ee47fece163bbdaa3e9c933cfbdfa5"
],
"version": "==9.0.4"
},
"pymongo": {
"extras": [
"srv"
@@ -895,50 +947,50 @@
},
"sqlalchemy": {
"hashes": [
"sha256:1506e988ebeaaf316f183da601f24eedd7452e163010ea63dbe52dc91c7fc70e",
"sha256:1a58052b5a93425f656675673ef1f7e005a3b72e3f2c91b8acca1b27ccadf5f4",
"sha256:1b74eeafaa11372627ce94e4dc88a6751b2b4d263015b3523e2b1e57291102f0",
"sha256:1be86ccea0c965a1e8cd6ccf6884b924c319fcc85765f16c69f1ae7148eba64b",
"sha256:1d35d49a972649b5080557c603110620a86aa11db350d7a7cb0f0a3f611948a0",
"sha256:243d0fb261f80a26774829bc2cee71df3222587ac789b7eaf6555c5b15651eed",
"sha256:26a3399eaf65e9ab2690c07bd5cf898b639e76903e0abad096cd609233ce5208",
"sha256:27d554ef5d12501898d88d255c54eef8414576f34672e02fe96d75908993cf53",
"sha256:3364b7066b3c7f4437dd345d47271f1251e0cfb0aba67e785343cdbdb0fff08c",
"sha256:3423dc2a3b94125094897118b52bdf4d37daf142cbcf26d48af284b763ab90e9",
"sha256:3c6aceebbc47db04f2d779db03afeaa2c73ea3f8dcd3987eb9efdb987ffa09a3",
"sha256:3ce5e81b800a8afc870bb8e0a275d81957e16f8c4b62415a7b386f29a0cb9763",
"sha256:411e7f140200c02c4b953b3dbd08351c9f9818d2bd591b56d0fa0716bd014f1e",
"sha256:4cde2e1096cbb3e62002efdb7050113aa5f01718035ba9f29f9d89c3758e7e4e",
"sha256:5768c268df78bacbde166b48be788b83dddaa2a5974b8810af422ddfe68a9bc8",
"sha256:599ccd23a7146e126be1c7632d1d47847fa9f333104d03325c4e15440fc7d927",
"sha256:5ed61e3463021763b853628aef8bc5d469fe12d95f82c74ef605049d810f3267",
"sha256:63a368231c53c93e2b67d0c5556a9836fdcd383f7e3026a39602aad775b14acf",
"sha256:63e73da7fb030ae0a46a9ffbeef7e892f5def4baf8064786d040d45c1d6d1dc5",
"sha256:6eb6d77c31e1bf4268b4d61b549c341cbff9842f8e115ba6904249c20cb78a61",
"sha256:6f8a934f9dfdf762c844e5164046a9cea25fabbc9ec865c023fe7f300f11ca4a",
"sha256:6fe7d61dc71119e21ddb0094ee994418c12f68c61b3d263ebaae50ea8399c4d4",
"sha256:759b51346aa388c2e606ee206c0bc6f15a5299f6174d1e10cadbe4530d3c7a98",
"sha256:76fdfc0f6f5341987474ff48e7a66c3cd2b8a71ddda01fa82fedb180b961630a",
"sha256:77d37c1b4e64c926fa3de23e8244b964aab92963d0f74d98cbc0783a9e04f501",
"sha256:79543f945be7a5ada9943d555cf9b1531cfea49241809dd1183701f94a748624",
"sha256:79fde625a0a55220d3624e64101ed68a059c1c1f126c74f08a42097a72ff66a9",
"sha256:7d3f175410a6db0ad96b10bfbb0a5530ecd4fcf1e2b5d83d968dd64791f810ed",
"sha256:8dd77fd6648b677d7742d2c3cc105a66e2681cc5e5fb247b88c7a7b78351cf74",
"sha256:a3f0dd6d15b6dc8b28a838a5c48ced7455c3e1fb47b89da9c79cc2090b072a50",
"sha256:bcb04441f370cbe6e37c2b8d79e4af9e4789f626c595899d94abebe8b38f9a4d",
"sha256:c3d99ba99007dab8233f635c32b5cd24fb1df8d64e17bc7df136cedbea427897",
"sha256:ca8a5ff2aa7f3ade6c498aaafce25b1eaeabe4e42b73e25519183e4566a16fc6",
"sha256:cb0d3e94c2a84215532d9bcf10229476ffd3b08f481c53754113b794afb62d14",
"sha256:d1b09ba72e4e6d341bb5bdd3564f1cea6095d4c3632e45dc69375a1dbe4e26ec",
"sha256:d32b5ffef6c5bcb452723a496bad2d4c52b346240c59b3e6dba279f6dcc06c14",
"sha256:d3793dcf5bc4d74ae1e9db15121250c2da476e1af8e45a1d9a52b1513a393459",
"sha256:dd81466bdbc82b060c3c110b2937ab65ace41dfa7b18681fdfad2f37f27acdd7",
"sha256:e4e571af672e1bb710b3cc1a9794b55bce1eae5aed41a608c0401885e3491179",
"sha256:ea8186be85da6587456c9ddc7bf480ebad1a0e6dcbad3967c4821233a4d4df57",
"sha256:eefebcc5c555803065128401a1e224a64607259b5eb907021bf9b175f315d2a6"
"sha256:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e",
"sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6",
"sha256:05b971ab1ac2994a14c56b35eaaa91f86ba080e9ad481b20d99d77f381bb6258",
"sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce",
"sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede",
"sha256:2e617727fe4091cedb3e4409b39368f424934c7faa78171749f704b49b4bb4ce",
"sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4",
"sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4",
"sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5",
"sha256:419b1276b55925b5ac9b4c7044e999f1787c69761a3c9756dec6e5c225ceca01",
"sha256:44ac5c89b6896f4740e7091f4a0ff2e62881da80c239dd9408f84f75a293dae9",
"sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9",
"sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67",
"sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9",
"sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9",
"sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301",
"sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8",
"sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615",
"sha256:7ca38746eac23dd7c20bec9278d2058c7ad662b2f1576e4c3dbfcd7c00cc48fa",
"sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4",
"sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446",
"sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4",
"sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178",
"sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af",
"sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b",
"sha256:b19ae41ef26c01a987e49e37c77b9ad060c59f94d3b3efdfdbf4f3daaca7b5fe",
"sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9",
"sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09",
"sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a",
"sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063",
"sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef",
"sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1",
"sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66",
"sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231",
"sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e",
"sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec",
"sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430",
"sha256:ea7da25ee458d8f404b93eb073116156fd7d8c2a776d8311534851f28277b4ce",
"sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9",
"sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa",
"sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.20"
"version": "==2.0.21"
},
"starlette": {
"hashes": [
@@ -958,11 +1010,11 @@
},
"typing-extensions": {
"hashes": [
"sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36",
"sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"
"sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
"sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
],
"markers": "python_version >= '3.7'",
"version": "==4.7.1"
"markers": "python_version >= '3.8'",
"version": "==4.8.0"
},
"tzlocal": {
"hashes": [
@@ -974,11 +1026,11 @@
},
"urllib3": {
"hashes": [
"sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11",
"sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"
"sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2",
"sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.4"
"version": "==2.0.6"
},
"uvicorn": {
"extras": [
+45 -14
View File
@@ -1,25 +1,54 @@
# Disclaimer :warning: :warning: :warning:
# Media Fusion Stremio Addon :clapper:
> The contents of these scripts and files are created for Educational Purpose. Use of the Add-on is at your own risk.
> This scrapper is written in python and serves API for [stremio](https://www.stremio.com/).
> All the credits goes to Tamil Blasters. I encourage you to use the website.
![Media Fusion Logo](resources/images/mediafusion_logo.png?raw=true)
## :warning: Disclaimer :warning:
![alt TamilBlasters](resources/images/tamilblasters.png?raw=true)
> The content of this script is created strictly for educational purposes. Use of the Add-on is at your own risk.
> This Add-on, written in Python, serves as an API for [stremio](https://www.stremio.com/).
> There is no affiliation with any scraping sites.
# Tamil Blasters Stremio Addon
## :sparkles: Features
Stremio Addon for Tamil Blasters Movies & Series.
![alt TamilBlasters](resources/images/ss.png?raw=true)
- Provides catalogs for multiple languages: Tamil, Hindi, Malayalam, Kannada, English, and dubbed movies & series.
![Media Fusion Catalog](resources/images/ss1.png?raw=true)
## Installation
- Supports streams for playback with torrent, Real Debrid, and Seedr integration.
![Media Fusion Streams](resources/images/ss2.png?raw=true)
1. Install from home page: https://882b9915d0fe-stremio-tamilblasters.baby-beamup.club
2. You can search on the third-party addon page as "Tamil Blasters"
3. enter this link in stremio search bar to
install: https://882b9915d0fe-stremio-tamilblasters.baby-beamup.club/manifest.json
## :rocket: Installation
# References
1. Install Stremio from [here](https://www.stremio.com/downloads).
2. Navigate to [Media Fusion](https://882b9915d0fe-mediafusion.baby-beamup.club) and click on the 'Configure Add-on' button.
## :hammer_and_wrench: Development
### Prerequisites
- **Python**: This project uses Python version 3.11. Ensure you have it installed.
- **MongoDB**: Set up a MongoDB server. You can use [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) to create a free MongoDB cluster.
### Setup
1. **Pipenv**: Use [Pipenv](https://pipenv.pypa.io/en/latest/) for managing project dependencies. If you don't have it installed, you can install it with:
```bash
pip install pipenv
```
2. **Clone**: Clone this repository.
```bash
git clone https://github.com/mhdzumair/MediaFusion
```
3. **Environment Variables**: Create a `.env` file in the root directory with the following variables:
```bash
MONGO_URI=<Your_MongoDB_URI>
SECRET_KEY=<Your_Random_32_Character_Secret>
HOST_URL=http://127.0.0.1:8000
```
4. **For scraping instructions**: refer to the [scrapping README](/scrappers/README.md).
## :books: References
- [Stremio Generic Add-on Guide](https://stremio.github.io/stremio-addon-guide/basics)
- [Stremio Add-on SDK API docs](https://github.com/Stremio/stremio-addon-sdk/tree/master/docs/api)
@@ -28,3 +57,5 @@ Stremio Addon for Tamil Blasters Movies & Series.
- [beautifulsoup4](https://beautiful-soup-4.readthedocs.io/en/latest/)
- [cinemagoer](https://cinemagoer.readthedocs.io/en/latest/)
- [beanie](https://roman-right.github.io/beanie/)
- [parse-torrent-title](https://github.com/platelminto/parse-torrent-title)
- [torrentio-scraper Stremio Add-on](https://github.com/TheBeastLT/torrentio-scraper)
+120 -62
View File
@@ -1,5 +1,7 @@
import json
import logging
from typing import Literal, Optional
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from fastapi import FastAPI, Request, Response, BackgroundTasks, Depends, HTTPException
@@ -7,22 +9,22 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from typing import Literal
from urllib.parse import quote
from db import database, crud, schemas
from db.config import settings
from scrappers import tamil_blasters_scrapper as tb_scrapper
from streaming_providers.exceptions import ProviderException
from streaming_providers.realdebrid.api import router as realdebrid_router
from streaming_providers.realdebrid.utils import get_direct_link_from_realdebrid
from streaming_providers.seedr.api import router as seedr_router
from streaming_providers.seedr.utils import get_direct_link_from_seedr
from scrappers import tamil_blasters_scrapper as tb_scrapper
from utils import crypto, torrent
from utils.parser import generate_catalog_ids, clean_name
from utils.parser import generate_catalog_ids
logging.basicConfig(
format="%(levelname)s::%(asctime)s - %(message)s", datefmt="%d-%b-%y %H:%M:%S", level=settings.logging_level
format="%(levelname)s::%(asctime)s - %(message)s",
datefmt="%d-%b-%y %H:%M:%S",
level=settings.logging_level,
)
app = FastAPI()
@@ -65,12 +67,12 @@ async def stop_scheduler():
app.state.scheduler.shutdown(wait=False)
@app.get("/")
@app.get("/", tags=["home"])
async def get_home(request: Request):
with open("resources/manifest.json") as file:
manifest = json.load(file)
return TEMPLATES.TemplateResponse(
"home.html",
"html/home.html",
{
"request": request,
"name": manifest.get("name"),
@@ -85,47 +87,81 @@ async def get_home(request: Request):
"English Movies & Series",
"Dubbed Movies & Series",
],
"logo": "static/tamilblasters.png",
"logo": "static/images/mediafusion_logo.png",
},
)
@app.get("/favicon.ico")
async def get_favicon():
return FileResponse("resources/tamilblasters.png", media_type="image/x-icon")
return FileResponse(
"resources/images/mediafusion_logo.png", media_type="image/x-icon"
)
@app.get("/configure")
@app.get("/{secret_str}/configure")
@app.get("/configure", tags=["configure"])
@app.get("/{secret_str}/configure", tags=["configure"])
async def configure(
response: Response, request: Request, user_data: schemas.UserData = Depends(crypto.decrypt_user_data)
response: Response,
request: Request,
user_data: schemas.UserData = Depends(crypto.decrypt_user_data),
):
response.headers.update(headers)
response.headers.update(no_cache_headers)
if user_data.streaming_provider:
user_data.streaming_provider.token = None
return TEMPLATES.TemplateResponse("configure.html", {"request": request, "user_data": user_data.model_dump()})
return TEMPLATES.TemplateResponse(
"html/configure.html", {"request": request, "user_data": user_data.model_dump()}
)
@app.get("/manifest.json")
@app.get("/{secret_str}/manifest.json")
async def get_manifest(response: Response, user_data: schemas.UserData = Depends(crypto.decrypt_user_data)):
@app.get("/manifest.json", tags=["manifest"])
@app.get("/{secret_str}/manifest.json", tags=["manifest"])
async def get_manifest(
response: Response, user_data: schemas.UserData = Depends(crypto.decrypt_user_data)
):
response.headers.update(headers)
response.headers.update(no_cache_headers)
with open("resources/manifest.json") as file:
manifest = json.load(file)
user_catalog_ids = generate_catalog_ids(user_data.preferred_movie_languages, user_data.preferred_series_languages)
filtered_catalogs = [cat for cat in manifest["catalogs"] if cat["id"] in user_catalog_ids]
user_catalog_ids = generate_catalog_ids(
user_data.preferred_movie_languages, user_data.preferred_series_languages
)
filtered_catalogs = [
cat for cat in manifest["catalogs"] if cat["id"] in user_catalog_ids
]
manifest["catalogs"] = filtered_catalogs
return manifest
@app.get("/{secret_str}/catalog/{catalog_type}/{catalog_id}.json", response_model=schemas.Movie)
@app.get("/catalog/{catalog_type}/{catalog_id}.json", response_model=schemas.Movie)
@app.get("/{secret_str}/catalog/{catalog_type}/{catalog_id}/skip={skip}.json", response_model=schemas.Movie)
@app.get("/catalog/{catalog_type}/{catalog_id}/skip={skip}.json", response_model=schemas.Movie)
@app.get(
"/{secret_str}/catalog/{catalog_type}/{catalog_id}.json",
response_model=schemas.Metas,
response_model_exclude_none=True,
response_model_by_alias=False,
tags=["catalog"],
)
@app.get(
"/catalog/{catalog_type}/{catalog_id}.json",
response_model=schemas.Metas,
response_model_exclude_none=True,
response_model_by_alias=False,
tags=["catalog"],
)
@app.get(
"/{secret_str}/catalog/{catalog_type}/{catalog_id}/skip={skip}.json",
response_model=schemas.Metas,
response_model_exclude_none=True,
response_model_by_alias=False,
tags=["catalog"],
)
@app.get(
"/catalog/{catalog_type}/{catalog_id}/skip={skip}.json",
response_model=schemas.Metas,
response_model_exclude_none=True,
response_model_by_alias=False,
tags=["catalog"],
)
async def get_catalog(
response: Response,
catalog_type: Literal["movie", "series"],
@@ -133,13 +169,21 @@ async def get_catalog(
skip: int = 0,
):
response.headers.update(headers)
movies = schemas.Movie()
movies.metas.extend(await crud.get_movies_and_series_meta(catalog_id, skip))
return movies
metas = schemas.Metas()
metas.metas.extend(await crud.get_meta_list(catalog_type, catalog_id, skip))
return metas
@app.get("/{secret_str}/catalog/{catalog_type}/tamil_blasters/search={search_query}.json", response_model=schemas.Movie)
@app.get("/catalog/{catalog_type}/tamil_blasters/search={search_query}.json", response_model=schemas.Movie)
@app.get(
"/{secret_str}/catalog/{catalog_type}/tamil_blasters/search={search_query}.json",
response_model=schemas.Metas,
tags=["search"],
)
@app.get(
"/catalog/{catalog_type}/tamil_blasters/search={search_query}.json",
response_model=schemas.Metas,
tags=["search"],
)
async def search_movie(
response: Response,
catalog_type: Literal["movie", "series"],
@@ -151,9 +195,11 @@ async def search_movie(
return await crud.process_search_query(search_query, catalog_type)
@app.get("/{secret_str}/meta/{catalog_type}/{meta_id}.json")
@app.get("/meta/{catalog_type}/{meta_id}.json")
async def get_meta(catalog_type: Literal["movie", "series"], meta_id: str, response: Response):
@app.get("/{secret_str}/meta/{catalog_type}/{meta_id}.json", tags=["meta"])
@app.get("/meta/{catalog_type}/{meta_id}.json", tags=["meta"])
async def get_meta(
catalog_type: Literal["movie", "series"], meta_id: str, response: Response
):
response.headers.update(headers)
if catalog_type == "movie":
return await crud.get_movie_meta(meta_id)
@@ -164,17 +210,25 @@ async def get_meta(catalog_type: Literal["movie", "series"], meta_id: str, respo
"/{secret_str}/stream/{catalog_type}/{video_id}.json",
response_model=schemas.Streams,
response_model_exclude_none=True,
tags=["stream"],
)
@app.get(
"/stream/{catalog_type}/{video_id}.json",
response_model=schemas.Streams,
response_model_exclude_none=True,
tags=["stream"],
)
@app.get("/stream/{catalog_type}/{video_id}.json", response_model=schemas.Streams, response_model_exclude_none=True)
@app.get(
"/{secret_str}/stream/{catalog_type}/{video_id}:{season}:{episode}.json",
response_model=schemas.Streams,
response_model_exclude_none=True,
tags=["stream"],
)
@app.get(
"/stream/{catalog_type}/{video_id}:{season}:{episode}.json",
response_model=schemas.Streams,
response_model_exclude_none=True,
tags=["stream"],
)
async def get_streams(
catalog_type: Literal["movie", "series"],
@@ -186,50 +240,46 @@ async def get_streams(
user_data: schemas.UserData = Depends(crypto.decrypt_user_data),
):
response.headers.update(headers)
streams = schemas.Streams()
if catalog_type == "movie":
fetched_streams = await crud.get_movie_streams(user_data, video_id)
fetched_streams = await crud.get_movie_streams(user_data, secret_str, video_id)
else:
fetched_streams = await crud.get_series_streams(user_data, video_id, season, episode)
fetched_streams = await crud.get_series_streams(
user_data, secret_str, video_id, season, episode
)
if user_data.streaming_provider:
for stream in fetched_streams:
torrent_name = quote(clean_name(f"{stream.stream_name} {stream.description}"))
base_proxy_url = (
f"{settings.host_url}/{secret_str}/streaming_provider?info_hash={stream.infoHash}&name={torrent_name}"
)
proxy_url = f"{base_proxy_url}&episode={episode}" if episode is not None else base_proxy_url
stream.url = proxy_url
stream.infoHash = None
stream.behaviorHints = {"notWebReady": True}
fetched_streams.reverse()
streams.streams.extend(fetched_streams)
return streams
return {"streams": fetched_streams}
@app.post("/scraper")
@app.post("/scraper", tags=["scraper"])
def run_scraper(
background_tasks: BackgroundTasks,
language: Literal["tamil", "malayalam", "telugu", "hindi", "kannada", "english"] = "tamil",
language: Literal[
"tamil", "malayalam", "telugu", "hindi", "kannada", "english"
] = "tamil",
video_type: Literal["hdrip", "tcrip", "dubbed", "series"] = "hdrip",
pages: int = 1,
start_page: int = 1,
is_scrape_home: bool = False,
):
background_tasks.add_task(tb_scrapper.run_scraper, language, video_type, pages, start_page, is_scrape_home)
background_tasks.add_task(
tb_scrapper.run_scraper, language, video_type, pages, start_page
)
return {"message": "Scraping in background..."}
@app.post("/encrypt-user-data")
@app.post("/encrypt-user-data", tags=["user_data"])
async def encrypt_user_data(user_data: schemas.UserData):
encrypted_str = crypto.encrypt_user_data(user_data)
return {"encrypted_str": encrypted_str}
@app.get("/{secret_str}/streaming_provider")
@app.get("/{secret_str}/streaming_provider", tags=["streaming_provider"])
async def streaming_provider_endpoint(
secret_str: str, info_hash: str, name: str, response: Response, episode: int = None
secret_str: str,
info_hash: str,
response: Response,
season: int = None,
episode: int = None,
):
response.headers.update(headers)
response.headers.update(no_cache_headers)
@@ -238,20 +288,28 @@ async def streaming_provider_endpoint(
if not user_data.streaming_provider:
raise HTTPException(status_code=400, detail="No streaming provider set.")
magnet_link = torrent.convert_info_hash_to_magnet(info_hash, name)
stream = await crud.get_stream_by_info_hash(info_hash)
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, stream.torrent_name
)
episode_data = stream.get_episode(season, episode)
try:
if user_data.streaming_provider.service == "seedr":
video_url = get_direct_link_from_seedr(
info_hash, magnet_link, user_data.streaming_provider.token, name, episode, 3, 1
video_url = await get_direct_link_from_seedr(
info_hash, magnet_link, user_data, stream, episode_data, 3, 1
)
else:
video_url = get_direct_link_from_realdebrid(
info_hash, magnet_link, user_data.streaming_provider.token, episode, 3, 1
info_hash, magnet_link, user_data, stream, episode_data, 3, 1
)
except ProviderException as error:
logging.info("Exception occurred: %s", error.message)
video_url = f"{settings.host_url}/static/{error.video_file_name}"
video_url = f"{settings.host_url}/static/exceptions/{error.video_file_name}"
return RedirectResponse(url=video_url, headers=response.headers)
+11 -8
View File
@@ -12,17 +12,17 @@ Accept: application/json
###
GET http://127.0.0.1:8000/catalog/movie/tamil_hd.json
GET http://127.0.0.1:8000/catalog/movie/tamil_hdrip.json
Accept: application/json
###
GET http://127.0.0.1:8000/catalog/movie/tamil_new/skip=10.json
GET http://127.0.0.1:8000/catalog/movie/tamil_hdrip/skip=10.json
Accept: application/json
###
GET http://127.0.0.1:8000/catalog/movie/tamil_dubbed.json
GET http://127.0.0.1:8000/{{secret_str}}/catalog/series/tamil_series.json
Accept: application/json
###
@@ -32,22 +32,25 @@ Accept: application/json
###
###
GET http://127.0.0.1:8000/{{secret_str}}/stream/movie/tt11468258.json
Accept: application/json
###
GET http://127.0.0.1:8000/meta/series/tb167521139113435.json
GET http://127.0.0.1:8000/{{secret_str}}/meta/series/mf39644457048721.json
Accept: application/json
###
GET http://127.0.0.1:8000/{{secret_str}}/stream/series/tb167521139113435:2:132.json
GET http://127.0.0.1:8000/meta/series/mf113075207220135:1:1.json
Accept: application/json
###
GET http://127.0.0.1:8000/meta/movie/ty219661034922033.json
GET http://127.0.0.1:8000/{{secret_str}}/stream/series/mf113075207220135:1:1.json
Accept: application/json
###
GET http://127.0.0.1:8000/meta/movie/tt11468258.json
Accept: application/json
+1 -1
View File
@@ -5,7 +5,7 @@ class Settings(BaseSettings):
mongo_uri: str
git_rev: str = "beta"
secret_key: str
host_url: str = "https://882b9915d0fe-stremio-tamilblasters.baby-beamup.club"
host_url: str = "https://882b9915d0fe-mediafusion.baby-beamup.club"
logging_level: str = "INFO"
class Config:
+310 -144
View File
@@ -2,220 +2,386 @@ import logging
from typing import Optional
from uuid import uuid4
from beanie import WriteRules
from imdb import Cinemagoer, IMDbDataAccessError
from db import schemas
from db.models import TamilBlasterMovie
from db.schemas import Stream
from utils.parser import extract_stream_details
from db.models import (
MediaFusionMovieMetaData,
MediaFusionSeriesMetaData,
Streams,
Season,
Episode,
MediaFusionMetaData,
)
from db.schemas import Stream, MetaIdProjection
from utils.parser import extract_stream_details, get_catalogs
ia = Cinemagoer()
async def get_movies_and_series_meta(catalog: str, skip: int = 0, limit: int = 100) -> list[schemas.Meta]:
movies_meta = []
movies = (
await TamilBlasterMovie.find(TamilBlasterMovie.catalog == catalog)
.sort("-created_at")
.skip(skip)
.limit(limit)
.to_list()
)
async def get_meta_list(
catalog_type: str, catalog: str, skip: int = 0, limit: int = 100
) -> list[schemas.Meta]:
unique_names = []
pipeline = [
# Lookup to join with the Streams collection
{
"$lookup": {
"from": "Streams",
"localField": "streams.$id",
"foreignField": "_id",
"as": "joined_streams",
}
},
# Unwind the joined streams to process each stream separately
{"$unwind": "$joined_streams"},
# Match the series based on catalog
{"$match": {"joined_streams.catalog": catalog}},
# Group by series ID and determine the latest date
{
"$group": {
"_id": "$_id",
"latest_date": {"$max": "$joined_streams.created_at"},
"title": {"$first": "$title"},
"poster": {"$first": "$poster"},
"type": {"$first": "$type"},
}
},
# Rename _id to id
{
"$project": {
"id": "$_id",
"latest_date": 1,
"title": 1,
"poster": 1,
"type": 1,
}
},
# Sort by the latest_date in descending order
{"$sort": {"latest_date": -1}},
# Pagination
{"$skip": skip},
{"$limit": limit},
]
for movie in movies:
meta_data = schemas.Meta.model_validate(movie.model_dump())
meta_data.id = movie.imdb_id if movie.imdb_id else movie.tamilblaster_id
if movie.name not in unique_names:
movies_meta.append(meta_data)
unique_names.append(movie.name)
return movies_meta
async def get_movies_data(video_id: str, video_type: str = "movie") -> list[Optional[TamilBlasterMovie]]:
if video_id.startswith("tt"):
movie_data = await TamilBlasterMovie.find(
TamilBlasterMovie.imdb_id == video_id, TamilBlasterMovie.type == video_type
if catalog_type == "movie":
meta_list = await MediaFusionMovieMetaData.aggregate(
pipeline, projection_model=schemas.Meta
).to_list()
else:
movie_data = await TamilBlasterMovie.find(
TamilBlasterMovie.tamilblaster_id == video_id, TamilBlasterMovie.type == video_type
meta_list = await MediaFusionSeriesMetaData.aggregate(
pipeline, projection_model=schemas.Meta
).to_list()
return meta_list
async def get_movie_data_by_id(
movie_id: str, fetch_links: bool = False
) -> Optional[MediaFusionMovieMetaData]:
movie_data = await MediaFusionMovieMetaData.get(movie_id, fetch_links=fetch_links)
return movie_data
async def get_movie_streams(user_data, video_id: str) -> list[Stream]:
movies_data = await get_movies_data(video_id)
if not movies_data:
async def get_series_data_by_id(
series_id: str, fetch_links: bool = False
) -> Optional[MediaFusionSeriesMetaData]:
series_data = await MediaFusionSeriesMetaData.get(
series_id, fetch_links=fetch_links
)
return series_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 []
stream_data = []
for movie_data in movies_data:
stream_data.extend(extract_stream_details(movie_data.name, movie_data.video_qualities, user_data))
return stream_data
return extract_stream_details(movie_data.streams, user_data, secret_str)
async def get_series_streams(user_data, video_id: str, season: int, episode: int) -> list[Stream]:
series_data = await get_movies_data(video_id, video_type="series")
async def get_series_streams(
user_data, secret_str: str, video_id: str, season: int, episode: int
) -> list[Stream]:
series_data = await get_series_data_by_id(video_id, True)
if not series_data:
return []
stream_data = []
for series in series_data:
if "-" in series.episode:
# Episode range detected in the database
start_ep, end_ep = map(int, series.episode.split("-"))
if start_ep <= episode <= end_ep and series.season == season:
stream_data.extend(
extract_stream_details(
f"{series.name} {series.season}:{series.episode}", series.video_qualities, user_data
)
)
elif int(series.episode) == episode and series.season == season:
stream_data.extend(
extract_stream_details(f"{series.name} {series.season}:{episode}", series.video_qualities, user_data)
)
matched_episode_streams = [
stream for stream in series_data.streams if stream.get_episode(season, episode)
]
return stream_data
return extract_stream_details(
matched_episode_streams, user_data, secret_str, season, episode
)
async def get_movie_meta(meta_id: str):
movies_data = await get_movies_data(meta_id)
if not movies_data:
return {
"meta": {
"id": meta_id,
"type": "movie",
}
}
movie_data = await get_movie_data_by_id(meta_id)
if not movie_data:
return {}
return {
"meta": {
"id": meta_id,
"type": "movie",
"name": movies_data[0].name,
"poster": movies_data[0].poster,
"background": movies_data[0].poster,
"name": movie_data.title,
"poster": movie_data.poster,
"background": movie_data.poster,
}
}
async def get_series_meta(meta_id: str):
series_data = await get_movies_data(meta_id, video_type="series")
series_data = await get_series_data_by_id(meta_id, True)
if not series_data:
return {
"meta": {
"id": meta_id,
"type": "series",
}
}
return {}
metadata = {
"meta": {
"id": meta_id,
"type": "series",
"name": series_data[0].name,
"poster": series_data[0].poster,
"background": series_data[0].poster,
"name": series_data.title,
"poster": series_data.poster,
"background": series_data.poster,
"videos": [],
}
}
for series in series_data:
if "-" in series.episode:
# Episode range detected
start_ep, end_ep = map(int, series.episode.split("-"))
for ep_num in range(start_ep, end_ep + 1):
# Loop through streams to populate the videos list
for stream in series_data.streams:
stream: Streams
if stream.season: # Ensure the stream has season data
for episode in stream.season.episodes:
metadata["meta"]["videos"].append(
{
"id": f"{meta_id}:{series.season}:{ep_num}",
"name": f"S{series.season} EP{ep_num}",
"season": series.season,
"episode": ep_num,
"released": series.created_at,
"id": f"{meta_id}:{stream.season.season_number}:{episode.episode_number}",
"name": f"S{stream.season.season_number} EP{episode.episode_number}",
"season": stream.season.season_number,
"episode": episode.episode_number,
"released": stream.created_at,
}
)
else:
episode = int(series.episode)
metadata["meta"]["videos"].append(
{
"id": f"{meta_id}:{series.season}:{episode}",
"name": f"S{series.season} EP{episode}",
"season": series.season,
"episode": episode,
"released": series.created_at,
}
)
return metadata
def search_imdb(title: str):
def search_imdb(title: str, year: int, retry: int = 5) -> dict:
try:
result = ia.search_movie(title)
result = ia.search_movie(f"{title} {year}")
except IMDbDataAccessError:
return search_imdb(title)
return search_imdb(title, year, retry - 1) if retry > 0 else {}
for movie in result:
if movie.get("title").lower() in title.lower():
return f"tt{movie.movieID}"
if movie.get("year") == year and movie.get("title").lower() in title.lower():
return {
"imdb_id": f"tt{movie.movieID}",
"poster": movie.get("full-size cover url"),
}
return {}
async def save_movie_metadata(metadata: dict):
movie_data = await TamilBlasterMovie.find_one(
TamilBlasterMovie.name == metadata["name"],
TamilBlasterMovie.catalog == metadata["catalog"],
TamilBlasterMovie.season == metadata["season"],
TamilBlasterMovie.episode == metadata["episode"],
# Try to get the existing movie
existing_movie = await MediaFusionMovieMetaData.find_one(
{"title": metadata["title"], "year": metadata.get("year")}
)
if movie_data:
movie_data.video_qualities.update(metadata["video_qualities"])
movie_data.created_at = metadata["created_at"]
logging.debug("updated video qualities for: %s", metadata["name"])
else:
movie_data = TamilBlasterMovie.model_validate(metadata)
movie_data.video_qualities = metadata["video_qualities"]
if not existing_movie:
# If the movie doesn't exist in our DB, search for IMDb ID
imdb_data = search_imdb(metadata["title"], metadata.get("year"))
meta_id = imdb_data.get("imdb_id")
series_data = await TamilBlasterMovie.find_one(
TamilBlasterMovie.name == metadata["name"],
TamilBlasterMovie.catalog == metadata["catalog"],
TamilBlasterMovie.type == "series",
)
if series_data:
movie_data.tamilblaster_id = series_data.tamilblaster_id
movie_data.imdb_id = series_data.imdb_id
if meta_id:
# Check if the movie with the found IMDb ID already exists in our DB
existing_movie = await MediaFusionMovieMetaData.get(meta_id)
else:
imdb_id = search_imdb(movie_data.name)
if any(
[
metadata["type"] == "series" and metadata["episode"].isdigit() and imdb_id,
all([metadata["type"] == "movie", imdb_id]),
]
):
movie_data.imdb_id = imdb_id
else:
movie_data.tamilblaster_id = f"tb{uuid4().fields[-1]}"
meta_id = f"mf{uuid4().fields[-1]}"
# Update the poster from IMDb if available
poster = imdb_data.get("poster") or metadata["poster"]
else:
poster = existing_movie.poster
meta_id = existing_movie.id
logging.info(f"new movie '{metadata['name']}' added.")
# Determine file index for the main movie file (largest file)
largest_file = max(
metadata["torrent_metadata"]["file_data"], key=lambda x: x["size"]
)
await movie_data.save()
if "language" in metadata:
languages = (
[metadata["language"]]
if isinstance(metadata["language"], str)
else metadata["language"]
)
else:
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"],
filename=largest_file["filename"],
file_index=largest_file["index"],
languages=languages,
resolution=metadata.get("resolution"),
codec=metadata.get("codec"),
quality=metadata.get("quality"),
audio=metadata.get("audio"),
encoder=metadata.get("encoder"),
source=metadata["source"],
catalog=get_catalogs(metadata["catalog"], languages),
created_at=metadata["created_at"],
)
if existing_movie:
# Check if the stream with the same info_hash already exists
await existing_movie.fetch_all_links()
matching_stream = next(
(stream for stream in existing_movie.streams if stream.id == new_stream.id),
None,
)
if not matching_stream:
existing_movie.streams.append(new_stream)
await existing_movie.save(link_rule=WriteRules.WRITE)
logging.info("Updated movie %s", existing_movie.title)
else:
# If the movie doesn't exist, create a new one
movie_data = MediaFusionMovieMetaData(
id=meta_id,
title=metadata["title"],
year=metadata["year"],
poster=poster,
streams=[new_stream],
)
await movie_data.insert(link_rule=WriteRules.WRITE)
logging.info("Added movie %s", movie_data.title)
async def save_series_metadata(metadata: dict):
# Try to get the existing series
series = await MediaFusionSeriesMetaData.find_one({"title": metadata["title"]})
if not series:
# If the series doesn't exist in our DB, search for IMDb ID
imdb_data = search_imdb(metadata["title"], metadata["year"])
meta_id = imdb_data.get("imdb_id")
if meta_id:
# Check if the series with the found IMDb ID already exists in our DB
series = await MediaFusionSeriesMetaData.get(meta_id)
if not series:
meta_id = meta_id or f"mf{uuid4().fields[-1]}"
poster = imdb_data.get("poster") or metadata["poster"]
# Create an initial entry for the series
series = MediaFusionSeriesMetaData(
id=meta_id,
title=metadata["title"],
year=metadata["year"],
poster=poster,
streams=[],
)
await series.insert()
logging.info("Added series %s", series.title)
await series.fetch_all_links()
existing_stream = next(
(
s
for s in series.streams
if s.id == metadata["torrent_metadata"]["info_hash"]
),
None,
)
if existing_stream:
# If the stream already exists, return
logging.info("Stream already exists for series %s", series.title)
return
season_number = metadata["season"]
# Extract episodes
episodes = [
Episode(
episode_number=file["episode"],
filename=file["filename"],
size=file["size"],
file_index=file["index"],
)
for file in metadata["torrent_metadata"]["file_data"]
if file["episode"]
]
# Determine languages
if "language" in metadata:
languages = (
[metadata["language"]]
if isinstance(metadata["language"], str)
else metadata["language"]
)
else:
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"],
languages=languages,
resolution=metadata.get("resolution"),
codec=metadata.get("codec"),
quality=metadata.get("quality"),
audio=metadata.get("audio"),
encoder=metadata.get("encoder"),
source=metadata["source"],
catalog=get_catalogs(metadata["catalog"], languages),
created_at=metadata["created_at"],
season=Season(season_number=season_number, episodes=episodes),
)
# Add the stream to the series
series.streams.append(stream)
await series.save(link_rule=WriteRules.WRITE)
logging.info("Updated series %s", series.title)
async def process_search_query(search_query: str, catalog_type: str) -> dict:
# Define the query with the text search and catalog type
query = {"$text": {"$search": search_query}, "type": catalog_type}
search_results = await TamilBlasterMovie.find(query).to_list()
logging.debug("Found %s results for %s in %s", len(search_results), search_query, catalog_type)
movies_meta = []
unique_names = []
# Perform the search
search_results = (
await MediaFusionMetaData.find(query).project(MetaIdProjection).to_list()
)
for movie in search_results:
meta_data = schemas.Meta.model_validate(movie.model_dump())
meta_data.id = movie.imdb_id or movie.tamilblaster_id
if movie.name not in unique_names:
movies_meta.append(meta_data)
unique_names.append(movie.name)
logging.debug(
"Found %s results for %s in %s", len(search_results), search_query, catalog_type
)
return {"metas": movies_meta}
metas = []
for item in search_results:
# Use the appropriate function to get the meta data
if catalog_type == "movie":
meta = await get_movie_meta(item.id)
else:
meta = await get_series_meta(item.id)
if not meta:
continue
metas.append(meta["meta"]) # Extract the 'meta' key from the result
return {"metas": metas}
async def get_stream_by_info_hash(info_hash: str) -> Streams | None:
stream = await Streams.get(info_hash)
return stream
+2 -2
View File
@@ -2,7 +2,7 @@ import motor.motor_asyncio
from beanie import init_beanie
from db.config import settings
from db.models import TamilBlasterMovie
from db.models import MediaFusionSeriesMetaData, MediaFusionMovieMetaData, Streams
async def init():
@@ -10,4 +10,4 @@ async def init():
client = motor.motor_asyncio.AsyncIOMotorClient(settings.mongo_uri)
# Init beanie with the Product document class
await init_beanie(database=client.streamio, document_models=[TamilBlasterMovie])
await init_beanie(database=client.mediafusion, document_models=[MediaFusionMovieMetaData, MediaFusionSeriesMetaData, Streams])
+61 -23
View File
@@ -2,33 +2,71 @@ from datetime import datetime
from typing import Optional
import pymongo
from beanie import Document
from pydantic import Field
from pymongo import IndexModel
from beanie import Document, Link
from pydantic import BaseModel, Field
from pymongo import IndexModel, ASCENDING
class TamilBlasterMovie(Document):
name: str
catalog: str
type: str
season: Optional[int] = None
episode: Optional[str] = None
poster: str
imdb_id: Optional[str] = None
tamilblaster_id: Optional[str] = None
class Episode(BaseModel):
episode_number: int
filename: str
size: int
file_index: int
class Season(BaseModel):
season_number: int
episodes: list[Episode]
class Streams(Document):
id: str
torrent_name: str
size: int
season: Optional[Season] = None
filename: Optional[str] = None
file_index: Optional[int] = None
announce_list: list[str]
languages: list[str]
source: str
catalog: list[str]
created_at: datetime = Field(default_factory=datetime.now)
video_qualities: dict
resolution: Optional[str]
codec: Optional[str]
quality: Optional[str]
audio: Optional[str]
encoder: Optional[str]
seeders: Optional[int] = None
def get_episode(self, season_number: int, episode_number: int) -> Optional[Episode]:
"""
Returns the Episode object for the given season and episode number.
"""
if self.season and self.season.season_number == season_number:
for episode in self.season.episodes:
if episode.episode_number == episode_number:
return episode
return None
class MediaFusionMetaData(Document):
id: str
title: str
year: Optional[int]
poster: str
streams: list[Link[Streams]]
class Settings:
is_root = True
indexes = [
IndexModel(
[
("name", pymongo.ASCENDING),
("catalog", pymongo.ASCENDING),
("season", pymongo.ASCENDING),
("episode", pymongo.ASCENDING),
],
unique=True,
),
IndexModel([("name", pymongo.TEXT)]),
IndexModel([("title", ASCENDING), ("year", ASCENDING)], unique=True),
IndexModel([("title", pymongo.TEXT)]),
]
class MediaFusionMovieMetaData(MediaFusionMetaData):
type: str = "movie"
class MediaFusionSeriesMetaData(MediaFusionMetaData):
type: str = "series"
+29 -8
View File
@@ -10,22 +10,23 @@ class Catalog(BaseModel):
class Meta(BaseModel):
id: Any
name: str
id: str
name: str = Field(alias="title")
type: str = Field(default="movie")
poster: str
videos: list | None = None
class Movie(BaseModel):
class Metas(BaseModel):
metas: list[Meta] = []
class Stream(BaseModel):
name: str | None = None
description: str | None = None
name: str
description: str
infoHash: str | None = None
fileIdx: int | None = None
url: str | None = None
stream_name: str | None = Field(exclude=True)
behaviorHints: dict[str, Any] | None = None
@@ -44,10 +45,26 @@ class StreamingProvider(BaseModel):
class UserData(BaseModel):
streaming_provider: StreamingProvider | None = None
preferred_movie_languages: list[str] = Field(
default=["Tamil", "Malayalam", "Telugu", "Hindi", "Kannada", "English", "Dubbed"]
default=[
"Tamil",
"Malayalam",
"Telugu",
"Hindi",
"Kannada",
"English",
"Dubbed",
]
)
preferred_series_languages: list[str] = Field(
default=["Tamil", "Malayalam", "Telugu", "Hindi", "Kannada", "English", "Dubbed"]
default=[
"Tamil",
"Malayalam",
"Telugu",
"Hindi",
"Kannada",
"English",
"Dubbed",
]
)
class Config:
@@ -56,3 +73,7 @@ class UserData(BaseModel):
class AuthorizeData(BaseModel):
device_code: str
class MetaIdProjection(BaseModel):
id: str
+9 -2
View File
@@ -42,6 +42,12 @@
opacity: 0.9;
}
.logo {
max-width: 300px;
display: block;
margin: 0 auto 20px;
}
button:hover {
opacity: 1;
}
@@ -57,7 +63,8 @@
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6 col-md-8 col-sm-10 config-container">
<h3 class="text-center mb-5">TamilBlasters Addon Configuration</h3>
<img class="logo" src="/static/images/mediafusion_logo.png">
<h3 class="text-center mb-5">MediaFusion Addon Configuration</h3>
<!-- Configuration Form -->
<form id="configForm">
@@ -137,7 +144,7 @@
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.3/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script src="/static/config_script.js"></script>
<script src="/static/js/config_script.js"></script>
</body>
</html>
+2 -2
View File
@@ -79,9 +79,9 @@
<img class="logo" src="{{logo}}">
<h3 class="text-center mb-5"><b>{{name}}</b> - {{version}}</h3>
<h5 class="description">
Watch Tamil, Hindi, Malayalam, Kannada, English, and dubbed movies & series with the TamilBlasters add-on.
Watch Tamil, Hindi, Malayalam, Kannada, English, and dubbed movies & series with the MediaFusion add-on.
Created by Mohamed Zumair.
<a href="https://github.com/mhdzumair/tamilblasters_streamio_addon" target="_blank">Source Code on GitHub</a>.
<a href="https://github.com/mhdzumair/MediaFusion" target="_blank">Source Code on GitHub</a>.
</h5>
<h4 class="mt-4">This add-on has:</h4>
Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+4 -4
View File
@@ -1,13 +1,13 @@
{
"id": "mhdzumair.addons.mediafusion",
"version": "1.0.0",
"version": "3.0.0",
"name": "Media Fusion",
"contactEmail": "mhdzumair@gmail",
"description": "Watch Tamil, Hindi, Malayalam, Kannada, English, and dubbed movies & series with the Media Fusion add-on. Source: https://github.com/mhdzumair/MediaFusion",
"logo": "https://882b9915d0fe-stremio-tamilblasters.baby-beamup.club/static/tamilblasters.png",
"logo": "http://127.0.0.1:8000/static/images/mediafusion_logo.png",
"behaviorHints": {
"configurable": true,
"configurationRequired": true
"configurationRequired": false
},
"resources": [
"catalog",
@@ -30,7 +30,7 @@
],
"idPrefixes": [
"tt",
"tb"
"mf"
]
}
],
+57
View File
@@ -0,0 +1,57 @@
# Scrapping with Media Fusion
Currently, the project supports scraping from TamilBlasters. Here's how to use the scraper:
1. **Setup Playwright**:
If you intend to use the scraper with Playwright, you need to ensure the Playwright browser binaries are installed. Specifically, for Firefox:
```bash
pipenv run playwright install firefox
```
2. **Get Help on Available Options **:
To understand the available options for the scraper, run:
```bash
pipenv run python3 -m scrappers.tamil_blasters_scrapper --help
```
This will display the available options:
```bash
usage: tamil_blasters_scrapper.py [-h] [--all] [-l {tamil,malayalam,telugu,hindi,kannada,english}] [-t {hdrip,tcrip,dubbed,series}] [-p PAGES] [-s START_PAGES] [-k SEARCH_KEYWORD] [--scrap-with-playwright] [--proxy-url PROXY_URL]
Scrap Movie metadata from TamilBlasters
options:
-h, --help show this help message and exit
--all scrap all type of movies & series
-l {tamil,malayalam,telugu,hindi,kannada,english}, --language {tamil,malayalam,telugu,hindi,kannada,english}
scrap movie language
-t {hdrip,tcrip,dubbed,series}, --video-type {hdrip,tcrip,dubbed,series}
scrap movie video type
-p PAGES, --pages PAGES
number of scrap pages
-s START_PAGES, --start-pages START_PAGES
page number to start scrap.
-k SEARCH_KEYWORD, --search-keyword SEARCH_KEYWORD
search keyword to scrap movies & series. ex: 'bigg boss'
--scrap-with-playwright
scrap with playwright
--proxy-url PROXY_URL
proxy url to scrap. ex: socks5://127.0.0.1:1080
```
3. **Run the Scraper**:
Execute the scraper with the desired options based on your requirements.
e.g. To scrap all movies & series, run:
```bash
pipenv run python3 -m scrappers.tamil_blasters_scrapper --all --scrap-with-playwright
```
Note: You may have to solve the cloudflare validation challenge manually when its required.
+90
View File
@@ -0,0 +1,90 @@
import logging
import cloudscraper
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from db import crud
from utils.torrent import extract_torrent_metadata
def get_scrapper_session(proxy_url=None):
session = requests.session()
session.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
}
adapter = HTTPAdapter(
max_retries=Retry(total=10, read=10, connect=10, backoff_factor=0.5)
)
session.mount("http://", adapter)
session.mount("https://", adapter)
if proxy_url:
session.proxies = {
"http": proxy_url,
"https": proxy_url,
}
scraper = cloudscraper.create_scraper(
browser={"browser": "chrome", "platform": "windows", "mobile": False},
delay=10,
sess=session,
)
return scraper
async def check_cloudflare_validation(page):
if await page.title() == "Just a moment...":
logging.info("Cloudflare validation required")
await page.wait_for_selector("a#elUserSignIn", timeout=9999999)
async def get_page_content(page, url):
await page.goto(url)
await check_cloudflare_validation(page)
return await page.content()
async def download_and_save_torrent(
torrent_element,
metadata: dict,
media_type: str,
page_link: str,
scraper=None,
page=None,
):
torrent_link = torrent_element.get("href")
logging.info(f"Downloading torrent: {torrent_link}")
if scraper:
response = scraper.get(torrent_link)
torrent_metadata = extract_torrent_metadata(response.content)
elif page:
async with page.expect_download() as download_info:
try:
await page.goto(torrent_link)
except:
pass
download = await download_info.value
torrent_path = await download.path()
with open(torrent_path, "rb") as torrent_file:
torrent_metadata = extract_torrent_metadata(torrent_file.read())
if not torrent_metadata:
logging.error(f"Info hash not found for {torrent_link}")
return False
metadata.update({"torrent_metadata": torrent_metadata})
# Saving the metadata
if media_type == "series":
if not metadata.get("season"):
logging.error(f"Season not found for {page_link}")
return False
await crud.save_series_metadata(metadata)
else:
if metadata.get("season"):
await crud.save_series_metadata(metadata)
else:
await crud.save_movie_metadata(metadata)
return True
+293 -155
View File
@@ -3,169 +3,241 @@
import argparse
import asyncio
import logging
import math
import random
import re
import cloudscraper
import requests
import PTN
from bs4 import BeautifulSoup
from dateutil.parser import parse as dateparser
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async
from db import database, crud
from utils.site_data import HOMEPAGE, TAMIL_BLASTER_LINKS
from utils.torrent import get_info_hash_from_url
from db import database
from scrappers.helpers import (
get_page_content,
get_scrapper_session,
download_and_save_torrent,
)
HOMEPAGE = "https://www.tamilblasters.life"
TAMIL_BLASTER_LINKS = {
"tamil": {
"hdrip": "7-tamil-new-movies-hdrips-bdrips-dvdrips-hdtv",
"tcrip": "8-tamil-new-movies-tcrip-dvdscr-hdcam-predvd",
"dubbed": "9-tamil-dubbed-movies-bdrips-hdrips-dvdscr-hdcam-in-multi-audios",
"series": "63-tamil-new-web-series-tv-shows",
},
"malayalam": {
"tcrip": "75-malayalam-new-movies-tcrip-dvdscr-hdcam-predvd",
"hdrip": "74-malayalam-new-movies-hdrips-bdrips-dvdrips-hdtv",
"dubbed": "76-malayalam-dubbed-movies-bdrips-hdrips-dvdscr-hdcam",
"series": "98-malayalam-new-web-series-tv-shows",
},
"telugu": {
"tcrip": "79-telugu-new-movies-tcrip-dvdscr-hdcam-predvd",
"hdrip": "78-telugu-new-movies-hdrips-bdrips-dvdrips-hdtv",
"dubbed": "80-telugu-dubbed-movies-bdrips-hdrips-dvdscr-hdcam",
"series": "96-telugu-new-web-series-tv-shows",
},
"hindi": {
"tcrip": "87-hindi-new-movies-tcrip-dvdscr-hdcam-predvd",
"hdrip": "86-hindi-new-movies-hdrips-bdrips-dvdrips-hdtv",
"dubbed": "88-hindi-dubbed-movies-bdrips-hdrips-dvdscr-hdcam",
"series": "forums/forum/89-hindi-new-web-series-tv-shows",
},
"kannada": {
"tcrip": "83-kannada-new-movies-tcrip-dvdscr-hdcam-predvd",
"hdrip": "82-kannada-new-movies-hdrips-bdrips-dvdrips-hdtv",
"dubbed": "84-kannada-dubbed-movies-bdrips-hdrips-dvdscr-hdcam",
"series": "103-kannada-new-web-series-tv-shows",
},
"english": {
"tcrip": "52-english-movies-hdcam-dvdscr-predvd",
"hdrip": "53-english-movies-hdrips-bdrips-dvdrips",
"series": "92-english-web-series-tv-shows",
},
}
def get_scrapper_session():
session = requests.session()
session.headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 7.1.2; MI 5X; Flow) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/347.0.0.268 Mobile Safari/537.36"
}
adapter = HTTPAdapter(max_retries=Retry(total=10, read=10, connect=10, backoff_factor=0.3))
session.mount("http://", adapter)
session.mount("https://", adapter)
scraper = cloudscraper.create_scraper(
browser={"browser": "chrome", "platform": "android", "desktop": False}, delay=10, sess=session
)
return scraper
async def get_search_results(page, keyword, page_number=1):
search_link = f"{HOMEPAGE}/index.php?/search/&q={keyword}&type=forums_topic&page={page_number}&search_and_or=or&search_in=titles&sortby=relevancy"
# Get page content and initialize BeautifulSoup
page_content = await get_page_content(page, search_link)
soup = BeautifulSoup(page_content, "html.parser")
return soup
def extract_info_hash(movie_page):
try:
magnet_link = movie_page.find("a", class_="magnet-plugin").get("href")
info_hash = re.search(r"urn:btih:(.{32,40})&", magnet_link).group(1)
return info_hash
except AttributeError:
logging.warning("magnet link not found")
try:
torrent_link = movie_page.select_one("a[data-fileext='torrent']").get("href")
return get_info_hash_from_url(torrent_link)
except AttributeError:
logging.warning("Torrent link not found either")
except TypeError:
logging.error("Not able to parse magnet link")
async def process_movie(
movie,
scraper=None,
page=None,
keyword=None,
language=None,
media_type=None,
supported_forums=None,
):
if keyword:
movie_link = movie.find("a", {"data-linktype": "link"})
forum_link = movie.find("a", href=re.compile(r"forums/forum/")).get("href")
forum_id = re.search(r"forums/forum/([^/]+)/", forum_link)[1]
if forum_id not in supported_forums:
logging.error(f"Unsupported forum {forum_id}")
return
# Extracting language and media_type from supported_forums
language = supported_forums[forum_id]["language"]
media_type = supported_forums[forum_id]["media_type"]
else:
movie_link = movie.find("a")
async def scrap_page(url, language, video_type):
scraper = get_scrapper_session()
response = scraper.get(url)
response.raise_for_status()
tamil_blasters = BeautifulSoup(response.content, "html.parser")
try:
movies = tamil_blasters.find("ol").select("li[data-rowid]")
except AttributeError:
logging.error(f"No data found for {language}:{video_type}")
if not movie_link:
logging.error(f"Movie link not found")
return
for movie in movies:
movie = movie.find("a")
season = episode = None
try:
if video_type == "series":
data = re.search(
r"^(.+\(\d{4}\)).*S(\d+).*EP?\s?(\d+|\(\d+\s?-\s?\d+\))(.+)",
movie.text.strip(),
)
title, season, video_quality = (
data[1],
int(data[2]),
data[4].strip("[] "),
)
episode = str(int(data[3])) if data[3].isdigit() else data[3].strip("()")
metadata = {"type": "series"}
else:
data = re.search(r"^(.+\(\d{4}\))(.+)", movie.text.strip())
title, video_quality = re.sub(r"\s+", " ", data[1].strip()), data[2].strip("[] ")
metadata = {"type": "movie"}
except TypeError:
logging.error(f"not able to parse: {movie.text.strip()}")
continue
logging.info(f"getting movie data for '{title}'")
metadata = PTN.parse(re.sub(r"\s+", " ", movie_link.text))
page_link = movie_link.get("href")
page_link = movie.get("href")
response = scraper.get(page_link)
movie_page = BeautifulSoup(response.content, "html.parser")
info_hash = extract_info_hash(movie_page)
if not info_hash:
logging.error(f"info hash not found for {page_link}")
continue
try:
if scraper: # If using the scraper
response = scraper.get(page_link)
movie_page_content = response.content
else: # If using playwright
movie_page_content = await get_page_content(page, page_link)
poster = movie_page.select_one("div[data-commenttype='forums'] img[data-src]").get("data-src")
created_at = dateparser(movie_page.find("time").get("datetime"))
movie_page = BeautifulSoup(movie_page_content, "html.parser")
# Extracting other details
poster_element = movie_page.select_one(
"div[data-commenttype='forums'] img[data-src]"
)
poster = poster_element.get("data-src") if poster_element else None
datetime_element = movie_page.select_one("time")
created_at = (
dateparser(datetime_element.get("datetime")) if datetime_element else None
)
# Updating metadata
metadata.update(
{
"name": title,
"catalog": f"{language}_{video_type}",
"video_qualities": {video_quality: info_hash},
"catalog": f"{language}_{media_type}",
"poster": poster,
"created_at": created_at,
"season": season,
"episode": episode,
"scrap_language": language.title(),
"source": "TamilBlasters",
}
)
await crud.save_movie_metadata(metadata)
# Extracting torrent details
torrent_elements = movie_page.select("a[data-fileext='torrent']")
if not torrent_elements:
logging.error(f"No torrents found for {page_link}")
return
for torrent_element in torrent_elements:
await download_and_save_torrent(
torrent_element,
scraper=scraper,
page=page,
metadata=metadata,
media_type=media_type,
page_link=page_link,
)
return True
except Exception as e:
logging.error(
f"Error processing movie {page_link}: {e}", exc_info=True, stack_info=True
)
return False
async def scrap_homepage():
scraper = get_scrapper_session()
response = scraper.get(HOMEPAGE)
async def scrap_page(url, language, media_type, proxy_url=None):
scraper = get_scrapper_session(proxy_url)
response = scraper.get(url)
if response.status_code == 403:
logging.error(
"Cloudflare validation required. Run with --scrap-with-playwright"
)
return
response.raise_for_status()
tamil_blasters = BeautifulSoup(response.content, "html.parser")
movie_list_div = tamil_blasters.select(
"div[id='ipsLayout_mainArea'] div[class='ipsWidget_inner ipsPad ipsType_richText']"
)[2]
movie_list = movie_list_div.find_all("p")[2:-2]
movies = tamil_blasters.select("li[data-rowid]")
for movie in movie_list:
if re.search(r"S(\d+).*EP?\s?(\d+|\(\d+\s?-\s?\d+\))", movie.text.strip()):
continue
for movie in movies:
await process_movie(
movie, scraper=scraper, language=language, media_type=media_type
)
data = re.search(r"^(.+\(\d{4}\))", movie.text.strip())
try:
title = re.sub(r"\s+", " ", data[1].strip())
except TypeError:
logging.error(movie.text)
continue
logging.info(f"getting movie data for '{title}'")
video_qualities = movie.find_all("a")[:-1]
metadata = {
"name": title,
"catalog": "any_any",
"video_qualities": {},
"type": "movie",
"season": None,
"episode": None,
async def scrap_page_with_playwright(url, language, media_type, proxy_url=None):
async with async_playwright() as p:
# Launch a new browser session
browser = await p.firefox.launch(
headless=False,
proxy={"server": proxy_url} if proxy_url else None,
)
page = await browser.new_page()
await stealth_async(page)
await asyncio.sleep(2)
page_content = await get_page_content(page, url)
tamil_blasters = BeautifulSoup(page_content, "html.parser")
movies = tamil_blasters.select("li[data-rowid]")
for movie in movies:
await process_movie(
movie, page=page, language=language, media_type=media_type
)
await browser.close()
async def scrap_search_keyword(keyword, proxy_url=None):
supported_forums = {
TAMIL_BLASTER_LINKS[language][media_type]: {
"language": language,
"media_type": media_type,
}
for language in TAMIL_BLASTER_LINKS
for media_type in TAMIL_BLASTER_LINKS[language]
}
for video_quality in video_qualities:
video_quality_name = video_quality.text.strip("[]")
async with async_playwright() as p:
# Launch a new browser session
browser = await p.firefox.launch(
headless=False,
proxy={"server": proxy_url} if proxy_url else None,
)
page = await browser.new_page()
await stealth_async(page)
await asyncio.sleep(2)
page_link = video_quality.get("href")
response = scraper.get(page_link)
movie_page = BeautifulSoup(response.content, "html.parser")
info_hash = extract_info_hash(movie_page)
if not info_hash:
logging.error(f"info hash not found for {page_link}")
continue
soup = await get_search_results(page, keyword)
results_element = soup.find("div", {"data-role": "resultsArea"})
poster = movie_page.select_one("div[data-commenttype='forums'] img[data-src]").get("data-src")
metadata["created_at"] = dateparser(movie_page.find("time").get("datetime"))
metadata["poster"] = poster
metadata["video_qualities"][video_quality_name] = info_hash
results_count = int(re.search(r"\d+", results_element.find("p").text).group())
logging.info(f"Found {results_count} results for {keyword}")
if all(
[
metadata.get("created_at"),
metadata.get("poster"),
metadata.get("video_qualities"),
]
):
await crud.save_movie_metadata(metadata)
movies = results_element.select("li[data-role='activityItem']")
if results_count > 25:
number_of_pages = math.ceil(results_count / 25)
logging.info(f"Found {number_of_pages} pages for {keyword}")
for page_number in range(2, number_of_pages + 1):
soup = await get_search_results(page, keyword, page_number)
movies.extend(soup.select("li[data-role='activityItem']"))
await asyncio.sleep(random.randint(2, 5))
for movie in movies:
await process_movie(
movie, page=page, keyword=keyword, supported_forums=supported_forums
)
await browser.close()
async def run_scraper(
@@ -173,39 +245,91 @@ async def run_scraper(
video_type: str = None,
pages: int = None,
start_page: int = None,
is_scrape_home: bool = True,
search_keyword: str = None,
scrap_with_playwright: bool = None,
proxy_url: str = None,
):
await database.init()
if is_scrape_home:
await scrap_homepage()
else:
try:
scrap_link = TAMIL_BLASTER_LINKS[language][video_type]
except KeyError:
logging.error(f"Unsupported language or video type: {language}_{video_type}")
return
for page in range(start_page, pages + start_page):
scrap_link = f"{scrap_link}/page/{page}/"
logging.info(f"Scrap page: {page}")
await scrap_page(scrap_link, language, video_type)
if search_keyword:
await scrap_search_keyword(search_keyword, proxy_url)
return
try:
scrap_link = f"{HOMEPAGE}/index.php?/forums/forum/{TAMIL_BLASTER_LINKS[language][video_type]}"
except KeyError:
logging.error(f"Unsupported language or video type: {language}_{video_type}")
return
for page in range(start_page, pages + start_page):
scrap_link = f"{scrap_link}/page/{page}/"
logging.info(f"Scrap page: {page}")
if scrap_with_playwright is True:
await scrap_page_with_playwright(
scrap_link, language, video_type, proxy_url
)
else:
await scrap_page(scrap_link, language, video_type, proxy_url)
logging.info(f"Scrap completed for : {language}_{video_type}")
async def run_schedule_scrape(pages: int = 1, start_page: int = 1):
async def run_schedule_scrape(
pages: int = 1,
start_page: int = 1,
scrap_with_playwright: bool = None,
proxy_url: str = None,
):
for language in TAMIL_BLASTER_LINKS:
for video_type in TAMIL_BLASTER_LINKS[language]:
await run_scraper(language, video_type, pages=pages, start_page=start_page, is_scrape_home=False)
await run_scraper(is_scrape_home=True)
await run_scraper(
language,
video_type,
pages=pages,
start_page=start_page,
scrap_with_playwright=scrap_with_playwright,
proxy_url=proxy_url,
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Scrap Movie metadata from TamilBlasters")
parser.add_argument("--all", action="store_true", help="scrap all type of movies & series")
parser.add_argument("--home", action="store_true", help="scrap home page")
parser.add_argument("-l", "--language", help="scrap movie language", default="tamil")
parser.add_argument("-t", "--video-type", help="scrap movie video type", default="hdrip")
parser.add_argument("-p", "--pages", type=int, default=1, help="number of scrap pages")
parser.add_argument("-s", "--start-pages", type=int, default=1, help="page number to start scrap.")
parser = argparse.ArgumentParser(
description="Scrap Movie metadata from TamilBlasters"
)
parser.add_argument(
"--all", action="store_true", help="scrap all type of movies & series"
)
parser.add_argument(
"-l",
"--language",
help="scrap movie language",
default="tamil",
choices=["tamil", "malayalam", "telugu", "hindi", "kannada", "english"],
)
parser.add_argument(
"-t",
"--video-type",
help="scrap movie video type",
default="hdrip",
choices=["hdrip", "tcrip", "dubbed", "series"],
)
parser.add_argument(
"-p", "--pages", type=int, default=1, help="number of scrap pages"
)
parser.add_argument(
"-s", "--start-pages", type=int, default=1, help="page number to start scrap."
)
parser.add_argument(
"-k",
"--search-keyword",
help="search keyword to scrap movies & series. ex: 'bigg boss'",
default=None,
)
parser.add_argument(
"--scrap-with-playwright", action="store_true", help="scrap with playwright"
)
parser.add_argument(
"--proxy-url",
help="proxy url to scrap. ex: socks5://127.0.0.1:1080",
default=None,
)
args = parser.parse_args()
logging.basicConfig(
@@ -214,6 +338,20 @@ if __name__ == "__main__":
level=logging.INFO,
)
if args.all:
asyncio.run(run_schedule_scrape(args.pages, args.start_pages))
asyncio.run(
run_schedule_scrape(
args.pages, args.start_pages, args.scrap_with_playwright, args.proxy_url
)
)
else:
asyncio.run(run_scraper(args.language, args.video_type, args.pages, args.start_pages, args.home))
asyncio.run(
run_scraper(
args.language,
args.video_type,
args.pages,
args.start_pages,
args.search_keyword,
args.scrap_with_playwright,
args.proxy_url,
)
)
+45 -19
View File
@@ -25,7 +25,13 @@ class RealDebrid:
self.disable_access_token()
def _make_request(
self, method: str, url: str, data=None, params=None, is_return_none=False, is_expected_to_fail=False
self,
method: str,
url: str,
data=None,
params=None,
is_return_none=False,
is_expected_to_fail=False,
) -> dict:
if method == "GET":
response = requests.get(url, params=params, headers=self.headers)
@@ -64,8 +70,12 @@ class RealDebrid:
def initialize_headers(self):
if self.encoded_token:
token_data = self.decode_token_str(self.encoded_token)
access_token_data = self.get_token(token_data["client_id"], token_data["client_secret"], token_data["code"])
self.headers = {"Authorization": f"Bearer {access_token_data['access_token']}"}
access_token_data = self.get_token(
token_data["client_id"], token_data["client_secret"], token_data["code"]
)
self.headers = {
"Authorization": f"Bearer {access_token_data['access_token']}"
}
@staticmethod
def encode_token_data(client_id: str, client_secret: str, code: str):
@@ -110,18 +120,24 @@ class RealDebrid:
if "client_secret" not in response_data:
return response_data
token_data = self.get_token(response_data["client_id"], response_data["client_secret"], device_code)
token_data = self.get_token(
response_data["client_id"], response_data["client_secret"], device_code
)
if "access_token" in token_data:
token = self.encode_token_data(
response_data["client_id"], response_data["client_secret"], token_data["refresh_token"]
response_data["client_id"],
response_data["client_secret"],
token_data["refresh_token"],
)
return {"token": token}
else:
return token_data
def add_magent_link(self, magnet_link):
return self._make_request("POST", f"{self.BASE_URL}/torrents/addMagnet", data={"magnet": magnet_link})
return self._make_request(
"POST", f"{self.BASE_URL}/torrents/addMagnet", data={"magnet": magnet_link}
)
def get_user_torrent_list(self):
return self._make_request("GET", f"{self.BASE_URL}/torrents")
@@ -130,34 +146,44 @@ class RealDebrid:
return self._make_request("GET", f"{self.BASE_URL}/torrents/info/{torrent_id}")
def get_torrent_instant_availability(self, torrent_hash):
return self._make_request("GET", f"{self.BASE_URL}/torrents/instantAvailability/{torrent_hash}")
return self._make_request(
"GET", f"{self.BASE_URL}/torrents/instantAvailability/{torrent_hash}"
)
def disable_access_token(self):
return self._make_request("GET", f"{self.BASE_URL}/disable_access_token", is_return_none=True)
return self._make_request(
"GET", f"{self.BASE_URL}/disable_access_token", is_return_none=True
)
def start_torrent_download(self, torrent_id, file_ids="all"):
return self._make_request(
"POST", f"{self.BASE_URL}/torrents/selectFiles/{torrent_id}", data={"files": file_ids}, is_return_none=True
"POST",
f"{self.BASE_URL}/torrents/selectFiles/{torrent_id}",
data={"files": file_ids},
is_return_none=True,
)
def get_available_torrent(self, info_hash, episode: int | None) -> dict[str, Any] | None:
def get_available_torrent(self, info_hash, filename: str) -> dict[str, Any] | None:
available_torrents = self.get_user_torrent_list()
for torrent in available_torrents:
if torrent["hash"] == info_hash:
if episode is None:
return torrent
torrent_name = PTN.parse(torrent["filename"])
if "episode" in torrent_name and int(torrent_name["episode"]) == episode:
return torrent
if torrent["hash"] == info_hash and torrent["filename"] == filename:
return torrent
def create_download_link(self, link):
response = self._make_request(
"POST", f"{self.BASE_URL}/unrestrict/link", data={"link": link}, is_expected_to_fail=True
"POST",
f"{self.BASE_URL}/unrestrict/link",
data={"link": link},
is_expected_to_fail=True,
)
if "download" in response:
return response
if "error_code" in response:
if response["error_code"] == 23:
raise ProviderException("Exceed remote traffic limit", "exceed_remote_traffic_limit.mp4")
raise ProviderException(f"Failed to create download link. response: {response}", "api_error.mp4")
raise ProviderException(
"Exceed remote traffic limit", "exceed_remote_traffic_limit.mp4"
)
raise ProviderException(
f"Failed to create download link. response: {response}", "api_error.mp4"
)
+39 -18
View File
@@ -1,16 +1,17 @@
import time
from db.models import Streams, Episode
from db.schemas import UserData
from streaming_providers.exceptions import ProviderException
from streaming_providers.realdebrid.client import RealDebrid
from utils.parser import select_episode_file
def check_existing_torrent(
rd_client, info_hash: str, episode: int | None, max_retries: int, retry_interval: int
rd_client, info_hash: str, filename: str, max_retries: int, retry_interval: int
) -> dict | None:
"""Check if the torrent is already in torrent list and return the direct link if available."""
retries = 0
torrent_info = rd_client.get_available_torrent(info_hash, episode)
torrent_info = rd_client.get_available_torrent(info_hash, filename)
if not torrent_info:
return None
torrent_id = torrent_info.get("id")
@@ -22,13 +23,17 @@ def check_existing_torrent(
elif torrent_info["status"] == "waiting_files_selection":
return torrent_info
elif torrent_info["status"] == "magnet_error":
raise ProviderException("Failed to add magnet link to Real-Debrid", "transfer_error.mp4")
raise ProviderException(
"Failed to add magnet link to Real-Debrid", "transfer_error.mp4"
)
time.sleep(retry_interval)
retries += 1
raise ProviderException("Torrent not downloaded yet.", "torrent_not_downloaded.mp4")
def wait_for_file_selection(rd_client, torrent_id: str, max_retries: int, retry_interval: int):
def wait_for_file_selection(
rd_client, torrent_id: str, max_retries: int, retry_interval: int
):
"""Wait for the torrent status to be 'waiting_files_selection'."""
retries = 0
while retries < max_retries:
@@ -37,10 +42,15 @@ def wait_for_file_selection(rd_client, torrent_id: str, max_retries: int, retry_
return
time.sleep(retry_interval)
retries += 1
raise ProviderException("Not enough seeders available for parse magnet link", "torrent_not_downloaded.mp4")
raise ProviderException(
"Not enough seeders available for parse magnet link",
"torrent_not_downloaded.mp4",
)
def wait_for_torrent_download(rd_client, torrent_id: str, max_retries: int, retry_interval: int) -> str:
def wait_for_torrent_download(
rd_client, torrent_id: str, max_retries: int, retry_interval: int
) -> str:
"""Wait for the torrent to be downloaded and return the direct link."""
retries = 0
while retries < max_retries:
@@ -54,11 +64,20 @@ def wait_for_torrent_download(rd_client, torrent_id: str, max_retries: int, retr
def get_direct_link_from_realdebrid(
info_hash: str, magnet_link: str, token: str, episode: int | None, max_retries=5, retry_interval=5
info_hash: str,
magnet_link: str,
user_data: UserData,
stream: Streams,
episode_data: Episode = None,
max_retries=5,
retry_interval=5,
) -> str:
rd_client = RealDebrid(encoded_token=token)
rd_client = RealDebrid(encoded_token=user_data.streaming_provider.token)
filename = episode_data.filename if episode_data else stream.filename
response_data = check_existing_torrent(rd_client, info_hash, episode, max_retries, retry_interval)
response_data = check_existing_torrent(
rd_client, info_hash, filename, max_retries, retry_interval
)
if response_data and "download" in response_data:
return response_data.get("download")
@@ -66,7 +85,9 @@ def get_direct_link_from_realdebrid(
if response_data is None:
response_data = rd_client.add_magent_link(magnet_link)
if "id" not in response_data:
raise ProviderException("Failed to add magnet link to Real-Debrid", "transfer_error.mp4")
raise ProviderException(
"Failed to add magnet link to Real-Debrid", "transfer_error.mp4"
)
torrent_id = response_data["id"]
wait_for_file_selection(rd_client, torrent_id, max_retries, retry_interval)
@@ -74,14 +95,14 @@ def get_direct_link_from_realdebrid(
# Retrieve torrent information
torrent_info = rd_client.get_torrent_info(torrent_id)
if not torrent_info["files"]:
raise ProviderException("No files available for this torrent", "transfer_error.mp4")
raise ProviderException(
"No files available for this torrent", "transfer_error.mp4"
)
# If episode number is provided, search for that episode
if episode:
selected_file = select_episode_file(torrent_info["files"], episode, "path")
else:
# Otherwise, select the largest file for download
selected_file = max(torrent_info["files"], key=lambda x: x["bytes"])
# select the file to download
selected_file = [
file for file in torrent_info["files"] if file["path"] == filename
][0]
file_id = selected_file["id"]
rd_client.start_torrent_download(torrent_id, file_id)
+40 -20
View File
@@ -1,8 +1,12 @@
import logging
import time
from seedrcc import Seedr
from db.models import Streams, Episode
from db.schemas import UserData
from streaming_providers.exceptions import ProviderException
from utils.parser import select_episode_file
from utils.parser import clean_name
def check_torrent_status(seedr, info_hash: str):
@@ -27,7 +31,9 @@ def add_magnet_and_get_torrent(seedr, magnet_link: str, info_hash: str):
if "error" in transfer:
if transfer["error"] == "invalid_token":
raise ProviderException("Invalid Seedr token", "invalid_token.mp4")
raise ProviderException("Error transferring magnet link to Seedr", "transfer_error.mp4")
raise ProviderException(
"Error transferring magnet link to Seedr", "transfer_error.mp4"
)
# Return the appropriate torrent name based on the transfer result
if transfer["result"] is True and "title" in transfer:
@@ -36,13 +42,23 @@ def add_magnet_and_get_torrent(seedr, magnet_link: str, info_hash: str):
torrent = check_torrent_status(seedr, info_hash)
if torrent:
return torrent["name"]
elif transfer["result"] in ("not_enough_space_added_to_wishlist", "not_enough_space_wishlist_full"):
raise ProviderException("Not enough space in Seedr account to add this torrent", "not_enough_space.mp4")
elif transfer["result"] in (
"not_enough_space_added_to_wishlist",
"not_enough_space_wishlist_full",
):
raise ProviderException(
"Not enough space in Seedr account to add this torrent",
"not_enough_space.mp4",
)
raise ProviderException("Error transferring magnet link to Seedr", "transfer_error.mp4")
raise ProviderException(
"Error transferring magnet link to Seedr", "transfer_error.mp4"
)
def wait_for_torrent_to_complete(seedr, info_hash: str, max_retries: int, retry_interval: int):
def wait_for_torrent_to_complete(
seedr, info_hash: str, max_retries: int, retry_interval: int
):
"""Waits for a torrent with the given info_hash to complete downloading."""
retries = 0
while retries < max_retries:
@@ -57,29 +73,27 @@ def wait_for_torrent_to_complete(seedr, info_hash: str, max_retries: int, retry_
raise ProviderException("Torrent not downloaded yet.", "torrent_not_downloaded.mp4")
def get_file_details_from_folder(seedr, folder_id: int, episode: int | None):
"""Gets the details of the largest file in a given folder."""
def get_file_details_from_folder(seedr, folder_id: int, filename: str):
"""Gets the details of the file in a given folder."""
folder_content = seedr.listContents(folder_id)
if episode:
return select_episode_file(folder_content["files"], episode, "name")
return max(folder_content["files"], key=lambda x: x["size"])
return [f for f in folder_content["files"] if f["name"] == filename][0]
def get_direct_link_from_seedr(
async def get_direct_link_from_seedr(
info_hash: str,
magnet_link: str,
token: str,
torrent_name: str,
episode: str | None,
user_data: UserData,
stream: Streams,
episode_data: Episode = None,
max_retries=5,
retry_interval=5,
) -> str:
"""Gets a direct download link from Seedr using a magnet link and token."""
seedr = Seedr(token=token)
seedr = Seedr(token=user_data.streaming_provider.token)
# Check for existing torrent or folder
torrent = check_torrent_status(seedr, info_hash)
folder = check_folder_status(seedr, torrent_name)
folder = check_folder_status(seedr, clean_name(stream.torrent_name))
# Handle the torrent based on its status or if it's already in a folder
if folder:
@@ -89,15 +103,21 @@ def get_direct_link_from_seedr(
folder_title = torrent["name"]
else:
folder_title = add_magnet_and_get_torrent(seedr, magnet_link, info_hash)
if clean_name(stream.torrent_name) != folder_title:
logging.warning(
f"Torrent name mismatch: '{clean_name(stream.torrent_name)}' != '{folder_title}'."
)
folder = check_folder_status(seedr, folder_title)
if not folder:
wait_for_torrent_to_complete(seedr, info_hash, max_retries, retry_interval)
folder = check_folder_status(seedr, folder_title)
folder_id = folder["id"]
if torrent_name != folder_title:
seedr.renameFolder(folder_id, torrent_name)
selected_file = get_file_details_from_folder(seedr, folder_id, episode)
selected_file = get_file_details_from_folder(
seedr,
folder_id,
clean_name(episode_data.filename if episode_data else stream.filename),
)
video_link = seedr.fetchFile(selected_file["folder_file_id"])["url"]
return video_link
+82 -49
View File
@@ -1,52 +1,70 @@
import PTN
import math
import re
from db.schemas import Stream
from streaming_providers.exceptions import ProviderException
from db.config import settings
from db.models import Streams
from db.schemas import Stream, UserData
from utils.site_data import ALWAYS_ENABLED, LANGUAGE_CATALOGS
def extract_stream_details(stream_name, video_qualities: dict, user_data) -> list[Stream]:
def extract_stream_details(
streams: list[Streams],
user_data: UserData,
secret_str: str,
season: int = None,
episode: int = None,
) -> list[Stream]:
stream_list = []
for quality_string, hash_value in video_qualities.items():
stream_details = {}
# Quality
quality = re.search(
r"(4K|\d{3,4}p|HQ HDRip|HDRip|HQ PreDVDRip|HQ Real-PreDVDRip|HQ-Real PreDVDRip|HQ-S Print)", quality_string
# Sort the streams by created_at time
streams = sorted(streams, key=lambda x: x.created_at, reverse=True)
for stream_data in streams:
quality_detail = " - ".join(
filter(
None,
[
stream_data.quality,
stream_data.resolution,
stream_data.codec,
stream_data.audio,
],
)
)
if quality:
quality = quality.group(0)
else:
quality = "N/A"
# Size
size = re.search(r"(\d+(\.\d+)?(MB|GB))", quality_string)
if size:
size = size.group(0)
else:
size = "N/A"
episode_data = stream_data.get_episode(season, episode)
# Languages
lang_dict = {
"Tam": "Tamil",
"Mal": "Malayalam",
"Tel": "Telugu",
"Kan": "Kannada",
"Hin": "Hindi",
"Eng": "English",
description_parts = [
quality_detail,
convert_bytes_to_readable(
episode_data.size if episode_data else stream_data.size
),
" + ".join(stream_data.languages),
stream_data.source,
user_data.streaming_provider.service.title()
if user_data.streaming_provider
else "Torrent",
]
description = ", ".join(filter(lambda x: bool(x), description_parts))
stream_details = {
"name": "MediaFusion",
"description": description,
"infoHash": stream_data.id,
"fileIdx": episode_data.file_index
if episode_data
else stream_data.file_index,
"behaviorHints": {"bingeGroup": f"MediaFusion-{quality_detail}"},
}
languages = []
for short, full in lang_dict.items():
if re.search(f"({short}|{full})", quality_string, re.IGNORECASE):
languages.append(full)
stream_details["name"] = "TamilBlasters"
stream_details["stream_name"] = stream_name
streaming_provider = user_data.streaming_provider.service.title() if user_data.streaming_provider else "Torrent"
stream_details["description"] = f"{quality}, {size}, {' + '.join(languages)}, {streaming_provider}"
stream_details["infoHash"] = hash_value
if user_data.streaming_provider:
base_proxy_url = f"{settings.host_url}/{secret_str}/streaming_provider?info_hash={stream_data.id}"
if episode_data:
base_proxy_url += f"&season={season}&episode={episode}"
stream_details["url"] = base_proxy_url
stream_details.pop("infoHash")
stream_details.pop("fileIdx")
stream_details["behaviorHints"]["notWebReady"] = True
stream_list.append(Stream(**stream_details))
@@ -64,25 +82,40 @@ def generate_catalog_ids(preferred_movie_languages, preferred_series_languages):
catalog_ids.extend(catalogs["series"])
# Handle the dubbed option
if "Dubbed" in preferred_movie_languages and language + "_dubbed" in catalogs["movie"]:
if (
"Dubbed" in preferred_movie_languages
and language + "_dubbed" in catalogs["movie"]
):
catalog_ids.append(language.lower() + "_dubbed")
return catalog_ids
def clean_name(name: str) -> str:
# Only allow alphanumeric characters, spaces, and `.,:_-`
cleaned_name = re.sub(r"[^a-zA-Z0-9 .,:_-]", "", name)
# Only allow alphanumeric characters, spaces, and `.,;:_~-[]()`
cleaned_name = re.sub(r"[^a-zA-Z0-9 .,;:_~\-()\[\]]", "", name)
return cleaned_name
def select_episode_file(torrent_files: list, episode: int, file_name_key: str) -> dict:
"""Select the file with the specified episode number."""
def convert_bytes_to_readable(size_bytes: int) -> str:
"""
Convert a size in bytes into a more human-readable format.
"""
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
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]}"
for file in torrent_files:
torrent_data = PTN.parse(file[file_name_key])
file_episode = torrent_data.get("episode")
if file_episode and int(file_episode) == episode:
return file
else:
raise ProviderException(f"Episode {episode} not found in this torrent", "episode_not_found.mp4")
def get_catalogs(catalog: str, languages: list[str]) -> list[str]:
base_catalogs = ["hdrip", "tcrip", "dubbed", "series"]
base_catalog = catalog.split("_")[1]
if base_catalog not in base_catalogs:
return [catalog]
# Generate the catalog for each supported language
return [f"{lang.lower()}_{base_catalog}" for lang in languages]
+24 -69
View File
@@ -1,73 +1,28 @@
HOMEPAGE = "https://www.1tamilblasters.lat"
TAMIL_BLASTER_LINKS = {
"tamil": {
"hdrip": f"{HOMEPAGE}/index.php?/forums/forum/7-tamil-new-movies-hdrips-bdrips-dvdrips-hdtv",
"tcrip": f"{HOMEPAGE}/index.php?/forums/forum/8-tamil-new-movies-tcrip-dvdscr-hdcam-predvd",
"dubbed": f"{HOMEPAGE}/index.php?/forums/forum/9-tamil-dubbed-movies-bdrips-hdrips-dvdscr-hdcam-in-multi-audios",
"series": f"{HOMEPAGE}/index.php?/forums/forum/63-tamil-new-web-series-tv-shows",
},
"malayalam": {
"tcrip": f"{HOMEPAGE}/index.php?/forums/forum/75-malayalam-new-movies-tcrip-dvdscr-hdcam-predvd",
"hdrip": f"{HOMEPAGE}/index.php?/forums/forum/74-malayalam-new-movies-hdrips-bdrips-dvdrips-hdtv",
"dubbed": f"{HOMEPAGE}/index.php?/forums/forum/76-malayalam-dubbed-movies-bdrips-hdrips-dvdscr-hdcam",
"series": f"{HOMEPAGE}/index.php?/forums/forum/98-malayalam-new-web-series-tv-shows",
},
"telugu": {
"tcrip": f"{HOMEPAGE}/index.php?/forums/forum/79-telugu-new-movies-tcrip-dvdscr-hdcam-predvd",
"hdrip": f"{HOMEPAGE}/index.php?/forums/forum/78-telugu-new-movies-hdrips-bdrips-dvdrips-hdtv",
"dubbed": f"{HOMEPAGE}/index.php?/forums/forum/80-telugu-dubbed-movies-bdrips-hdrips-dvdscr-hdcam",
"series": f"{HOMEPAGE}/index.php?/forums/forum/96-telugu-new-web-series-tv-shows",
},
"hindi": {
"tcrip": f"{HOMEPAGE}/index.php?/forums/forum/87-hindi-new-movies-tcrip-dvdscr-hdcam-predvd",
"hdrip": f"{HOMEPAGE}/index.php?/forums/forum/86-hindi-new-movies-hdrips-bdrips-dvdrips-hdtv",
"dubbed": f"{HOMEPAGE}/index.php?/forums/forum/88-hindi-dubbed-movies-bdrips-hdrips-dvdscr-hdcam",
"series": f"{HOMEPAGE}/index.php?/forums/forum/89-hindi-new-web-series-tv-shows",
},
"kannada": {
"tcrip": f"{HOMEPAGE}/index.php?/forums/forum/83-kannada-new-movies-tcrip-dvdscr-hdcam-predvd",
"hdrip": f"{HOMEPAGE}/index.php?/forums/forum/82-kannada-new-movies-hdrips-bdrips-dvdrips-hdtv",
"dubbed": f"{HOMEPAGE}/index.php?/forums/forum/84-kannada-dubbed-movies-bdrips-hdrips-dvdscr-hdcam",
"series": f"{HOMEPAGE}/index.php?/forums/forum/103-kannada-new-web-series-tv-shows",
},
"english": {
"tcrip": f"{HOMEPAGE}/index.php?/forums/forum/52-english-movies-hdcam-dvdscr-predvd",
"hdrip": f"{HOMEPAGE}/index.php?/forums/forum/53-english-movies-hdrips-bdrips-dvdrips",
"series": f"{HOMEPAGE}/index.php?/forums/forum/92-english-web-series-tv-shows",
},
}
LANGUAGE_CATALOGS = {
"Tamil": {"movie": ["tamil_hdrip", "tamil_tcrip", "tamil_dubbed"], "series": ["tamil_series"]},
"Malayalam": {"movie": ["malayalam_hdrip", "malayalam_tcrip", "malayalam_dubbed"], "series": ["malayalam_series"]},
"Telugu": {"movie": ["telugu_hdrip", "telugu_tcrip", "telugu_dubbed"], "series": ["telugu_series"]},
"Hindi": {"movie": ["hindi_hdrip", "hindi_tcrip", "hindi_dubbed"], "series": ["hindi_series"]},
"Kannada": {"movie": ["kannada_hdrip", "kannada_tcrip", "kannada_dubbed"], "series": ["kannada_series"]},
"English": {"movie": ["english_hdrip", "english_tcrip"], "series": ["english_series"]},
"Tamil": {
"movie": ["tamil_hdrip", "tamil_tcrip", "tamil_dubbed"],
"series": ["tamil_series"],
},
"Malayalam": {
"movie": ["malayalam_hdrip", "malayalam_tcrip", "malayalam_dubbed"],
"series": ["malayalam_series"],
},
"Telugu": {
"movie": ["telugu_hdrip", "telugu_tcrip", "telugu_dubbed"],
"series": ["telugu_series"],
},
"Hindi": {
"movie": ["hindi_hdrip", "hindi_tcrip", "hindi_dubbed"],
"series": ["hindi_series"],
},
"Kannada": {
"movie": ["kannada_hdrip", "kannada_tcrip", "kannada_dubbed"],
"series": ["kannada_series"],
},
"English": {
"movie": ["english_hdrip", "english_tcrip"],
"series": ["english_series"],
},
}
ALWAYS_ENABLED = ["tamil_blasters"]
TRACKERS = [
"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",
"http://125.227.35.196:6969/announce",
"http://210.244.71.25:6969/announce",
"http://210.244.71.26:6969/announce",
"http://213.159.215.198:6970/announce",
"http://37.19.5.139:6969/announce",
"http://37.19.5.155:6881/announce",
"http://46.4.109.148:6969/announce",
"http://87.248.186.252:8080/announce",
]
+70 -15
View File
@@ -1,37 +1,92 @@
import bencodepy
import hashlib
import logging
import requests
from urllib.parse import quote
from utils.site_data import TRACKERS
import PTN
import bencodepy
from utils.parser import clean_name
TRACKERS = [
"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 get_info_hash_from_url(torrent_url):
def extract_torrent_metadata(content) -> dict:
try:
response = requests.get(torrent_url)
response.raise_for_status()
torrent_data = bencodepy.decode(response.content)
torrent_data = bencodepy.decode(content)
info = torrent_data[b"info"]
info_encoded = bencodepy.encode(info)
m = hashlib.sha1()
m.update(info_encoded)
return m.hexdigest()
info_hash = m.hexdigest()
# 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()
parsed_data = PTN.parse(filename)
file_data = [
{
"filename": filename,
"size": total_size,
"index": 0,
"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 {
"info_hash": info_hash,
"announce_list": announce_list,
"total_size": total_size,
"file_data": file_data,
"torrent_name": torrent_name,
}
except Exception as e:
logging.error(f"Error occurred: {e}")
return False
return {}
def convert_info_hash_to_magnet(info_hash: str, name: str = "") -> str:
def convert_info_hash_to_magnet(info_hash: str, trackers: list[str], name: str) -> str:
magnet_link = f"magnet:?xt=urn:btih:{info_hash}"
if name:
encoded_name = quote(name, safe="")
magnet_link += f"&dn={encoded_name}"
for tracker in TRACKERS:
encoded_name = quote(clean_name(name), safe="")
magnet_link += f"&dn={encoded_name}"
for tracker in trackers or TRACKERS:
encoded_tracker = quote(tracker, safe="")
magnet_link += f"&tr={encoded_tracker}"
return magnet_link