new version

This commit is contained in:
UrloMythus
2026-01-11 14:29:22 +01:00
parent b8a40b5afc
commit 7785e8c604
45 changed files with 5463 additions and 832 deletions

View File

@@ -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")