mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
52d8956034
* Refactor ZileanScraper to use parallel requests for searching and filtering streams with new endpoints * Fix DLHD scraping & enable DLHD without MediaFlow * Switch to httpx for async HTTP requests * Implement caching for PikPak token to reduce login error * Refactor torrent cleanup logic * Add inactivity monitor extension to close idle spiders Introduced `InactivityMonitor` to automatically close spiders that remain inactive for a specified time. This extension checks activity at regular intervals and uses configurable settings for check intervals and inactivity timeouts. If no items are scraped within the timeout period, the spider is closed to free resources. * Handle TypeError in dynamic sorting of streams * Refactor torrent info scraper to support pre-processing. Introduced a pre-processing function mapping to handle specific indexer requirements before parsing the HTML. Added a custom pre-processing function for "TheRARBG" to handle URL adjustments, improving modularity and readability in the `get_torrent_info` function. * Refine error logging and fix hash key in spider * Fix Prowlarr not stop on max process limit * #315: Integrate ScrapeOps logging into all scrapers & spiders Added ScrapeOps logging to Zilean, Torrentio, Prowlarr, and Prowlarr Feed scrapers to enhance request tracking and error handling. Configured ScrapeOps API key in settings and updated Pipfile/Pipfile.lock with scrapeops-python-requests and scrapeops-scrapy dependencies. * Refactor scraper cache status handler * verify torrent before parsing on prowlarr & prioritize magnet on badass_torrents * Add support for provide locally hosted mediaflow proxy public address and reduce the time leg on private ip address checking * handle RD exception cases * do not setup scrapeops when api key is none * Enhanced dynamic sorting of torrent streams Revised the dynamic_sort_key function to handle different key types more efficiently with match-case. Simplified error handling and improved logging to capture sorting data in the case of exceptions. * add missing last update date for metadata * update domain for nowmesports
196 lines
6.2 KiB
Python
196 lines
6.2 KiB
Python
import hashlib
|
|
import logging
|
|
from contextlib import AsyncExitStack, asynccontextmanager
|
|
from typing import Awaitable, Iterable, AsyncIterator, Optional, TypeVar
|
|
from urllib.parse import quote
|
|
|
|
import PTT
|
|
import anyio
|
|
import bencodepy
|
|
import httpx
|
|
from anyio import (
|
|
create_task_group,
|
|
create_memory_object_stream,
|
|
CapacityLimiter,
|
|
)
|
|
from anyio.streams.memory import MemoryObjectSendStream
|
|
from demagnetize.core import Demagnetizer
|
|
from torf import Magnet, MagnetError
|
|
|
|
import utils.runtime_const
|
|
from utils.parser import is_contain_18_plus_keywords
|
|
from utils.runtime_const import TRACKERS
|
|
|
|
# remove logging from demagnetize
|
|
logging.getLogger("demagnetize").setLevel(logging.CRITICAL)
|
|
|
|
|
|
def extract_torrent_metadata(content: bytes, is_parse_ptt: bool = True) -> dict:
|
|
try:
|
|
torrent_data = bencodepy.decode(content)
|
|
info = torrent_data[b"info"]
|
|
info_encoded = bencodepy.encode(info)
|
|
m = hashlib.sha1()
|
|
m.update(info_encoded)
|
|
info_hash = m.hexdigest()
|
|
|
|
# Extract file size, file list, and announce list
|
|
files = info[b"files"] if b"files" in info else [info]
|
|
total_size = sum(file[b"length"] for file in files)
|
|
file_data = []
|
|
|
|
for idx, file in enumerate(files):
|
|
filename = (
|
|
file[b"path"][0].decode()
|
|
if b"files" in info
|
|
else file[b"name"].decode()
|
|
)
|
|
parsed_data = PTT.parse_title(filename)
|
|
file_data.append(
|
|
{
|
|
"filename": filename,
|
|
"size": file[b"length"],
|
|
"index": idx,
|
|
"seasons": parsed_data.get("seasons"),
|
|
"episodes": parsed_data.get("episodes"),
|
|
}
|
|
)
|
|
|
|
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"]
|
|
if is_contain_18_plus_keywords(torrent_name):
|
|
logging.warning(
|
|
f"Torrent name contains 18+ keywords: {torrent_name}. Skipping"
|
|
)
|
|
return {}
|
|
|
|
largest_file = max(file_data, key=lambda x: x["size"])
|
|
|
|
metadata = {
|
|
"info_hash": info_hash,
|
|
"announce_list": announce_list,
|
|
"total_size": total_size,
|
|
"file_data": file_data,
|
|
"torrent_name": torrent_name,
|
|
"largest_file": largest_file,
|
|
}
|
|
if is_parse_ptt:
|
|
metadata.update(PTT.parse_title(torrent_name, True))
|
|
return metadata
|
|
except Exception as e:
|
|
logging.exception(f"Error occurred: {e}")
|
|
return {}
|
|
|
|
|
|
async def convert_info_hash_to_magnet(info_hash: str, trackers: list[str]) -> str:
|
|
magnet_link = f"magnet:?xt=urn:btih:{info_hash}"
|
|
for tracker in set(trackers) or TRACKERS:
|
|
encoded_tracker = quote(tracker, safe="")
|
|
magnet_link += f"&tr={encoded_tracker}"
|
|
return magnet_link
|
|
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def acollect(
|
|
coros: Iterable[Awaitable[T]],
|
|
limit: Optional[CapacityLimiter] = None,
|
|
timeout: int = 30,
|
|
) -> AsyncIterator[AsyncIterator[T]]:
|
|
async with create_task_group() as tg:
|
|
sender, receiver = create_memory_object_stream[T]()
|
|
async with sender:
|
|
for c in coros:
|
|
tg.start_soon(_acollect_pipe, c, limit, sender.clone(), timeout)
|
|
async with receiver:
|
|
yield receiver
|
|
|
|
|
|
async def _acollect_pipe(
|
|
coro: Awaitable[T],
|
|
limit: Optional[CapacityLimiter],
|
|
sender: MemoryObjectSendStream[T],
|
|
timeout: int,
|
|
) -> None:
|
|
async with AsyncExitStack() as stack:
|
|
if limit is not None:
|
|
await stack.enter_async_context(limit)
|
|
await stack.enter_async_context(sender)
|
|
try:
|
|
with anyio.fail_after(timeout):
|
|
value = await coro
|
|
await sender.send(value)
|
|
except Exception as e:
|
|
# Send the exception instead of the value
|
|
await sender.send(e)
|
|
|
|
|
|
async def info_hashes_to_torrent_metadata(
|
|
info_hashes: list[str], trackers: list[str]
|
|
) -> list[dict]:
|
|
torrents_data = []
|
|
|
|
demagnetizer = Demagnetizer()
|
|
async with acollect(
|
|
[
|
|
demagnetizer.demagnetize(Magnet(xt=info_hash, tr=trackers or TRACKERS))
|
|
for info_hash in info_hashes
|
|
],
|
|
timeout=60,
|
|
) as async_iterator:
|
|
async for torrent_result in async_iterator:
|
|
try:
|
|
if isinstance(torrent_result, Exception):
|
|
pass
|
|
else:
|
|
torrents_data.append(
|
|
extract_torrent_metadata(torrent_result.dump())
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"Error processing torrent: {e}")
|
|
|
|
return torrents_data
|
|
|
|
|
|
async def init_best_trackers():
|
|
# get the best trackers from https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt
|
|
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.get(
|
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt",
|
|
timeout=30,
|
|
)
|
|
if response.status_code == 200:
|
|
trackers = [tracker for tracker in response.text.split("\n") if tracker]
|
|
utils.runtime_const.TRACKERS.extend(trackers)
|
|
utils.runtime_const.TRACKERS = list(set(utils.runtime_const.TRACKERS))
|
|
|
|
logging.info(
|
|
f"Loaded {len(trackers)} trackers. Total: {len(utils.runtime_const.TRACKERS)}"
|
|
)
|
|
else:
|
|
logging.error(f"Failed to load trackers: {response.status_code}")
|
|
except (httpx.ConnectTimeout, Exception) as e:
|
|
logging.error(f"Failed to load trackers: {e}")
|
|
|
|
|
|
def parse_magnet(magnet_link: str) -> tuple[str, list[str]]:
|
|
"""
|
|
Parse magnet link and return info hash and trackers
|
|
"""
|
|
try:
|
|
magnet = Magnet.from_string(magnet_link)
|
|
except MagnetError:
|
|
return "", []
|
|
return magnet.infohash, magnet.tr
|
|
|
|
|
|
def get_info_hash_from_magnet(magnet_link: str) -> str:
|
|
info_hash, _ = parse_magnet(magnet_link)
|
|
return info_hash
|