From 7785e8c60404a2bb715dd7be3c83474a389397fd Mon Sep 17 00:00:00 2001 From: UrloMythus Date: Sun, 11 Jan 2026 14:29:22 +0100 Subject: [PATCH] new version --- mediaflow_proxy/configs.py | 29 +- mediaflow_proxy/extractors/F16Px.py | 104 ++ mediaflow_proxy/extractors/base.py | 117 +- mediaflow_proxy/extractors/dlhd.py | 791 +++++------- mediaflow_proxy/extractors/factory.py | 21 +- mediaflow_proxy/extractors/filelions.py | 3 +- mediaflow_proxy/extractors/filemoon.py | 52 + mediaflow_proxy/extractors/lulustream.py | 27 + mediaflow_proxy/extractors/sportsonline.py | 195 +++ mediaflow_proxy/extractors/streamwish.py | 81 ++ mediaflow_proxy/extractors/turbovidplay.py | 68 + mediaflow_proxy/extractors/vavoo.py | 99 +- mediaflow_proxy/extractors/vidmoly.py | 63 + mediaflow_proxy/extractors/vidoza.py | 73 ++ mediaflow_proxy/extractors/voe.py | 68 + mediaflow_proxy/handlers.py | 31 +- mediaflow_proxy/mpd_processor.py | 52 +- mediaflow_proxy/routes/extractor.py | 38 +- mediaflow_proxy/routes/playlist_builder.py | 209 ++- mediaflow_proxy/routes/proxy.py | 396 +++++- mediaflow_proxy/schemas.py | 15 +- .../__pycache__/all_debrid.cpython-313.pyc | Bin 3375 -> 0 bytes .../__pycache__/base.cpython-313.pyc | Bin 1615 -> 0 bytes .../__pycache__/real_debrid.cpython-313.pyc | Bin 2539 -> 0 bytes mediaflow_proxy/static/index.html | 11 +- mediaflow_proxy/static/playlist_builder.html | 17 +- mediaflow_proxy/static/speedtest.js | 8 +- mediaflow_proxy/utils/aes.py | 39 + mediaflow_proxy/utils/aesgcm.py | 193 +++ mediaflow_proxy/utils/cache_utils.py | 67 +- mediaflow_proxy/utils/codec.py | 465 +++++++ mediaflow_proxy/utils/compat.py | 231 ++++ mediaflow_proxy/utils/constanttime.py | 218 ++++ mediaflow_proxy/utils/cryptomath.py | 366 ++++++ mediaflow_proxy/utils/deprecations.py | 218 ++++ mediaflow_proxy/utils/hls_prebuffer.py | 317 +++-- mediaflow_proxy/utils/hls_utils.py | 54 + mediaflow_proxy/utils/http_utils.py | 154 ++- mediaflow_proxy/utils/m3u8_processor.py | 23 +- mediaflow_proxy/utils/mpd_utils.py | 10 +- mediaflow_proxy/utils/python_aes.py | 122 ++ mediaflow_proxy/utils/python_aesgcm.py | 12 + mediaflow_proxy/utils/rijndael.py | 1118 +++++++++++++++++ mediaflow_proxy/utils/tlshashlib.py | 32 + mediaflow_proxy/utils/tlshmac.py | 88 ++ 45 files changed, 5463 insertions(+), 832 deletions(-) create mode 100644 mediaflow_proxy/extractors/F16Px.py create mode 100644 mediaflow_proxy/extractors/filemoon.py create mode 100644 mediaflow_proxy/extractors/lulustream.py create mode 100644 mediaflow_proxy/extractors/sportsonline.py create mode 100644 mediaflow_proxy/extractors/streamwish.py create mode 100644 mediaflow_proxy/extractors/turbovidplay.py create mode 100644 mediaflow_proxy/extractors/vidmoly.py create mode 100644 mediaflow_proxy/extractors/vidoza.py create mode 100644 mediaflow_proxy/extractors/voe.py delete mode 100644 mediaflow_proxy/speedtest/providers/__pycache__/all_debrid.cpython-313.pyc delete mode 100644 mediaflow_proxy/speedtest/providers/__pycache__/base.cpython-313.pyc delete mode 100644 mediaflow_proxy/speedtest/providers/__pycache__/real_debrid.cpython-313.pyc create mode 100644 mediaflow_proxy/utils/aes.py create mode 100644 mediaflow_proxy/utils/aesgcm.py create mode 100644 mediaflow_proxy/utils/codec.py create mode 100644 mediaflow_proxy/utils/compat.py create mode 100644 mediaflow_proxy/utils/constanttime.py create mode 100644 mediaflow_proxy/utils/cryptomath.py create mode 100644 mediaflow_proxy/utils/deprecations.py create mode 100644 mediaflow_proxy/utils/hls_utils.py create mode 100644 mediaflow_proxy/utils/python_aes.py create mode 100644 mediaflow_proxy/utils/python_aesgcm.py create mode 100644 mediaflow_proxy/utils/rijndael.py create mode 100644 mediaflow_proxy/utils/tlshashlib.py create mode 100644 mediaflow_proxy/utils/tlshmac.py diff --git a/mediaflow_proxy/configs.py b/mediaflow_proxy/configs.py index 049105a..bae1ddf 100644 --- a/mediaflow_proxy/configs.py +++ b/mediaflow_proxy/configs.py @@ -19,11 +19,14 @@ class TransportConfig(BaseSettings): proxy_url: Optional[str] = Field( None, description="Primary proxy URL. Example: socks5://user:pass@proxy:1080 or http://proxy:8080" ) + disable_ssl_verification_globally: bool = Field( + False, description="Disable SSL verification for all requests globally." + ) all_proxy: bool = Field(False, description="Enable proxy for all routes by default") transport_routes: Dict[str, RouteConfig] = Field( default_factory=dict, description="Pattern-based route configuration" ) - timeout: int = Field(30, description="Timeout for HTTP requests in seconds") + timeout: int = Field(60, description="Timeout for HTTP requests in seconds") def get_mounts( self, async_http: bool = True @@ -33,11 +36,13 @@ class TransportConfig(BaseSettings): """ mounts = {} transport_cls = httpx.AsyncHTTPTransport if async_http else httpx.HTTPTransport + global_verify = not self.disable_ssl_verification_globally # Configure specific routes for pattern, route in self.transport_routes.items(): mounts[pattern] = transport_cls( - verify=route.verify_ssl, proxy=route.proxy_url or self.proxy_url if route.proxy else None + verify=route.verify_ssl if global_verify else False, + proxy=route.proxy_url or self.proxy_url if route.proxy else None, ) # Hardcoded configuration for jxoplay.xyz domain - SSL verification disabled @@ -45,9 +50,23 @@ class TransportConfig(BaseSettings): verify=False, proxy=self.proxy_url if self.all_proxy else None ) + mounts["all://dlhd.dad"] = transport_cls( + verify=False, proxy=self.proxy_url if self.all_proxy else None + ) + + mounts["all://*.newkso.ru"] = transport_cls( + verify=False, proxy=self.proxy_url if self.all_proxy else None + ) + + # Apply global settings for proxy and SSL + default_proxy_url = self.proxy_url if self.all_proxy else None + if default_proxy_url or not global_verify: + mounts["all://"] = transport_cls(proxy=default_proxy_url, verify=global_verify) + # Set default proxy for all routes if enabled - if self.all_proxy: - mounts["all://"] = transport_cls(proxy=self.proxy_url) + # This part is now handled above to combine proxy and SSL settings + # if self.all_proxy: + # mounts["all://"] = transport_cls(proxy=self.proxy_url) return mounts @@ -78,6 +97,8 @@ class Settings(BaseSettings): dash_prebuffer_cache_size: int = 50 # Maximum number of segments to cache in memory. dash_prebuffer_max_memory_percent: int = 80 # Maximum percentage of system memory to use for DASH pre-buffer cache. dash_prebuffer_emergency_threshold: int = 90 # Emergency threshold percentage to trigger aggressive cache cleanup. + mpd_live_init_cache_ttl: int = 0 # TTL (seconds) for live init segment cache; 0 disables caching. + mpd_live_playlist_depth: int = 8 # Number of recent segments to expose per live playlist variant. user_agent: str = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" # The user agent to use for HTTP requests. diff --git a/mediaflow_proxy/extractors/F16Px.py b/mediaflow_proxy/extractors/F16Px.py new file mode 100644 index 0000000..fbb7708 --- /dev/null +++ b/mediaflow_proxy/extractors/F16Px.py @@ -0,0 +1,104 @@ +# https://github.com/Gujal00/ResolveURL/blob/55c7f66524ebd65bc1f88650614e627b00167fa0/script.module.resolveurl/lib/resolveurl/plugins/f16px.py + +import base64 +import json +import re +from typing import Dict, Any +from urllib.parse import urlparse + +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError +from mediaflow_proxy.utils import python_aesgcm + + +class F16PxExtractor(BaseExtractor): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mediaflow_endpoint = "hls_manifest_proxy" + + @staticmethod + def _b64url_decode(value: str) -> bytes: + # base64url -> base64 + value = value.replace("-", "+").replace("_", "/") + padding = (-len(value)) % 4 + if padding: + value += "=" * padding + return base64.b64decode(value) + + def _join_key_parts(self, parts) -> bytes: + return b"".join(self._b64url_decode(p) for p in parts) + + async def extract(self, url: str) -> Dict[str, Any]: + parsed = urlparse(url) + host = parsed.netloc + origin = f"{parsed.scheme}://{parsed.netloc}" + + match = re.search(r"/e/([A-Za-z0-9]+)", parsed.path or "") + if not match: + raise ExtractorError("F16PX: Invalid embed URL") + + media_id = match.group(1) + api_url = f"https://{host}/api/videos/{media_id}/embed/playback" + + headers = self.base_headers.copy() + headers["referer"] = f"https://{host}/" + + resp = await self._make_request(api_url, headers=headers) + try: + data = resp.json() + except Exception: + raise ExtractorError("F16PX: Invalid JSON response") + + # Case 1: plain sources + if "sources" in data and data["sources"]: + src = data["sources"][0].get("url") + if not src: + raise ExtractorError("F16PX: Empty source URL") + return { + "destination_url": src, + "request_headers": headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } + + # Case 2: encrypted playback + pb = data.get("playback") + if not pb: + raise ExtractorError("F16PX: No playback data") + + try: + iv = self._b64url_decode(pb["iv"]) # nonce + key = self._join_key_parts(pb["key_parts"]) # AES key + payload = self._b64url_decode(pb["payload"]) # ciphertext + tag + + cipher = python_aesgcm.new(key) + decrypted = cipher.open(iv, payload) # AAD = '' like ResolveURL + + if decrypted is None: + raise ExtractorError("F16PX: GCM authentication failed") + + decrypted_json = json.loads(decrypted.decode("utf-8", "ignore")) + + except ExtractorError: + raise + except Exception as e: + raise ExtractorError(f"F16PX: Decryption failed ({e})") + + sources = decrypted_json.get("sources") or [] + if not sources: + raise ExtractorError("F16PX: No sources after decryption") + + best = sources[0].get("url") + if not best: + raise ExtractorError("F16PX: Empty source URL after decryption") + + self.base_headers.clear() + self.base_headers["referer"] = f"{origin}/" + self.base_headers["origin"] = origin + self.base_headers["Accept-Language"] = "en-US,en;q=0.5" + self.base_headers["Accept"] = "*/*" + self.base_headers['user-agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0' + + return { + "destination_url": best, + "request_headers": self.base_headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } diff --git a/mediaflow_proxy/extractors/base.py b/mediaflow_proxy/extractors/base.py index 359fc5d..0e271a7 100644 --- a/mediaflow_proxy/extractors/base.py +++ b/mediaflow_proxy/extractors/base.py @@ -1,48 +1,121 @@ from abc import ABC, abstractmethod from typing import Dict, Optional, Any +import asyncio import httpx +import logging from mediaflow_proxy.configs import settings -from mediaflow_proxy.utils.http_utils import create_httpx_client +from mediaflow_proxy.utils.http_utils import create_httpx_client, DownloadError + +logger = logging.getLogger(__name__) class ExtractorError(Exception): """Base exception for all extractors.""" - pass class BaseExtractor(ABC): - """Base class for all URL extractors.""" + """Base class for all URL extractors. + + Improvements: + - Built-in retry/backoff for transient network errors + - Configurable timeouts and per-request overrides + - Better logging of non-200 responses and body previews for debugging + """ def __init__(self, request_headers: dict): self.base_headers = { "user-agent": settings.user_agent, } self.mediaflow_endpoint = "proxy_stream_endpoint" - self.base_headers.update(request_headers) + # merge incoming headers (e.g. Accept-Language / Referer) with default base headers + self.base_headers.update(request_headers or {}) async def _make_request( - self, url: str, method: str = "GET", headers: Optional[Dict] = None, **kwargs + self, + url: str, + method: str = "GET", + headers: Optional[Dict] = None, + timeout: Optional[float] = None, + retries: int = 3, + backoff_factor: float = 0.5, + raise_on_status: bool = True, + **kwargs, ) -> httpx.Response: - """Make HTTP request with error handling.""" - try: - async with create_httpx_client() as client: - request_headers = self.base_headers.copy() - request_headers.update(headers or {}) - response = await client.request( - method, - url, - headers=request_headers, - **kwargs, - ) - response.raise_for_status() - return response - except httpx.HTTPError as e: - raise ExtractorError(f"HTTP request failed for URL {url}: {str(e)}") - except Exception as e: - raise ExtractorError(f"Request failed for URL {url}: {str(e)}") + """ + Make HTTP request with retry and timeout support. + + Parameters + ---------- + timeout : float | None + Seconds to wait for the request (applied to httpx.Timeout). Defaults to 15s. + retries : int + Number of attempts for transient errors. + backoff_factor : float + Base for exponential backoff between retries. + raise_on_status : bool + If True, HTTP non-2xx raises DownloadError (preserves status code). + """ + attempt = 0 + last_exc = None + + # build request headers merging base and per-request + request_headers = self.base_headers.copy() + if headers: + request_headers.update(headers) + + timeout_cfg = httpx.Timeout(timeout or 15.0) + + while attempt < retries: + try: + async with create_httpx_client(timeout=timeout_cfg) as client: + response = await client.request( + method, + url, + headers=request_headers, + **kwargs, + ) + + if raise_on_status: + try: + response.raise_for_status() + except httpx.HTTPStatusError as e: + # Provide a short body preview for debugging + body_preview = "" + try: + body_preview = e.response.text[:500] + except Exception: + body_preview = "" + logger.debug( + "HTTPStatusError for %s (status=%s) -- body preview: %s", + url, + e.response.status_code, + body_preview, + ) + raise DownloadError(e.response.status_code, f"HTTP error {e.response.status_code} while requesting {url}") + return response + + except DownloadError: + # Do not retry on explicit HTTP status errors (they are intentional) + raise + except (httpx.ReadTimeout, httpx.ConnectTimeout, httpx.NetworkError, httpx.TransportError) as e: + # Transient network error — retry with backoff + last_exc = e + attempt += 1 + sleep_for = backoff_factor * (2 ** (attempt - 1)) + logger.warning("Transient network error (attempt %s/%s) for %s: %s — retrying in %.1fs", + attempt, retries, url, e, sleep_for) + await asyncio.sleep(sleep_for) + continue + except Exception as e: + # Unexpected exception — wrap as ExtractorError to keep interface consistent + logger.exception("Unhandled exception while requesting %s: %s", url, e) + raise ExtractorError(f"Request failed for URL {url}: {str(e)}") + + logger.error("All retries failed for %s: %s", url, last_exc) + raise ExtractorError(f"Request failed for URL {url}: {str(last_exc)}") @abstractmethod async def extract(self, url: str, **kwargs) -> Dict[str, Any]: diff --git a/mediaflow_proxy/extractors/dlhd.py b/mediaflow_proxy/extractors/dlhd.py index 2d58d5a..940e696 100644 --- a/mediaflow_proxy/extractors/dlhd.py +++ b/mediaflow_proxy/extractors/dlhd.py @@ -1,543 +1,332 @@ import re import base64 import logging -from typing import Any, Dict, Optional -from urllib.parse import urlparse, quote, urlunparse + +from typing import Any, Dict, Optional, List +from urllib.parse import urlparse, quote_plus, urljoin + + +import httpx + from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError + logger = logging.getLogger(__name__) +# Silenzia l'errore ConnectionResetError su Windows +logging.getLogger('asyncio').setLevel(logging.CRITICAL) + class DLHDExtractor(BaseExtractor): - """DLHD (DaddyLive) URL extractor for M3U8 streams.""" + """DLHD (DaddyLive) URL extractor for M3U8 streams. + + + Notes: + - Multi-domain support for daddylive.sx / dlhd.dad + - Robust extraction of auth parameters and server lookup + - Uses retries/timeouts via BaseExtractor where possible + - Multi-iframe fallback for resilience + """ + def __init__(self, request_headers: dict): super().__init__(request_headers) - # Default to HLS proxy endpoint self.mediaflow_endpoint = "hls_manifest_proxy" - # Cache for the resolved base URL to avoid repeated network calls - self._cached_base_url = None - # Store iframe context for newkso.ru requests - self._iframe_context = None + self._iframe_context: Optional[str] = None - def _get_headers_for_url(self, url: str, base_headers: dict) -> dict: - """Get appropriate headers for the given URL, applying newkso.ru specific headers if needed.""" - headers = base_headers.copy() - - # Check if URL contains newkso.ru domain - parsed_url = urlparse(url) - if "newkso.ru" in parsed_url.netloc: - # Use iframe URL as referer if available, otherwise use the newkso domain itself - if self._iframe_context: - iframe_origin = f"https://{urlparse(self._iframe_context).netloc}" - newkso_headers = { - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', - 'Referer': self._iframe_context, - 'Origin': iframe_origin - } - logger.info(f"Applied newkso.ru specific headers with iframe context for URL: {url}") - logger.debug(f"Headers applied: {newkso_headers}") - else: - # Fallback to newkso domain itself - newkso_origin = f"{parsed_url.scheme}://{parsed_url.netloc}" - newkso_headers = { - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', - 'Referer': newkso_origin, - 'Origin': newkso_origin - } - logger.info(f"Applied newkso.ru specific headers (fallback) for URL: {url}") - logger.debug(f"Headers applied: {newkso_headers}") + + + async def _make_request(self, url: str, method: str = "GET", headers: Optional[Dict] = None, **kwargs) -> Any: + """Override to disable SSL verification for this extractor and use fetch_with_retry if available.""" + from mediaflow_proxy.utils.http_utils import create_httpx_client, fetch_with_retry + + + timeout = kwargs.pop("timeout", 15) + retries = kwargs.pop("retries", 3) + backoff_factor = kwargs.pop("backoff_factor", 0.5) + + + async with create_httpx_client(verify=False, timeout=httpx.Timeout(timeout)) as client: + try: + return await fetch_with_retry(client, method, url, headers or {}, timeout=timeout) + except Exception: + logger.debug("fetch_with_retry failed or unavailable; falling back to direct request for %s", url) + response = await client.request(method, url, headers=headers or {}, timeout=timeout) + response.raise_for_status() + return response + + + async def _extract_lovecdn_stream(self, iframe_url: str, iframe_content: str, headers: dict) -> Dict[str, Any]: + """ + Estrattore alternativo per iframe lovecdn.ru che usa un formato diverso. + """ + try: + # Cerca pattern di stream URL diretto + m3u8_patterns = [ + r'["\']([^"\']*\.m3u8[^"\']*)["\']', + r'source[:\s]+["\']([^"\']+)["\']', + r'file[:\s]+["\']([^"\']+\.m3u8[^"\']*)["\']', + r'hlsManifestUrl[:\s]*["\']([^"\']+)["\']', + ] - headers.update(newkso_headers) - - return headers + stream_url = None + for pattern in m3u8_patterns: + matches = re.findall(pattern, iframe_content) + for match in matches: + if '.m3u8' in match and match.startswith('http'): + stream_url = match + logger.info(f"Found direct m3u8 URL: {stream_url}") + break + if stream_url: + break + + # Pattern 2: Cerca costruzione dinamica URL + if not stream_url: + channel_match = re.search(r'(?:stream|channel)["\s:=]+["\']([^"\']+)["\']', iframe_content) + server_match = re.search(r'(?:server|domain|host)["\s:=]+["\']([^"\']+)["\']', iframe_content) + + if channel_match: + channel_name = channel_match.group(1) + server = server_match.group(1) if server_match else 'newkso.ru' + stream_url = f"https://{server}/{channel_name}/mono.m3u8" + logger.info(f"Constructed stream URL: {stream_url}") + + if not stream_url: + # Fallback: cerca qualsiasi URL che sembri uno stream + url_pattern = r'https?://[^\s"\'<>]+\.m3u8[^\s"\'<>]*' + matches = re.findall(url_pattern, iframe_content) + if matches: + stream_url = matches[0] + logger.info(f"Found fallback stream URL: {stream_url}") + + if not stream_url: + raise ExtractorError(f"Could not find stream URL in lovecdn.ru iframe") + + # Usa iframe URL come referer + iframe_origin = f"https://{urlparse(iframe_url).netloc}" + stream_headers = { + 'User-Agent': headers['User-Agent'], + 'Referer': iframe_url, + 'Origin': iframe_origin + } + + # Determina endpoint in base al dominio dello stream + endpoint = "hls_key_proxy" - async def _make_request(self, url: str, method: str = "GET", headers: dict = None, **kwargs): - """Override _make_request to apply newkso.ru specific headers when needed.""" - request_headers = headers or {} + logger.info(f"Using lovecdn.ru stream with endpoint: {endpoint}") + + return { + "destination_url": stream_url, + "request_headers": stream_headers, + "mediaflow_endpoint": endpoint, + } + + except Exception as e: + raise ExtractorError(f"Failed to extract lovecdn.ru stream: {e}") + + async def _extract_new_auth_flow(self, iframe_url: str, iframe_content: str, headers: dict) -> Dict[str, Any]: + """Handles the new authentication flow found in recent updates.""" - # Apply newkso.ru specific headers if the URL contains newkso.ru - final_headers = self._get_headers_for_url(url, request_headers) + def _extract_params(js: str) -> Dict[str, Optional[str]]: + params = {} + patterns = { + "channel_key": r'(?:const|var|let)\s+(?:CHANNEL_KEY|channelKey)\s*=\s*["\']([^"\']+)["\']', + "auth_token": r'(?:const|var|let)\s+AUTH_TOKEN\s*=\s*["\']([^"\']+)["\']', + "auth_country": r'(?:const|var|let)\s+AUTH_COUNTRY\s*=\s*["\']([^"\']+)["\']', + "auth_ts": r'(?:const|var|let)\s+AUTH_TS\s*=\s*["\']([^"\']+)["\']', + "auth_expiry": r'(?:const|var|let)\s+AUTH_EXPIRY\s*=\s*["\']([^"\']+)["\']', + } + for key, pattern in patterns.items(): + match = re.search(pattern, js) + params[key] = match.group(1) if match else None + return params + + params = _extract_params(iframe_content) - return await super()._make_request(url, method, final_headers, **kwargs) + missing_params = [k for k, v in params.items() if not v] + if missing_params: + # This is not an error, just means it's not the new flow + raise ExtractorError(f"Not the new auth flow: missing params {missing_params}") + + logger.info("New auth flow detected. Proceeding with POST auth.") + + # 1. Initial Auth POST + auth_url = 'https://security.newkso.ru/auth2.php' + # Use files parameter to force multipart/form-data which is required by the server + # (None, value) tells httpx to send it as a form field, not a file upload + multipart_data = { + 'channelKey': (None, params["channel_key"]), + 'country': (None, params["auth_country"]), + 'timestamp': (None, params["auth_ts"]), + 'expiry': (None, params["auth_expiry"]), + 'token': (None, params["auth_token"]), + } + + iframe_origin = f"https://{urlparse(iframe_url).netloc}" + auth_headers = headers.copy() + auth_headers.update({ + 'Accept': '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + 'Origin': iframe_origin, + 'Referer': iframe_url, + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site', + 'Priority': 'u=1, i', + }) + + from mediaflow_proxy.utils.http_utils import create_httpx_client + try: + async with create_httpx_client(verify=False) as client: + # Note: using 'files' instead of 'data' to ensure multipart/form-data Content-Type + auth_resp = await client.post(auth_url, files=multipart_data, headers=auth_headers, timeout=12) + auth_resp.raise_for_status() + auth_data = auth_resp.json() + if not (auth_data.get("valid") or auth_data.get("success")): + raise ExtractorError(f"Initial auth failed with response: {auth_data}") + logger.info("New auth flow: Initial auth successful.") + except Exception as e: + raise ExtractorError(f"New auth flow failed during initial auth POST: {e}") + + # 2. Server Lookup + server_lookup_url = f"https://{urlparse(iframe_url).netloc}/server_lookup.js?channel_id={params['channel_key']}" + try: + # Use _make_request as it handles retries and expects JSON + lookup_resp = await self._make_request(server_lookup_url, headers=headers, timeout=10) + server_data = lookup_resp.json() + server_key = server_data.get('server_key') + if not server_key: + raise ExtractorError(f"No server_key in lookup response: {server_data}") + logger.info(f"New auth flow: Server lookup successful - Server key: {server_key}") + except Exception as e: + raise ExtractorError(f"New auth flow failed during server lookup: {e}") + + # 3. Build final stream URL + channel_key = params['channel_key'] + auth_token = params['auth_token'] + # The JS logic uses .css, not .m3u8 + if server_key == 'top1/cdn': + stream_url = f'https://top1.newkso.ru/top1/cdn/{channel_key}/mono.css' + else: + stream_url = f'https://{server_key}new.newkso.ru/{server_key}/{channel_key}/mono.css' + + logger.info(f'New auth flow: Constructed stream URL: {stream_url}') + + stream_headers = { + 'User-Agent': headers['User-Agent'], + 'Referer': iframe_url, + 'Origin': iframe_origin, + 'Authorization': f'Bearer {auth_token}', + 'X-Channel-Key': channel_key + } + + return { + "destination_url": stream_url, + "request_headers": stream_headers, + "mediaflow_endpoint": "hls_manifest_proxy", + } async def extract(self, url: str, **kwargs) -> Dict[str, Any]: - """Extract DLHD stream URL and required headers (logica tvproxy adattata async, con fallback su endpoint alternativi).""" - from urllib.parse import urlparse, quote_plus + """Main extraction flow: resolve base, fetch players, extract iframe, auth and final m3u8.""" + baseurl = "https://dlhd.dad/" - async def get_daddylive_base_url(): - if self._cached_base_url: - return self._cached_base_url - try: - resp = await self._make_request("https://daddylive.sx/") - # resp.url is the final URL after redirects - base_url = str(resp.url) - if not base_url.endswith('/'): - base_url += '/' - self._cached_base_url = base_url - return base_url - except Exception: - # Fallback to default if request fails - return "https://daddylive.sx/" - - def extract_channel_id(url): - match_premium = re.search(r'/premium(\d+)/mono\.m3u8$', url) - if match_premium: - return match_premium.group(1) - # Handle both normal and URL-encoded patterns - match_player = re.search(r'/(?:watch|stream|cast|player)/stream-(\d+)\.php', url) - if match_player: - return match_player.group(1) - # Handle URL-encoded patterns like %2Fstream%2Fstream-123.php or just stream-123.php - match_encoded = re.search(r'(?:%2F|/)stream-(\d+)\.php', url, re.IGNORECASE) - if match_encoded: - return match_encoded.group(1) - # Handle direct stream- pattern without path - match_direct = re.search(r'stream-(\d+)\.php', url) - if match_direct: - return match_direct.group(1) + def extract_channel_id(u: str) -> Optional[str]: + match_watch_id = re.search(r'watch\.php\?id=(\d+)', u) + if match_watch_id: + return match_watch_id.group(1) return None - async def try_endpoint(baseurl, endpoint, channel_id): - stream_url = f"{baseurl}{endpoint}stream-{channel_id}.php" + + async def get_stream_data(initial_url: str): daddy_origin = urlparse(baseurl).scheme + "://" + urlparse(baseurl).netloc daddylive_headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', 'Referer': baseurl, 'Origin': daddy_origin } - # 1. Richiesta alla pagina stream/cast/player/watch - resp1 = await self._make_request(stream_url, headers=daddylive_headers) - # 2. Estrai link Player 2 - iframes = re.findall(r']*href="([^"]+)"[^>]*>\s*]*>\s*Player\s*2\s*', resp1.text) - if not iframes: - raise ExtractorError("No Player 2 link found") - url2 = iframes[0] - url2 = baseurl + url2 - url2 = url2.replace('//cast', '/cast') - daddylive_headers['Referer'] = url2 - daddylive_headers['Origin'] = url2 - # 3. Richiesta alla pagina Player 2 - resp2 = await self._make_request(url2, headers=daddylive_headers) - # 4. Estrai iframe - iframes2 = re.findall(r'iframe src="([^"]*)', resp2.text) - if not iframes2: - raise ExtractorError("No iframe found in Player 2 page") - iframe_url = iframes2[0] - # Store iframe context for newkso.ru requests - self._iframe_context = iframe_url - resp3 = await self._make_request(iframe_url, headers=daddylive_headers) - iframe_content = resp3.text - # 5. Estrai parametri auth (robusto) - Handle both old and new formats - def extract_var_old_format(js, name): - # Try multiple patterns for variable extraction (old format) - patterns = [ - rf'var (?:__)?{name}\s*=\s*atob\("([^"]+)"\)', - rf'var (?:__)?{name}\s*=\s*atob\(\'([^\']+)\'\)', - rf'(?:var\s+)?(?:__)?{name}\s*=\s*atob\s*\(\s*["\']([^"\']+)["\']\s*\)', - rf'(?:let|const)\s+(?:__)?{name}\s*=\s*atob\s*\(\s*["\']([^"\']+)["\']\s*\)' - ] - for pattern in patterns: - m = re.search(pattern, js) - if m: - try: - return base64.b64decode(m.group(1)).decode('utf-8') - except Exception as decode_error: - logger.warning(f"Failed to decode base64 for variable {name}: {decode_error}") - continue - return None - - def extract_xjz_format(js): - """Extract parameters from the new XJZ base64-encoded JSON format.""" + + + # 1. Request initial page + resp1 = await self._make_request(initial_url, headers=daddylive_headers, timeout=15) + player_links = re.findall(r']*data-url="([^"]+)"[^>]*>Player\s*\d+', resp1.text) + if not player_links: + raise ExtractorError("No player links found on the page.") + + + # Prova tutti i player e raccogli tutti gli iframe validi + last_player_error = None + iframe_candidates = [] + + for player_url in player_links: try: - # Look for the XJZ variable assignment - xjz_pattern = r'const\s+XJZ\s*=\s*["\']([^"\']+)["\']' - match = re.search(xjz_pattern, js) - if not match: - return None - xjz_b64 = match.group(1) - import json - # Decode the first base64 layer (JSON) - xjz_json = base64.b64decode(xjz_b64).decode('utf-8') - xjz_obj = json.loads(xjz_json) - # Each value is also base64-encoded, decode each - decoded = {} - for k, v in xjz_obj.items(): - try: - decoded[k] = base64.b64decode(v).decode('utf-8') - except Exception as e: - logger.warning(f"Failed to decode XJZ field {k}: {e}") - decoded[k] = v - return decoded - except Exception as e: - logger.warning(f"Failed to extract XJZ format: {e}") - return None + if not player_url.startswith('http'): + player_url = baseurl + player_url.lstrip('/') - def extract_bundle_format(js): - """Extract parameters from new BUNDLE format (legacy fallback).""" + + daddylive_headers['Referer'] = player_url + daddylive_headers['Origin'] = player_url + resp2 = await self._make_request(player_url, headers=daddylive_headers, timeout=12) + iframes2 = re.findall(r' str: - """Lookup server information and generate stream URL.""" - try: - # Construct server lookup URL - server_lookup_url = f"{lookup_url_base}/server_lookup.php?channel_id={quote(auth_data['channel_key'])}" - - # Make server lookup request - server_response = await self._make_request(server_lookup_url, headers=headers) - - server_data = server_response.json() - server_key = server_data.get("server_key") - - if not server_key: - raise ExtractorError("Failed to get server key") - - # Extract domain parts from auth URL for constructing stream URL - auth_domain_parts = urlparse(auth_url_base).netloc.split(".") - domain_suffix = ".".join(auth_domain_parts[1:]) if len(auth_domain_parts) > 1 else auth_domain_parts[0] - - # Generate the m3u8 URL based on server response pattern - if "/" in server_key: - # Handle special case like "top1/cdn" - parts = server_key.split("/") - return f"https://{parts[0]}.{domain_suffix}/{server_key}/{auth_data['channel_key']}/mono.m3u8" - else: - # Handle normal case - return f"https://{server_key}new.{domain_suffix}/{server_key}/{auth_data['channel_key']}/mono.m3u8" - - except Exception as e: - raise ExtractorError(f"Server lookup failed: {str(e)}") - - def _extract_auth_data(self, html_content: str) -> Dict[str, str]: - """Extract authentication data from player page.""" - try: - channel_key_match = re.search(r'var\s+channelKey\s*=\s*["\']([^"\']+)["\']', html_content) - if not channel_key_match: - return {} - channel_key = channel_key_match.group(1) - - # New pattern with atob - auth_ts_match = re.search(r'var\s+__c\s*=\s*atob\([\'"]([^\'"]+)[\'"]\)', html_content) - auth_rnd_match = re.search(r'var\s+__d\s*=\s*atob\([\'"]([^\'"]+)[\'"]\)', html_content) - auth_sig_match = re.search(r'var\s+__e\s*=\s*atob\([\'"]([^\'"]+)[\'"]\)', html_content) - - if auth_ts_match and auth_rnd_match and auth_sig_match: - return { - "channel_key": channel_key, - "auth_ts": base64.b64decode(auth_ts_match.group(1)).decode("utf-8"), - "auth_rnd": base64.b64decode(auth_rnd_match.group(1)).decode("utf-8"), - "auth_sig": base64.b64decode(auth_sig_match.group(1)).decode("utf-8"), - } - - # Original pattern - auth_ts_match = re.search(r'var\s+authTs\s*=\s*["\']([^"\']+)["\']', html_content) - auth_rnd_match = re.search(r'var\s+authRnd\s*=\s*["\']([^"\']+)["\']', html_content) - auth_sig_match = re.search(r'var\s+authSig\s*=\s*["\']([^"\']+)["\']', html_content) - - if auth_ts_match and auth_rnd_match and auth_sig_match: - return { - "channel_key": channel_key, - "auth_ts": auth_ts_match.group(1), - "auth_rnd": auth_rnd_match.group(1), - "auth_sig": auth_sig_match.group(1), - } - return {} - except Exception: - return {} - - def _extract_auth_url_base(self, html_content: str) -> Optional[str]: - """Extract auth URL base from player page script content.""" - try: - # New atob pattern for auth base URL - auth_url_base_match = re.search(r'var\s+__a\s*=\s*atob\([\'"]([^\'"]+)[\'"]\)', html_content) - if auth_url_base_match: - decoded_url = base64.b64decode(auth_url_base_match.group(1)).decode("utf-8") - return decoded_url.strip().rstrip("/") - - # Look for auth URL or domain in fetchWithRetry call or similar patterns - auth_url_match = re.search(r'fetchWithRetry\([\'"]([^\'"]*/auth\.php)', html_content) - - if auth_url_match: - auth_url = auth_url_match.group(1) - # Extract base URL up to the auth.php part - return auth_url.split("/auth.php")[0] - - # Try finding domain directly - domain_match = re.search(r'[\'"]https://([^/\'\"]+)(?:/[^\'\"]*)?/auth\.php', html_content) - - if domain_match: - return f"https://{domain_match.group(1)}" - - return None - except Exception: - return None - - def _get_origin(self, url: str) -> str: - """Extract origin from URL.""" - parsed = urlparse(url) - return f"{parsed.scheme}://{parsed.netloc}" - - def _derive_auth_url_base(self, player_domain: str) -> Optional[str]: - """Attempt to derive auth URL base from player domain.""" - try: - # Typical pattern is to use a subdomain for auth domain - parsed = urlparse(player_domain) - domain_parts = parsed.netloc.split(".") - - # Get the top-level domain and second-level domain - if len(domain_parts) >= 2: - base_domain = ".".join(domain_parts[-2:]) - # Try common subdomains for auth - for prefix in ["auth", "api", "cdn"]: - potential_auth_domain = f"https://{prefix}.{base_domain}" - return potential_auth_domain - - return None - except Exception: - return None diff --git a/mediaflow_proxy/extractors/factory.py b/mediaflow_proxy/extractors/factory.py index f9b1ba1..042f3ac 100644 --- a/mediaflow_proxy/extractors/factory.py +++ b/mediaflow_proxy/extractors/factory.py @@ -3,17 +3,27 @@ from typing import Dict, Type from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError from mediaflow_proxy.extractors.dlhd import DLHDExtractor from mediaflow_proxy.extractors.doodstream import DoodStreamExtractor +from mediaflow_proxy.extractors.sportsonline import SportsonlineExtractor from mediaflow_proxy.extractors.filelions import FileLionsExtractor +from mediaflow_proxy.extractors.filemoon import FileMoonExtractor +from mediaflow_proxy.extractors.F16Px import F16PxExtractor from mediaflow_proxy.extractors.livetv import LiveTVExtractor +from mediaflow_proxy.extractors.lulustream import LuluStreamExtractor from mediaflow_proxy.extractors.maxstream import MaxstreamExtractor from mediaflow_proxy.extractors.mixdrop import MixdropExtractor from mediaflow_proxy.extractors.okru import OkruExtractor from mediaflow_proxy.extractors.streamtape import StreamtapeExtractor +from mediaflow_proxy.extractors.streamwish import StreamWishExtractor from mediaflow_proxy.extractors.supervideo import SupervideoExtractor +from mediaflow_proxy.extractors.turbovidplay import TurboVidPlayExtractor from mediaflow_proxy.extractors.uqload import UqloadExtractor from mediaflow_proxy.extractors.vavoo import VavooExtractor +from mediaflow_proxy.extractors.vidmoly import VidmolyExtractor +from mediaflow_proxy.extractors.vidoza import VidozaExtractor from mediaflow_proxy.extractors.vixcloud import VixCloudExtractor from mediaflow_proxy.extractors.fastream import FastreamExtractor +from mediaflow_proxy.extractors.voe import VoeExtractor + class ExtractorFactory: """Factory for creating URL extractors.""" @@ -21,17 +31,26 @@ class ExtractorFactory: _extractors: Dict[str, Type[BaseExtractor]] = { "Doodstream": DoodStreamExtractor, "FileLions": FileLionsExtractor, + "FileMoon": FileMoonExtractor, + "F16Px": F16PxExtractor, "Uqload": UqloadExtractor, "Mixdrop": MixdropExtractor, "Streamtape": StreamtapeExtractor, + "StreamWish": StreamWishExtractor, "Supervideo": SupervideoExtractor, + "TurboVidPlay": TurboVidPlayExtractor, "VixCloud": VixCloudExtractor, "Okru": OkruExtractor, "Maxstream": MaxstreamExtractor, "LiveTV": LiveTVExtractor, + "LuluStream": LuluStreamExtractor, "DLHD": DLHDExtractor, "Vavoo": VavooExtractor, - "Fastream": FastreamExtractor + "Vidmoly": VidmolyExtractor, + "Vidoza": VidozaExtractor, + "Fastream": FastreamExtractor, + "Voe": VoeExtractor, + "Sportsonline": SportsonlineExtractor, } @classmethod diff --git a/mediaflow_proxy/extractors/filelions.py b/mediaflow_proxy/extractors/filelions.py index 6f68763..25271ed 100644 --- a/mediaflow_proxy/extractors/filelions.py +++ b/mediaflow_proxy/extractors/filelions.py @@ -12,7 +12,8 @@ class FileLionsExtractor(BaseExtractor): headers = {} patterns = [ # See https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/filelions.py r'''sources:\s*\[{file:\s*["'](?P[^"']+)''', - r'''["']hls[24]["']:\s*["'](?P[^"']+)''' + r'''["']hls4["']:\s*["'](?P[^"']+)''', + r'''["']hls2["']:\s*["'](?P[^"']+)''' ] final_url = await eval_solver(self, url, headers, patterns) diff --git a/mediaflow_proxy/extractors/filemoon.py b/mediaflow_proxy/extractors/filemoon.py new file mode 100644 index 0000000..808042a --- /dev/null +++ b/mediaflow_proxy/extractors/filemoon.py @@ -0,0 +1,52 @@ +import re +from typing import Dict, Any +from urllib.parse import urlparse, urljoin + +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError +from mediaflow_proxy.utils.packed import eval_solver + + +class FileMoonExtractor(BaseExtractor): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mediaflow_endpoint = "hls_manifest_proxy" + + async def extract(self, url: str, **kwargs) -> Dict[str, Any]: + response = await self._make_request(url) + + pattern = r'iframe.*?src=["\'](.*?)["\']' + match = re.search(pattern, response.text, re.DOTALL) + if not match: + raise ExtractorError("Failed to extract iframe URL") + + iframe_url = match.group(1) + + parsed = urlparse(str(response.url)) + base_url = f"{parsed.scheme}://{parsed.netloc}" + + if iframe_url.startswith("//"): + iframe_url = f"{parsed.scheme}:{iframe_url}" + elif not urlparse(iframe_url).scheme: + iframe_url = urljoin(base_url, iframe_url) + + headers = {"Referer": url} + patterns = [r'file:"(.*?)"'] + + final_url = await eval_solver( + self, + iframe_url, + headers, + patterns, + ) + + test_resp = await self._make_request(final_url, headers=headers) + if test_resp.status_code == 404: + raise ExtractorError("Stream not found (404)") + + self.base_headers["referer"] = url + + return { + "destination_url": final_url, + "request_headers": self.base_headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } diff --git a/mediaflow_proxy/extractors/lulustream.py b/mediaflow_proxy/extractors/lulustream.py new file mode 100644 index 0000000..4c1d4c9 --- /dev/null +++ b/mediaflow_proxy/extractors/lulustream.py @@ -0,0 +1,27 @@ +import re +from typing import Dict, Any + +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError + + +class LuluStreamExtractor(BaseExtractor): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mediaflow_endpoint = "hls_manifest_proxy" + + async def extract(self, url: str, **kwargs) -> Dict[str, Any]: + response = await self._make_request(url) + + # See https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/lulustream.py + pattern = r'''sources:\s*\[{file:\s*["'](?P[^"']+)''' + match = re.search(pattern, response.text, re.DOTALL) + if not match: + raise ExtractorError("Failed to extract source URL") + final_url = match.group(1) + + self.base_headers["referer"] = url + return { + "destination_url": final_url, + "request_headers": self.base_headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } diff --git a/mediaflow_proxy/extractors/sportsonline.py b/mediaflow_proxy/extractors/sportsonline.py new file mode 100644 index 0000000..f1c3b90 --- /dev/null +++ b/mediaflow_proxy/extractors/sportsonline.py @@ -0,0 +1,195 @@ +import re +import logging +from typing import Any, Dict, Optional +from urllib.parse import urlparse + +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError +from mediaflow_proxy.utils.packed import detect, unpack + +logger = logging.getLogger(__name__) + + +class SportsonlineExtractor(BaseExtractor): + """Sportsonline/Sportzonline URL extractor for M3U8 streams. + + Strategy: + 1. Fetch page -> find first