mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
418 lines
15 KiB
Python
418 lines
15 KiB
Python
import asyncio
|
|
import math
|
|
import re
|
|
|
|
import requests
|
|
from imdb import Cinemagoer
|
|
|
|
from db.config import settings
|
|
from db.models import TorrentStreams, MediaFusionTVMetaData
|
|
from db.schemas import Stream, UserData
|
|
from streaming_providers.alldebrid.utils import (
|
|
update_ad_cache_status,
|
|
fetch_downloaded_info_hashes_from_ad,
|
|
)
|
|
from streaming_providers.debridlink.utils import (
|
|
update_dl_cache_status,
|
|
fetch_downloaded_info_hashes_from_dl,
|
|
)
|
|
from streaming_providers.offcloud.utils import (
|
|
update_oc_cache_status,
|
|
fetch_downloaded_info_hashes_from_oc,
|
|
)
|
|
from streaming_providers.pikpak.utils import (
|
|
update_pikpak_cache_status,
|
|
fetch_downloaded_info_hashes_from_pikpak,
|
|
)
|
|
from streaming_providers.premiumize.utils import (
|
|
update_pm_cache_status,
|
|
fetch_downloaded_info_hashes_from_premiumize,
|
|
)
|
|
from streaming_providers.qbittorrent.utils import (
|
|
update_qbittorrent_cache_status,
|
|
fetch_info_hashes_from_webdav,
|
|
)
|
|
from streaming_providers.realdebrid.utils import (
|
|
update_rd_cache_status,
|
|
fetch_downloaded_info_hashes_from_rd,
|
|
)
|
|
from streaming_providers.seedr.utils import (
|
|
update_seedr_cache_status,
|
|
fetch_downloaded_info_hashes_from_seedr,
|
|
)
|
|
from streaming_providers.torbox.utils import (
|
|
update_torbox_cache_status,
|
|
fetch_downloaded_info_hashes_from_torbox,
|
|
)
|
|
from utils import const
|
|
|
|
ia = Cinemagoer()
|
|
ADULT_CONTENT_KEYWORDS = re.compile(
|
|
settings.adult_content_regex_keywords,
|
|
re.IGNORECASE,
|
|
)
|
|
# Define provider-specific cache update functions
|
|
CACHE_UPDATE_FUNCTIONS = {
|
|
"alldebrid": update_ad_cache_status,
|
|
"debridlink": update_dl_cache_status,
|
|
"offcloud": update_oc_cache_status,
|
|
"pikpak": update_pikpak_cache_status,
|
|
"realdebrid": update_rd_cache_status,
|
|
"seedr": update_seedr_cache_status,
|
|
"torbox": update_torbox_cache_status,
|
|
"premiumize": update_pm_cache_status,
|
|
"qbittorrent": update_qbittorrent_cache_status,
|
|
}
|
|
|
|
# Define provider-specific downloaded info hashes fetch functions
|
|
FETCH_DOWNLOADED_INFO_HASHES_FUNCTIONS = {
|
|
"alldebrid": fetch_downloaded_info_hashes_from_ad,
|
|
"debridlink": fetch_downloaded_info_hashes_from_dl,
|
|
"offcloud": fetch_downloaded_info_hashes_from_oc,
|
|
"pikpak": fetch_downloaded_info_hashes_from_pikpak,
|
|
"realdebrid": fetch_downloaded_info_hashes_from_rd,
|
|
"seedr": fetch_downloaded_info_hashes_from_seedr,
|
|
"torbox": fetch_downloaded_info_hashes_from_torbox,
|
|
"premiumize": fetch_downloaded_info_hashes_from_premiumize,
|
|
"qbittorrent": fetch_info_hashes_from_webdav,
|
|
}
|
|
|
|
|
|
async def filter_and_sort_streams(
|
|
streams: list[TorrentStreams], user_data: UserData
|
|
) -> list[TorrentStreams]:
|
|
# Convert to sets for faster lookups
|
|
selected_catalogs_set = set(user_data.selected_catalogs)
|
|
selected_resolutions_set = set(user_data.selected_resolutions)
|
|
|
|
# Step 1: Filter streams by selected catalogs, resolutions, and size
|
|
filtered_streams = [
|
|
stream
|
|
for stream in streams
|
|
if any(catalog_id in selected_catalogs_set for catalog_id in stream.catalog)
|
|
and stream.resolution in selected_resolutions_set
|
|
and stream.size <= user_data.max_size
|
|
]
|
|
|
|
if not filtered_streams:
|
|
return []
|
|
|
|
# Step 2: Update cache status based on provider
|
|
cache_update_function = CACHE_UPDATE_FUNCTIONS.get(
|
|
user_data.streaming_provider.service
|
|
if user_data.streaming_provider
|
|
else "torrent"
|
|
)
|
|
if cache_update_function:
|
|
if asyncio.iscoroutinefunction(cache_update_function):
|
|
await cache_update_function(filtered_streams, user_data)
|
|
else:
|
|
await asyncio.to_thread(cache_update_function, filtered_streams, user_data)
|
|
|
|
# Step 3: Dynamically sort streams based on user preferences
|
|
def dynamic_sort_key(stream):
|
|
# Compute sort key values only once per stream
|
|
sort_key_values = {
|
|
sort_key: (
|
|
getattr(stream, sort_key)
|
|
if getattr(stream, sort_key) is not None
|
|
else 0
|
|
)
|
|
for sort_key in user_data.torrent_sorting_priority
|
|
}
|
|
|
|
# Create the sort tuple, using resolution ranking for resolution sorting
|
|
return tuple(
|
|
const.RESOLUTION_RANKING[stream.resolution]
|
|
if sort_key == "resolution"
|
|
else sort_key_values[sort_key]
|
|
for sort_key in user_data.torrent_sorting_priority
|
|
)
|
|
|
|
dynamically_sorted_streams = sorted(
|
|
filtered_streams, key=dynamic_sort_key, reverse=True
|
|
)
|
|
|
|
# Step 4: Limit streams per resolution based on user preference, after dynamic sorting
|
|
limited_streams = []
|
|
streams_count_per_resolution = {}
|
|
for stream in dynamically_sorted_streams:
|
|
count = streams_count_per_resolution.get(stream.resolution, 0)
|
|
if count < user_data.max_streams_per_resolution:
|
|
limited_streams.append(stream)
|
|
streams_count_per_resolution[stream.resolution] = count + 1
|
|
|
|
return limited_streams
|
|
|
|
|
|
async def parse_stream_data(
|
|
streams: list[TorrentStreams],
|
|
user_data: UserData,
|
|
secret_str: str,
|
|
season: int = None,
|
|
episode: int = None,
|
|
) -> list[Stream]:
|
|
stream_list = []
|
|
streams = await filter_and_sort_streams(streams, user_data)
|
|
|
|
# Compute values that do not change per iteration outside the loop
|
|
show_full_torrent_name = user_data.show_full_torrent_name
|
|
streaming_provider_name = (
|
|
user_data.streaming_provider.service.title()
|
|
if user_data.streaming_provider
|
|
else "Torrent"
|
|
)
|
|
has_streaming_provider = user_data.streaming_provider is not None
|
|
base_proxy_url_template = (
|
|
f"{settings.host_url}/streaming_provider/{secret_str}/stream?info_hash={{}}"
|
|
if has_streaming_provider
|
|
else None
|
|
)
|
|
|
|
for stream_data in streams:
|
|
episode_data = stream_data.get_episode(season, episode)
|
|
|
|
if show_full_torrent_name:
|
|
torrent_name = (
|
|
f"{stream_data.torrent_name}/{episode_data.title}"
|
|
if episode_data
|
|
else stream_data.torrent_name
|
|
)
|
|
torrent_name = "📂 " + torrent_name.replace(".torrent", "").replace(".", " ")
|
|
else:
|
|
torrent_name = None
|
|
|
|
quality_detail_parts = [
|
|
("📺 " + stream_data.quality) if stream_data.quality else None,
|
|
("🎞️ " + stream_data.codec) if stream_data.codec else None,
|
|
("🎵 " + stream_data.audio) if stream_data.audio else None,
|
|
]
|
|
quality_detail = " ".join(filter(None, quality_detail_parts))
|
|
|
|
resolution = stream_data.resolution.upper() if stream_data.resolution else "N/A"
|
|
streaming_provider_status = "⚡️" if stream_data.cached else "⏳"
|
|
|
|
seeders_info = (
|
|
f"👤 {stream_data.seeders}" if stream_data.seeders is not None else None
|
|
)
|
|
if episode_data and episode_data.size:
|
|
size_info = f"{convert_bytes_to_readable(episode_data.size)} / {convert_bytes_to_readable(stream_data.size)}"
|
|
else:
|
|
size_info = convert_bytes_to_readable(stream_data.size)
|
|
|
|
languages = (
|
|
"🌐 " + " + ".join(stream_data.languages) if stream_data.languages else None
|
|
)
|
|
source_info = f"🔗 {stream_data.source}"
|
|
|
|
primary_info = torrent_name if show_full_torrent_name else quality_detail
|
|
secondary_info = " ".join(filter(None, [size_info, seeders_info]))
|
|
|
|
description_parts = [
|
|
primary_info,
|
|
secondary_info,
|
|
languages,
|
|
source_info,
|
|
]
|
|
description = "\n".join(filter(None, description_parts))
|
|
|
|
stream_details = {
|
|
"name": f"MediaFusion {streaming_provider_name} {resolution} {streaming_provider_status}",
|
|
"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}-{resolution}",
|
|
},
|
|
}
|
|
|
|
if has_streaming_provider:
|
|
base_proxy_url = base_proxy_url_template.format(stream_data.id) + (
|
|
f"&season={season}&episode={episode}" if episode_data else ""
|
|
)
|
|
stream_details.update(
|
|
{"url": base_proxy_url, "behaviorHints": {"notWebReady": True}}
|
|
)
|
|
stream_details.pop("infoHash", None)
|
|
stream_details.pop("fileIdx", None)
|
|
|
|
stream_list.append(Stream(**stream_details))
|
|
|
|
return stream_list
|
|
|
|
|
|
def convert_bytes_to_readable(size_bytes: int) -> str:
|
|
"""
|
|
Convert a size in bytes into a more human-readable format.
|
|
"""
|
|
if not size_bytes:
|
|
return ""
|
|
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]}"
|
|
|
|
|
|
def convert_size_to_bytes(size_str: str) -> int:
|
|
"""Convert size string to bytes."""
|
|
match = re.match(r"(\d+(?:\.\d+)?)\s*(GB|MB)", size_str, re.IGNORECASE)
|
|
if match:
|
|
size, unit = match.groups()
|
|
size = float(size)
|
|
return int(size * 1024**3) if "GB" in unit.upper() else int(size * 1024**2)
|
|
return 0
|
|
|
|
|
|
def get_catalogs(catalog: str, languages: list[str]) -> list[str]:
|
|
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]
|
|
|
|
|
|
def search_imdb(title: str, year: int, retry: int = 5) -> dict:
|
|
try:
|
|
result = ia.search_movie(f"{title} {year}")
|
|
except Exception:
|
|
return search_imdb(title, year, retry - 1) if retry > 0 else {}
|
|
for movie in result:
|
|
if movie.get("year") == year and movie.get("title").lower() in title.lower():
|
|
imdb_id = f"tt{movie.movieID}"
|
|
poster = f"https://live.metahub.space/poster/small/{imdb_id}/img"
|
|
if requests.get(poster).status_code == 200:
|
|
return {
|
|
"imdb_id": imdb_id,
|
|
"poster": poster.replace("small", "medium"),
|
|
"background": f"https://live.metahub.space/background/medium/{imdb_id}/img",
|
|
"title": movie.myTitle,
|
|
}
|
|
poster = movie.get("full-size cover url")
|
|
return {
|
|
"imdb_id": imdb_id,
|
|
"poster": poster,
|
|
"background": poster,
|
|
"title": movie.myTitle,
|
|
}
|
|
return {}
|
|
|
|
|
|
def get_imdb_data(video_id: str) -> tuple[str, str]:
|
|
try:
|
|
movie = ia.get_movie(video_id.removeprefix("tt"), info="main")
|
|
except Exception:
|
|
return "", ""
|
|
return movie.get("title"), movie.get("year")
|
|
|
|
|
|
def parse_tv_stream_data(tv_data: MediaFusionTVMetaData) -> list[Stream]:
|
|
stream_list = []
|
|
for stream in tv_data.streams:
|
|
if stream.behaviorHints.get("is_redirect", False):
|
|
response = requests.get(
|
|
stream.url,
|
|
headers=stream.behaviorHints["proxyHeaders"]["request"],
|
|
allow_redirects=False,
|
|
)
|
|
if response.status_code == 302:
|
|
stream.url = response.headers["Location"]
|
|
stream_list.append(
|
|
Stream(
|
|
name="MediaFusion",
|
|
description=f"{stream.name}, {tv_data.tv_language}, {stream.source}",
|
|
url=stream.url,
|
|
ytId=stream.ytId,
|
|
behaviorHints=stream.behaviorHints,
|
|
)
|
|
)
|
|
|
|
return stream_list
|
|
|
|
|
|
async def fetch_downloaded_info_hashes(user_data: UserData) -> list[str]:
|
|
if fetch_downloaded_info_hashes_function := FETCH_DOWNLOADED_INFO_HASHES_FUNCTIONS.get(
|
|
user_data.streaming_provider.service
|
|
):
|
|
if asyncio.iscoroutinefunction(fetch_downloaded_info_hashes_function):
|
|
downloaded_info_hashes = await fetch_downloaded_info_hashes_function(
|
|
user_data
|
|
)
|
|
else:
|
|
downloaded_info_hashes = await asyncio.to_thread(
|
|
fetch_downloaded_info_hashes_function, user_data
|
|
)
|
|
|
|
return downloaded_info_hashes
|
|
|
|
return []
|
|
|
|
|
|
def generate_manifest(manifest: dict, user_data: UserData) -> dict:
|
|
resources = manifest.get("resources", [])
|
|
|
|
# Ensure catalogs are enabled
|
|
if user_data.enable_catalogs:
|
|
# Reorder catalogs based on the user's selection order
|
|
ordered_catalogs = []
|
|
for catalog_id in user_data.selected_catalogs:
|
|
for catalog in manifest.get("catalogs", []):
|
|
if catalog["id"] == catalog_id:
|
|
ordered_catalogs.append(catalog)
|
|
break
|
|
|
|
manifest["catalogs"] = ordered_catalogs
|
|
else:
|
|
# If catalogs are not enabled, clear them from the manifest
|
|
manifest["catalogs"] = []
|
|
# Define a default stream resource if catalogs are disabled
|
|
resources = [
|
|
{
|
|
"name": "stream",
|
|
"types": ["movie", "series", "tv"],
|
|
"idPrefixes": ["tt", "mf"],
|
|
}
|
|
]
|
|
|
|
# Adjust manifest details based on the selected streaming provider
|
|
if user_data.streaming_provider:
|
|
provider_name = user_data.streaming_provider.service.title()
|
|
manifest["name"] += f" {provider_name}"
|
|
manifest["id"] += f".{provider_name.lower()}"
|
|
|
|
# Include watchlist catalogs if enabled
|
|
if user_data.streaming_provider.enable_watchlist_catalogs:
|
|
watchlist_catalogs = [
|
|
{
|
|
"id": f"{provider_name.lower()}_watchlist_movies",
|
|
"name": f"{provider_name} Watchlist",
|
|
"type": "movie",
|
|
"extra": [{"name": "skip", "isRequired": False}],
|
|
},
|
|
{
|
|
"id": f"{provider_name.lower()}_watchlist_series",
|
|
"name": f"{provider_name} Watchlist",
|
|
"type": "series",
|
|
"extra": [{"name": "skip", "isRequired": False}],
|
|
},
|
|
]
|
|
# Prepend watchlist catalogs to the sorted user-selected catalogs
|
|
manifest["catalogs"] = watchlist_catalogs + manifest["catalogs"]
|
|
resources = manifest["resources"]
|
|
|
|
# Ensure the resource list is updated accordingly
|
|
manifest["resources"] = resources
|
|
return manifest
|
|
|
|
|
|
def is_contain_18_plus_keywords(title: str) -> bool:
|
|
"""
|
|
Check if the title contains 18+ keywords to filter out adult content.
|
|
"""
|
|
return ADULT_CONTENT_KEYWORDS.search(title) is not None
|