mirror of
https://github.com/UrloMythus/UnHided.git
synced 2026-04-11 11:50:51 +00:00
new version
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query, HTTPException, Request, Depends
|
||||
from fastapi import APIRouter, Query, HTTPException, Request, Depends, BackgroundTasks
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from mediaflow_proxy.extractors.base import ExtractorError
|
||||
@@ -9,6 +9,7 @@ from mediaflow_proxy.extractors.factory import ExtractorFactory
|
||||
from mediaflow_proxy.schemas import ExtractorURLParams
|
||||
from mediaflow_proxy.utils.cache_utils import get_cached_extractor_result, set_cache_extractor_result
|
||||
from mediaflow_proxy.utils.http_utils import (
|
||||
DownloadError,
|
||||
encode_mediaflow_proxy_url,
|
||||
get_original_scheme,
|
||||
ProxyRequestHeaders,
|
||||
@@ -19,12 +20,24 @@ from mediaflow_proxy.utils.base64_utils import process_potential_base64_url
|
||||
extractor_router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def refresh_extractor_cache(cache_key: str, extractor_params: ExtractorURLParams, proxy_headers: ProxyRequestHeaders):
|
||||
"""Asynchronously refreshes the extractor cache in the background."""
|
||||
try:
|
||||
logger.info(f"Background cache refresh started for key: {cache_key}")
|
||||
extractor = ExtractorFactory.get_extractor(extractor_params.host, proxy_headers.request)
|
||||
response = await extractor.extract(extractor_params.destination, **extractor_params.extra_params)
|
||||
await set_cache_extractor_result(cache_key, response)
|
||||
logger.info(f"Background cache refresh completed for key: {cache_key}")
|
||||
except Exception as e:
|
||||
logger.error(f"Background cache refresh failed for key {cache_key}: {e}")
|
||||
|
||||
|
||||
@extractor_router.head("/video")
|
||||
@extractor_router.get("/video")
|
||||
async def extract_url(
|
||||
extractor_params: Annotated[ExtractorURLParams, Query()],
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
proxy_headers: Annotated[ProxyRequestHeaders, Depends(get_proxy_headers)],
|
||||
):
|
||||
"""Extract clean links from various video hosting services."""
|
||||
@@ -35,13 +48,21 @@ async def extract_url(
|
||||
|
||||
cache_key = f"{extractor_params.host}_{extractor_params.model_dump_json()}"
|
||||
response = await get_cached_extractor_result(cache_key)
|
||||
if not response:
|
||||
|
||||
if response:
|
||||
logger.info(f"Serving from cache for key: {cache_key}")
|
||||
# Schedule a background task to refresh the cache without blocking the user
|
||||
background_tasks.add_task(refresh_extractor_cache, cache_key, extractor_params, proxy_headers)
|
||||
else:
|
||||
logger.info(f"Cache miss for key: {cache_key}. Fetching fresh data.")
|
||||
extractor = ExtractorFactory.get_extractor(extractor_params.host, proxy_headers.request)
|
||||
response = await extractor.extract(extractor_params.destination, **extractor_params.extra_params)
|
||||
await set_cache_extractor_result(cache_key, response)
|
||||
else:
|
||||
response["request_headers"].update(proxy_headers.request)
|
||||
|
||||
# Ensure the latest request headers are used, even with cached data
|
||||
if "request_headers" not in response:
|
||||
response["request_headers"] = {}
|
||||
response["request_headers"].update(proxy_headers.request)
|
||||
response["mediaflow_proxy_url"] = str(
|
||||
request.url_for(response.pop("mediaflow_endpoint")).replace(scheme=get_original_scheme(request))
|
||||
)
|
||||
@@ -49,6 +70,12 @@ async def extract_url(
|
||||
# Add API password to query params
|
||||
response["query_params"]["api_password"] = request.query_params.get("api_password")
|
||||
|
||||
if "max_res" in request.query_params:
|
||||
response["query_params"]["max_res"] = request.query_params.get("max_res")
|
||||
|
||||
if "no_proxy" in request.query_params:
|
||||
response["query_params"]["no_proxy"] = request.query_params.get("no_proxy")
|
||||
|
||||
if extractor_params.redirect_stream:
|
||||
stream_url = encode_mediaflow_proxy_url(
|
||||
**response,
|
||||
@@ -58,6 +85,9 @@ async def extract_url(
|
||||
|
||||
return response
|
||||
|
||||
except DownloadError as e:
|
||||
logger.error(f"Extraction failed: {str(e)}")
|
||||
raise HTTPException(status_code=e.status_code, detail=str(e))
|
||||
except ExtractorError as e:
|
||||
logger.error(f"Extraction failed: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@@ -20,6 +20,7 @@ def rewrite_m3u_links_streaming(m3u_lines_iterator: Iterator[str], base_url: str
|
||||
includendo gli headers da #EXTVLCOPT e #EXTHTTP. Yields rewritten lines.
|
||||
"""
|
||||
current_ext_headers: Dict[str, str] = {} # Dizionario per conservare gli headers dalle direttive
|
||||
current_kodi_props: Dict[str, str] = {} # Dizionario per conservare le proprietà KODI
|
||||
|
||||
for line_with_newline in m3u_lines_iterator:
|
||||
line_content = line_with_newline.rstrip('\n')
|
||||
@@ -27,6 +28,9 @@ def rewrite_m3u_links_streaming(m3u_lines_iterator: Iterator[str], base_url: str
|
||||
|
||||
is_header_tag = False
|
||||
if logical_line.startswith('#EXTVLCOPT:'):
|
||||
# Yield the original line to preserve it
|
||||
yield line_with_newline
|
||||
|
||||
is_header_tag = True
|
||||
try:
|
||||
option_str = logical_line.split(':', 1)[1]
|
||||
@@ -43,12 +47,15 @@ def rewrite_m3u_links_streaming(m3u_lines_iterator: Iterator[str], base_url: str
|
||||
current_ext_headers[header_key] = header_value
|
||||
elif key_vlc.startswith('http-'):
|
||||
# Gestisce http-user-agent, http-referer etc.
|
||||
header_key = '-'.join(word.capitalize() for word in key_vlc[len('http-'):].split('-'))
|
||||
header_key = key_vlc[len('http-'):]
|
||||
current_ext_headers[header_key] = value_vlc
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ Error parsing #EXTVLCOPT '{logical_line}': {e}")
|
||||
|
||||
elif logical_line.startswith('#EXTHTTP:'):
|
||||
# Yield the original line to preserve it
|
||||
yield line_with_newline
|
||||
|
||||
is_header_tag = True
|
||||
try:
|
||||
json_str = logical_line.split(':', 1)[1]
|
||||
@@ -57,9 +64,22 @@ def rewrite_m3u_links_streaming(m3u_lines_iterator: Iterator[str], base_url: str
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ Error parsing #EXTHTTP '{logical_line}': {e}")
|
||||
current_ext_headers = {} # Resetta in caso di errore
|
||||
|
||||
elif logical_line.startswith('#KODIPROP:'):
|
||||
# Yield the original line to preserve it
|
||||
yield line_with_newline
|
||||
|
||||
is_header_tag = True
|
||||
try:
|
||||
prop_str = logical_line.split(':', 1)[1]
|
||||
if '=' in prop_str:
|
||||
key_kodi, value_kodi = prop_str.split('=', 1)
|
||||
current_kodi_props[key_kodi.strip()] = value_kodi.strip()
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ Error parsing #KODIPROP '{logical_line}': {e}")
|
||||
|
||||
|
||||
if is_header_tag:
|
||||
yield line_with_newline
|
||||
continue
|
||||
|
||||
if logical_line and not logical_line.startswith('#') and \
|
||||
@@ -75,47 +95,55 @@ def rewrite_m3u_links_streaming(m3u_lines_iterator: Iterator[str], base_url: str
|
||||
processed_url_content = f"{base_url}/proxy/hls/manifest.m3u8?d={encoded_url}"
|
||||
elif 'vixsrc.to' in logical_line:
|
||||
encoded_url = urllib.parse.quote(logical_line, safe='')
|
||||
processed_url_content = f"{base_url}/extractor/video?host=VixCloud&redirect_stream=true&d={encoded_url}"
|
||||
processed_url_content = f"{base_url}/extractor/video?host=VixCloud&redirect_stream=true&d={encoded_url}&max_res=true&no_proxy=true"
|
||||
elif '.m3u8' in logical_line:
|
||||
encoded_url = urllib.parse.quote(logical_line, safe='')
|
||||
processed_url_content = f"{base_url}/proxy/hls/manifest.m3u8?d={encoded_url}"
|
||||
elif '.mpd' in logical_line:
|
||||
# Estrai parametri DRM dall'URL MPD se presenti
|
||||
# Estrai parametri DRM dall'URL MPD se presenti (es. &key_id=...&key=...)
|
||||
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||||
|
||||
# Parse dell'URL per estrarre parametri
|
||||
parsed_url = urlparse(logical_line)
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
|
||||
# Estrai key_id e key se presenti
|
||||
# Estrai key_id e key se presenti nei parametri della query
|
||||
key_id = query_params.get('key_id', [None])[0]
|
||||
key = query_params.get('key', [None])[0]
|
||||
|
||||
# Rimuovi key_id e key dai parametri originali
|
||||
clean_params = {k: v for k, v in query_params.items() if k not in ['key_id', 'key']}
|
||||
clean_query_params = {k: v for k, v in query_params.items() if k not in ['key_id', 'key']}
|
||||
|
||||
# Ricostruisci l'URL senza i parametri DRM
|
||||
clean_query = urlencode(clean_params, doseq=True) if clean_params else ''
|
||||
clean_query = urlencode(clean_query_params, doseq=True)
|
||||
clean_url = urlunparse((
|
||||
parsed_url.scheme,
|
||||
parsed_url.netloc,
|
||||
parsed_url.path,
|
||||
parsed_url.params,
|
||||
clean_query,
|
||||
parsed_url.fragment
|
||||
'' # Rimuovi il frammento per evitare problemi
|
||||
))
|
||||
|
||||
# Encode the MPD URL like other URL types
|
||||
clean_url_for_param = urllib.parse.quote(clean_url, safe='')
|
||||
|
||||
# Costruisci l'URL MediaFlow con parametri DRM separati
|
||||
processed_url_content = f"{base_url}/proxy/mpd/manifest.m3u8?d={clean_url_for_param}"
|
||||
# Codifica l'URL pulito per il parametro 'd'
|
||||
encoded_clean_url = urllib.parse.quote(clean_url, safe='')
|
||||
|
||||
# Aggiungi parametri DRM se presenti
|
||||
# Costruisci l'URL MediaFlow con parametri DRM separati
|
||||
processed_url_content = f"{base_url}/proxy/mpd/manifest.m3u8?d={encoded_clean_url}"
|
||||
|
||||
# Aggiungi i parametri DRM all'URL di MediaFlow se sono stati trovati
|
||||
if key_id:
|
||||
processed_url_content += f"&key_id={key_id}"
|
||||
if key:
|
||||
processed_url_content += f"&key={key}"
|
||||
|
||||
# Aggiungi chiavi da #KODIPROP se presenti
|
||||
license_key = current_kodi_props.get('inputstream.adaptive.license_key')
|
||||
if license_key and ':' in license_key:
|
||||
key_id_kodi, key_kodi = license_key.split(':', 1)
|
||||
processed_url_content += f"&key_id={key_id_kodi}"
|
||||
processed_url_content += f"&key={key_kodi}"
|
||||
|
||||
elif '.php' in logical_line:
|
||||
encoded_url = urllib.parse.quote(logical_line, safe='')
|
||||
processed_url_content = f"{base_url}/proxy/hls/manifest.m3u8?d={encoded_url}"
|
||||
@@ -130,6 +158,9 @@ def rewrite_m3u_links_streaming(m3u_lines_iterator: Iterator[str], base_url: str
|
||||
processed_url_content += header_params_str
|
||||
current_ext_headers = {}
|
||||
|
||||
# Resetta le proprietà KODI dopo averle usate
|
||||
current_kodi_props = {}
|
||||
|
||||
# Aggiungi api_password sempre alla fine
|
||||
if api_password:
|
||||
processed_url_content += f"&api_password={api_password}"
|
||||
@@ -150,7 +181,7 @@ async def async_download_m3u_playlist(url: str) -> list[str]:
|
||||
}
|
||||
lines = []
|
||||
try:
|
||||
async with httpx.AsyncClient(verify=True, timeout=30) as client:
|
||||
async with httpx.AsyncClient(verify=True, timeout=30, follow_redirects=True) as client:
|
||||
async with client.stream('GET', url, headers=headers) as response:
|
||||
response.raise_for_status()
|
||||
async for line_bytes in response.aiter_lines():
|
||||
@@ -164,47 +195,135 @@ async def async_download_m3u_playlist(url: str) -> list[str]:
|
||||
raise
|
||||
return lines
|
||||
|
||||
def parse_channel_entries(lines: list[str]) -> list[list[str]]:
|
||||
"""
|
||||
Analizza le linee di una playlist M3U e le raggruppa in entry di canali.
|
||||
Ogni entry è una lista di linee che compongono un singolo canale
|
||||
(da #EXTINF fino all'URL, incluse le righe intermedie).
|
||||
"""
|
||||
entries = []
|
||||
current_entry = []
|
||||
for line in lines:
|
||||
stripped_line = line.strip()
|
||||
if stripped_line.startswith('#EXTINF:'):
|
||||
if current_entry: # In caso di #EXTINF senza URL precedente
|
||||
logger.warning(f"Found a new #EXTINF tag before a URL was found for the previous entry. Discarding: {current_entry}")
|
||||
current_entry = [line]
|
||||
elif current_entry:
|
||||
current_entry.append(line)
|
||||
if stripped_line and not stripped_line.startswith('#'):
|
||||
entries.append(current_entry)
|
||||
current_entry = []
|
||||
return entries
|
||||
|
||||
|
||||
async def async_generate_combined_playlist(playlist_definitions: list[str], base_url: str, api_password: Optional[str]):
|
||||
"""Genera una playlist combinata da multiple definizioni, scaricando in parallelo."""
|
||||
# Prepara gli URL
|
||||
playlist_urls = []
|
||||
# Prepara i task di download
|
||||
download_tasks = []
|
||||
for definition in playlist_definitions:
|
||||
if '&' in definition:
|
||||
parts = definition.split('&', 1)
|
||||
playlist_url_str = parts[1] if len(parts) > 1 else parts[0]
|
||||
should_proxy = True
|
||||
playlist_url_str = definition
|
||||
should_sort = False
|
||||
|
||||
if definition.startswith('sort:'):
|
||||
should_sort = True
|
||||
definition = definition[len('sort:'):]
|
||||
|
||||
if definition.startswith('no_proxy:'): # Può essere combinato con sort:
|
||||
should_proxy = False
|
||||
playlist_url_str = definition[len('no_proxy:'):]
|
||||
else:
|
||||
playlist_url_str = definition
|
||||
playlist_urls.append(playlist_url_str)
|
||||
|
||||
download_tasks.append({
|
||||
"url": playlist_url_str,
|
||||
"proxy": should_proxy,
|
||||
"sort": should_sort
|
||||
})
|
||||
|
||||
# Scarica tutte le playlist in parallelo
|
||||
results = await asyncio.gather(*[async_download_m3u_playlist(url) for url in playlist_urls], return_exceptions=True)
|
||||
|
||||
first_playlist_header_handled = False
|
||||
for idx, lines in enumerate(results):
|
||||
if isinstance(lines, Exception):
|
||||
yield f"# ERROR processing playlist {playlist_urls[idx]}: {str(lines)}\n"
|
||||
results = await asyncio.gather(*[async_download_m3u_playlist(task["url"]) for task in download_tasks], return_exceptions=True)
|
||||
|
||||
# Raggruppa le playlist da ordinare e quelle da non ordinare
|
||||
sorted_playlist_lines = []
|
||||
unsorted_playlists_data = []
|
||||
|
||||
for idx, result in enumerate(results):
|
||||
task_info = download_tasks[idx]
|
||||
if isinstance(result, Exception):
|
||||
# Aggiungi errore come playlist non ordinata
|
||||
unsorted_playlists_data.append({'lines': [f"# ERROR processing playlist {task_info['url']}: {str(result)}\n"], 'proxy': False})
|
||||
continue
|
||||
playlist_lines: list[str] = lines # type: ignore
|
||||
current_playlist_had_lines = False
|
||||
first_line_of_this_segment = True
|
||||
lines_processed_for_current_playlist = 0
|
||||
rewritten_lines_iter = rewrite_m3u_links_streaming(iter(playlist_lines), base_url, api_password)
|
||||
for line in rewritten_lines_iter:
|
||||
current_playlist_had_lines = True
|
||||
is_extm3u_line = line.strip().startswith('#EXTM3U')
|
||||
lines_processed_for_current_playlist += 1
|
||||
if not first_playlist_header_handled:
|
||||
yield line
|
||||
if is_extm3u_line:
|
||||
|
||||
if task_info.get("sort", False):
|
||||
sorted_playlist_lines.extend(result)
|
||||
else:
|
||||
unsorted_playlists_data.append({'lines': result, 'proxy': task_info['proxy']})
|
||||
|
||||
# Gestione dell'header #EXTM3U
|
||||
first_playlist_header_handled = False
|
||||
def yield_header_once(lines_iter):
|
||||
nonlocal first_playlist_header_handled
|
||||
has_header = False
|
||||
for line in lines_iter:
|
||||
is_extm3u = line.strip().startswith('#EXTM3U')
|
||||
if is_extm3u:
|
||||
has_header = True
|
||||
if not first_playlist_header_handled:
|
||||
first_playlist_header_handled = True
|
||||
else:
|
||||
if first_line_of_this_segment and is_extm3u_line:
|
||||
pass
|
||||
else:
|
||||
yield line
|
||||
first_line_of_this_segment = False
|
||||
if current_playlist_had_lines and not first_playlist_header_handled:
|
||||
else:
|
||||
yield line
|
||||
if has_header and not first_playlist_header_handled:
|
||||
first_playlist_header_handled = True
|
||||
|
||||
# 1. Processa e ordina le playlist marcate con 'sort'
|
||||
if sorted_playlist_lines:
|
||||
# Estrai le entry dei canali
|
||||
# Modifica: Estrai le entry e mantieni l'informazione sul proxy
|
||||
channel_entries_with_proxy_info = []
|
||||
for idx, result in enumerate(results):
|
||||
task_info = download_tasks[idx]
|
||||
if task_info.get("sort") and isinstance(result, list):
|
||||
entries = parse_channel_entries(result) # result è la lista di linee della playlist
|
||||
for entry_lines in entries:
|
||||
# L'opzione proxy si applica a tutto il blocco del canale
|
||||
channel_entries_with_proxy_info.append((entry_lines, task_info["proxy"]))
|
||||
|
||||
# Ordina le entry in base al nome del canale (da #EXTINF)
|
||||
# La prima riga di ogni entry è sempre #EXTINF
|
||||
channel_entries_with_proxy_info.sort(key=lambda x: x[0][0].split(',')[-1].strip())
|
||||
|
||||
# Gestisci l'header una sola volta per il blocco ordinato
|
||||
if not first_playlist_header_handled:
|
||||
yield "#EXTM3U\n"
|
||||
first_playlist_header_handled = True
|
||||
|
||||
# Applica la riscrittura dei link in modo selettivo
|
||||
for entry_lines, should_proxy in channel_entries_with_proxy_info:
|
||||
# L'URL è l'ultima riga dell'entry
|
||||
url = entry_lines[-1]
|
||||
# Yield tutte le righe prima dell'URL
|
||||
for line in entry_lines[:-1]:
|
||||
yield line
|
||||
|
||||
if should_proxy:
|
||||
# Usa un iteratore fittizio per processare una sola linea
|
||||
rewritten_url_iter = rewrite_m3u_links_streaming(iter([url]), base_url, api_password)
|
||||
yield next(rewritten_url_iter, url) # Prende l'URL riscritto, con fallback all'originale
|
||||
else:
|
||||
yield url # Lascia l'URL invariato
|
||||
|
||||
|
||||
# 2. Accoda le playlist non ordinate
|
||||
for playlist_data in unsorted_playlists_data:
|
||||
lines_iterator = iter(playlist_data['lines'])
|
||||
if playlist_data['proxy']:
|
||||
lines_iterator = rewrite_m3u_links_streaming(lines_iterator, base_url, api_password)
|
||||
|
||||
for line in yield_header_once(lines_iterator):
|
||||
yield line
|
||||
|
||||
|
||||
@playlist_builder_router.get("/playlist")
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
from urllib.parse import quote, unquote
|
||||
import re
|
||||
import logging
|
||||
import httpx
|
||||
import time
|
||||
|
||||
from fastapi import Request, Depends, APIRouter, Query, HTTPException
|
||||
from fastapi.responses import Response, RedirectResponse
|
||||
from fastapi.responses import Response
|
||||
|
||||
from mediaflow_proxy.handlers import (
|
||||
handle_hls_stream_proxy,
|
||||
@@ -21,11 +24,22 @@ from mediaflow_proxy.schemas import (
|
||||
HLSManifestParams,
|
||||
MPDManifestParams,
|
||||
)
|
||||
from mediaflow_proxy.utils.http_utils import get_proxy_headers, ProxyRequestHeaders
|
||||
from mediaflow_proxy.utils.http_utils import (
|
||||
get_proxy_headers,
|
||||
ProxyRequestHeaders,
|
||||
create_httpx_client,
|
||||
)
|
||||
from mediaflow_proxy.utils.base64_utils import process_potential_base64_url
|
||||
|
||||
proxy_router = APIRouter()
|
||||
|
||||
# DLHD extraction cache: {original_url: {"data": extraction_result, "timestamp": time.time()}}
|
||||
_dlhd_extraction_cache = {}
|
||||
_dlhd_cache_duration = 600 # 10 minutes in seconds
|
||||
|
||||
_sportsonline_extraction_cache = {}
|
||||
_sportsonline_cache_duration = 600 # 10 minutes in seconds
|
||||
|
||||
|
||||
def sanitize_url(url: str) -> str:
|
||||
"""
|
||||
@@ -122,41 +136,146 @@ def extract_drm_params_from_url(url: str) -> tuple[str, str, str]:
|
||||
return clean_url, key_id, key
|
||||
|
||||
|
||||
def _check_and_redirect_dlhd_stream(request: Request, destination: str) -> RedirectResponse | None:
|
||||
def _invalidate_dlhd_cache(destination: str):
|
||||
"""Invalidate DLHD cache for a specific destination URL."""
|
||||
if destination in _dlhd_extraction_cache:
|
||||
del _dlhd_extraction_cache[destination]
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"DLHD cache invalidated for: {destination}")
|
||||
|
||||
|
||||
async def _check_and_extract_dlhd_stream(
|
||||
request: Request,
|
||||
destination: str,
|
||||
proxy_headers: ProxyRequestHeaders,
|
||||
force_refresh: bool = False
|
||||
) -> dict | None:
|
||||
"""
|
||||
Check if destination contains stream-{numero} pattern and redirect to extractor if needed.
|
||||
Check if destination contains DLHD/DaddyLive patterns and extract stream directly.
|
||||
Uses caching to avoid repeated extractions (10 minute cache).
|
||||
|
||||
Args:
|
||||
request (Request): The incoming HTTP request.
|
||||
destination (str): The destination URL to check.
|
||||
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
||||
force_refresh (bool): Force re-extraction even if cached data exists.
|
||||
|
||||
Returns:
|
||||
RedirectResponse | None: RedirectResponse if redirect is needed, None otherwise.
|
||||
dict | None: Extracted stream data if DLHD link detected, None otherwise.
|
||||
"""
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from mediaflow_proxy.extractors.factory import ExtractorFactory
|
||||
from mediaflow_proxy.extractors.base import ExtractorError
|
||||
from mediaflow_proxy.utils.http_utils import DownloadError
|
||||
|
||||
# Check for stream-{numero} pattern (e.g., stream-1, stream-123, etc.)
|
||||
if re.search(r'stream-\d+', destination):
|
||||
from urllib.parse import urlencode
|
||||
# Check for common DLHD/DaddyLive patterns in the URL
|
||||
# This includes stream-XXX pattern and domain names like dlhd.dad or daddylive.sx
|
||||
is_dlhd_link = (
|
||||
re.search(r'stream-\d+', destination) or
|
||||
"dlhd.dad" in urlparse(destination).netloc or
|
||||
"daddylive.sx" in urlparse(destination).netloc
|
||||
)
|
||||
|
||||
if not is_dlhd_link:
|
||||
return None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"DLHD link detected: {destination}")
|
||||
|
||||
# Check cache first (unless force_refresh is True)
|
||||
current_time = time.time()
|
||||
if not force_refresh and destination in _dlhd_extraction_cache:
|
||||
cached_entry = _dlhd_extraction_cache[destination]
|
||||
cache_age = current_time - cached_entry["timestamp"]
|
||||
|
||||
# Build redirect URL to extractor
|
||||
redirect_params = {
|
||||
"host": "DLHD",
|
||||
"redirect_stream": "true",
|
||||
"d": destination
|
||||
if cache_age < _dlhd_cache_duration:
|
||||
logger.info(f"Using cached DLHD data (age: {cache_age:.1f}s)")
|
||||
return cached_entry["data"]
|
||||
else:
|
||||
logger.info(f"DLHD cache expired (age: {cache_age:.1f}s), re-extracting...")
|
||||
del _dlhd_extraction_cache[destination]
|
||||
|
||||
# Extract stream data
|
||||
try:
|
||||
logger.info(f"Extracting DLHD stream data from: {destination}")
|
||||
extractor = ExtractorFactory.get_extractor("DLHD", proxy_headers.request)
|
||||
result = await extractor.extract(destination)
|
||||
|
||||
logger.info(f"DLHD extraction successful. Stream URL: {result.get('destination_url')}")
|
||||
|
||||
# Cache the result
|
||||
_dlhd_extraction_cache[destination] = {
|
||||
"data": result,
|
||||
"timestamp": current_time
|
||||
}
|
||||
logger.info(f"DLHD data cached for {_dlhd_cache_duration}s")
|
||||
|
||||
# Preserve api_password if present
|
||||
if "api_password" in request.query_params:
|
||||
redirect_params["api_password"] = request.query_params["api_password"]
|
||||
return result
|
||||
|
||||
# Build the redirect URL
|
||||
base_url = str(request.url_for("extract_url"))
|
||||
redirect_url = f"{base_url}?{urlencode(redirect_params)}"
|
||||
|
||||
return RedirectResponse(url=redirect_url, status_code=302)
|
||||
|
||||
return None
|
||||
except (ExtractorError, DownloadError) as e:
|
||||
logger.error(f"DLHD extraction failed: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=f"DLHD extraction failed: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error during DLHD extraction: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"DLHD extraction failed: {str(e)}")
|
||||
|
||||
|
||||
async def _check_and_extract_sportsonline_stream(
|
||||
request: Request,
|
||||
destination: str,
|
||||
proxy_headers: ProxyRequestHeaders,
|
||||
force_refresh: bool = False
|
||||
) -> dict | None:
|
||||
"""
|
||||
Check if destination contains Sportsonline/Sportzonline patterns and extract stream directly.
|
||||
Uses caching to avoid repeated extractions (10 minute cache).
|
||||
|
||||
Args:
|
||||
request (Request): The incoming HTTP request.
|
||||
destination (str): The destination URL to check.
|
||||
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
||||
force_refresh (bool): Force re-extraction even if cached data exists.
|
||||
|
||||
Returns:
|
||||
dict | None: Extracted stream data if Sportsonline link detected, None otherwise.
|
||||
"""
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from mediaflow_proxy.extractors.factory import ExtractorFactory
|
||||
from mediaflow_proxy.extractors.base import ExtractorError
|
||||
from mediaflow_proxy.utils.http_utils import DownloadError
|
||||
|
||||
parsed_netloc = urlparse(destination).netloc
|
||||
is_sportsonline_link = "sportzonline." in parsed_netloc or "sportsonline." in parsed_netloc
|
||||
|
||||
if not is_sportsonline_link:
|
||||
return None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Sportsonline link detected: {destination}")
|
||||
|
||||
current_time = time.time()
|
||||
if not force_refresh and destination in _sportsonline_extraction_cache:
|
||||
cached_entry = _sportsonline_extraction_cache[destination]
|
||||
if current_time - cached_entry["timestamp"] < _sportsonline_cache_duration:
|
||||
logger.info(f"Using cached Sportsonline data (age: {current_time - cached_entry['timestamp']:.1f}s)")
|
||||
return cached_entry["data"]
|
||||
else:
|
||||
logger.info("Sportsonline cache expired, re-extracting...")
|
||||
del _sportsonline_extraction_cache[destination]
|
||||
|
||||
try:
|
||||
logger.info(f"Extracting Sportsonline stream data from: {destination}")
|
||||
extractor = ExtractorFactory.get_extractor("Sportsonline", proxy_headers.request)
|
||||
result = await extractor.extract(destination)
|
||||
logger.info(f"Sportsonline extraction successful. Stream URL: {result.get('destination_url')}")
|
||||
_sportsonline_extraction_cache[destination] = {"data": result, "timestamp": current_time}
|
||||
logger.info(f"Sportsonline data cached for {_sportsonline_cache_duration}s")
|
||||
return result
|
||||
except (ExtractorError, DownloadError, Exception) as e:
|
||||
logger.error(f"Sportsonline extraction failed: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=f"Sportsonline extraction failed: {str(e)}")
|
||||
|
||||
|
||||
@proxy_router.head("/hls/manifest.m3u8")
|
||||
@@ -179,12 +298,197 @@ async def hls_manifest_proxy(
|
||||
Response: The HTTP response with the processed m3u8 playlist or streamed content.
|
||||
"""
|
||||
# Sanitize destination URL to fix common encoding issues
|
||||
original_destination = hls_params.destination
|
||||
hls_params.destination = sanitize_url(hls_params.destination)
|
||||
|
||||
# Check if destination contains stream-{numero} pattern and redirect to extractor
|
||||
redirect_response = _check_and_redirect_dlhd_stream(request, hls_params.destination)
|
||||
if redirect_response:
|
||||
return redirect_response
|
||||
# Check if this is a retry after 403 error (dlhd_retry parameter)
|
||||
force_refresh = request.query_params.get("dlhd_retry") == "1"
|
||||
|
||||
# Check if destination contains DLHD pattern and extract stream directly
|
||||
dlhd_result = await _check_and_extract_dlhd_stream(
|
||||
request, hls_params.destination, proxy_headers, force_refresh=force_refresh
|
||||
)
|
||||
dlhd_original_url = None
|
||||
if dlhd_result:
|
||||
# Store original DLHD URL for cache invalidation on 403 errors
|
||||
dlhd_original_url = hls_params.destination
|
||||
|
||||
# Update destination and headers with extracted stream data
|
||||
hls_params.destination = dlhd_result["destination_url"]
|
||||
extracted_headers = dlhd_result.get("request_headers", {})
|
||||
proxy_headers.request.update(extracted_headers)
|
||||
|
||||
# Check if extractor wants key-only proxy (DLHD uses hls_key_proxy endpoint)
|
||||
if dlhd_result.get("mediaflow_endpoint") == "hls_key_proxy":
|
||||
hls_params.key_only_proxy = True
|
||||
|
||||
# Also add headers to query params so they propagate to key/segment requests
|
||||
# This is necessary because M3U8Processor encodes headers as h_* query params
|
||||
from fastapi.datastructures import QueryParams
|
||||
query_dict = dict(request.query_params)
|
||||
for header_name, header_value in extracted_headers.items():
|
||||
# Add header with h_ prefix to query params
|
||||
query_dict[f"h_{header_name}"] = header_value
|
||||
# Add DLHD original URL to track for cache invalidation
|
||||
if dlhd_original_url:
|
||||
query_dict["dlhd_original"] = dlhd_original_url
|
||||
# Remove retry flag from subsequent requests
|
||||
query_dict.pop("dlhd_retry", None)
|
||||
# Update request query params
|
||||
request._query_params = QueryParams(query_dict)
|
||||
|
||||
# Check if destination contains Sportsonline pattern and extract stream directly
|
||||
sportsonline_result = await _check_and_extract_sportsonline_stream(
|
||||
request, hls_params.destination, proxy_headers
|
||||
)
|
||||
if sportsonline_result:
|
||||
# Update destination and headers with extracted stream data
|
||||
hls_params.destination = sportsonline_result["destination_url"]
|
||||
extracted_headers = sportsonline_result.get("request_headers", {})
|
||||
proxy_headers.request.update(extracted_headers)
|
||||
|
||||
# Check if extractor wants key-only proxy
|
||||
if sportsonline_result.get("mediaflow_endpoint") == "hls_key_proxy":
|
||||
hls_params.key_only_proxy = True
|
||||
|
||||
# Also add headers to query params so they propagate to key/segment requests
|
||||
from fastapi.datastructures import QueryParams
|
||||
query_dict = dict(request.query_params)
|
||||
for header_name, header_value in extracted_headers.items():
|
||||
# Add header with h_ prefix to query params
|
||||
query_dict[f"h_{header_name}"] = header_value
|
||||
# Remove retry flag from subsequent requests
|
||||
query_dict.pop("dlhd_retry", None)
|
||||
# Update request query params
|
||||
request._query_params = QueryParams(query_dict)
|
||||
|
||||
# Wrap the handler to catch 403 errors and retry with cache invalidation
|
||||
try:
|
||||
result = await _handle_hls_with_dlhd_retry(request, hls_params, proxy_headers, dlhd_original_url)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.exception(f"Unexpected error in hls_manifest_proxy: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
async def _handle_hls_with_dlhd_retry(
|
||||
request: Request,
|
||||
hls_params: HLSManifestParams,
|
||||
proxy_headers: ProxyRequestHeaders,
|
||||
dlhd_original_url: str | None
|
||||
):
|
||||
"""
|
||||
Handle HLS request with automatic retry on 403 errors for DLHD streams.
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if hls_params.max_res:
|
||||
from mediaflow_proxy.utils.hls_utils import parse_hls_playlist
|
||||
from mediaflow_proxy.utils.m3u8_processor import M3U8Processor
|
||||
|
||||
async with create_httpx_client(
|
||||
headers=proxy_headers.request,
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
try:
|
||||
response = await client.get(hls_params.destination)
|
||||
response.raise_for_status()
|
||||
playlist_content = response.text
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=f"Failed to fetch HLS manifest from origin: {e.response.status_code} {e.response.reason_phrase}",
|
||||
) from e
|
||||
except httpx.TimeoutException as e:
|
||||
raise HTTPException(
|
||||
status_code=504,
|
||||
detail=f"Timeout while fetching HLS manifest: {e}",
|
||||
) from e
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(status_code=502, detail=f"Network error fetching HLS manifest: {e}") from e
|
||||
|
||||
streams = parse_hls_playlist(playlist_content, base_url=hls_params.destination)
|
||||
if not streams:
|
||||
raise HTTPException(
|
||||
status_code=404, detail="No streams found in the manifest."
|
||||
)
|
||||
|
||||
highest_res_stream = max(
|
||||
streams,
|
||||
key=lambda s: s.get("resolution", (0, 0))[0]
|
||||
* s.get("resolution", (0, 0))[1],
|
||||
)
|
||||
|
||||
if highest_res_stream.get("resolution", (0, 0)) == (0, 0):
|
||||
logging.warning("Selected stream has resolution (0, 0); resolution parsing may have failed or not be available in the manifest.")
|
||||
|
||||
# Rebuild the manifest preserving master-level directives
|
||||
# but removing non-selected variant blocks
|
||||
lines = playlist_content.splitlines()
|
||||
highest_variant_index = streams.index(highest_res_stream)
|
||||
|
||||
variant_index = -1
|
||||
new_manifest_lines = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if line.startswith("#EXT-X-STREAM-INF"):
|
||||
variant_index += 1
|
||||
next_line = ""
|
||||
if i + 1 < len(lines) and not lines[i + 1].startswith("#"):
|
||||
next_line = lines[i + 1]
|
||||
|
||||
# Only keep the selected variant
|
||||
if variant_index == highest_variant_index:
|
||||
new_manifest_lines.append(line)
|
||||
if next_line:
|
||||
new_manifest_lines.append(next_line)
|
||||
|
||||
# Skip variant block (stream-inf + optional url)
|
||||
i += 2 if next_line else 1
|
||||
continue
|
||||
|
||||
# Preserve all other lines (master directives, media tags, etc.)
|
||||
new_manifest_lines.append(line)
|
||||
i += 1
|
||||
|
||||
new_manifest = "\n".join(new_manifest_lines)
|
||||
|
||||
# Process the new manifest to proxy all URLs within it
|
||||
processor = M3U8Processor(request, hls_params.key_url, hls_params.force_playlist_proxy, hls_params.key_only_proxy, hls_params.no_proxy)
|
||||
processed_manifest = await processor.process_m3u8(new_manifest, base_url=hls_params.destination)
|
||||
|
||||
return Response(content=processed_manifest, media_type="application/vnd.apple.mpegurl")
|
||||
|
||||
return await handle_hls_stream_proxy(request, hls_params, proxy_headers)
|
||||
|
||||
|
||||
@proxy_router.head("/hls/key_proxy/manifest.m3u8", name="hls_key_proxy")
|
||||
@proxy_router.get("/hls/key_proxy/manifest.m3u8", name="hls_key_proxy")
|
||||
async def hls_key_proxy(
|
||||
request: Request,
|
||||
hls_params: Annotated[HLSManifestParams, Query()],
|
||||
proxy_headers: Annotated[ProxyRequestHeaders, Depends(get_proxy_headers)],
|
||||
):
|
||||
"""
|
||||
Proxify HLS stream requests, but only proxy the key URL, leaving segment URLs direct.
|
||||
|
||||
Args:
|
||||
request (Request): The incoming HTTP request.
|
||||
hls_params (HLSManifestParams): The parameters for the HLS stream request.
|
||||
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
||||
|
||||
Returns:
|
||||
Response: The HTTP response with the processed m3u8 playlist.
|
||||
"""
|
||||
# Sanitize destination URL to fix common encoding issues
|
||||
hls_params.destination = sanitize_url(hls_params.destination)
|
||||
|
||||
# Set the key_only_proxy flag to True
|
||||
hls_params.key_only_proxy = True
|
||||
|
||||
return await handle_hls_stream_proxy(request, hls_params, proxy_headers)
|
||||
|
||||
@@ -208,7 +512,7 @@ async def hls_segment_proxy(
|
||||
"""
|
||||
from mediaflow_proxy.utils.hls_prebuffer import hls_prebuffer
|
||||
from mediaflow_proxy.configs import settings
|
||||
|
||||
|
||||
# Sanitize segment URL to fix common encoding issues
|
||||
segment_url = sanitize_url(segment_url)
|
||||
|
||||
@@ -217,11 +521,13 @@ async def hls_segment_proxy(
|
||||
for key, value in request.query_params.items():
|
||||
if key.startswith("h_"):
|
||||
headers[key[2:]] = value
|
||||
|
||||
|
||||
# Try to get segment from pre-buffer cache first
|
||||
if settings.enable_hls_prebuffer:
|
||||
cached_segment = await hls_prebuffer.get_segment(segment_url, headers)
|
||||
if cached_segment:
|
||||
# Avvia prebuffer dei successivi in background
|
||||
asyncio.create_task(hls_prebuffer.prebuffer_from_segment(segment_url, headers))
|
||||
return Response(
|
||||
content=cached_segment,
|
||||
media_type="video/mp2t",
|
||||
@@ -231,8 +537,11 @@ async def hls_segment_proxy(
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
)
|
||||
|
||||
# Fallback to direct streaming if not in cache
|
||||
|
||||
# Fallback to direct streaming se non in cache:
|
||||
# prima di restituire, prova comunque a far partire il prebuffer dei successivi
|
||||
if settings.enable_hls_prebuffer:
|
||||
asyncio.create_task(hls_prebuffer.prebuffer_from_segment(segment_url, headers))
|
||||
return await handle_stream_request("GET", segment_url, proxy_headers)
|
||||
|
||||
|
||||
@@ -308,16 +617,21 @@ async def proxy_stream_endpoint(
|
||||
# Sanitize destination URL to fix common encoding issues
|
||||
destination = sanitize_url(destination)
|
||||
|
||||
# Check if destination contains stream-{numero} pattern and redirect to extractor
|
||||
redirect_response = _check_and_redirect_dlhd_stream(request, destination)
|
||||
if redirect_response:
|
||||
return redirect_response
|
||||
# Check if destination contains DLHD pattern and extract stream directly
|
||||
dlhd_result = await _check_and_extract_dlhd_stream(request, destination, proxy_headers)
|
||||
if dlhd_result:
|
||||
# Update destination and headers with extracted stream data
|
||||
destination = dlhd_result["destination_url"]
|
||||
proxy_headers.request.update(dlhd_result.get("request_headers", {}))
|
||||
if proxy_headers.request.get("range", "").strip() == "":
|
||||
proxy_headers.request.pop("range", None)
|
||||
|
||||
if proxy_headers.request.get("if-range", "").strip() == "":
|
||||
proxy_headers.request.pop("if-range", None)
|
||||
|
||||
if "range" not in proxy_headers.request:
|
||||
proxy_headers.request["range"] = "bytes=0-"
|
||||
|
||||
content_range = proxy_headers.request.get("range", "bytes=0-")
|
||||
if "nan" in content_range.casefold():
|
||||
# Handle invalid range requests "bytes=NaN-NaN"
|
||||
raise HTTPException(status_code=416, detail="Invalid Range Header")
|
||||
proxy_headers.request.update({"range": content_range})
|
||||
if filename:
|
||||
# If a filename is provided, set it in the headers using RFC 6266 format
|
||||
try:
|
||||
@@ -430,4 +744,4 @@ async def get_mediaflow_proxy_public_ip():
|
||||
Returns:
|
||||
Response: The HTTP response with the public IP address in the form of a JSON object. {"ip": "xxx.xxx.xxx.xxx"}
|
||||
"""
|
||||
return await get_public_ip()
|
||||
return await get_public_ip()
|
||||
|
||||
Reference in New Issue
Block a user