MikeRedemtion/Sumatran
0
1from flask import Flask, request, Response, jsonify2import requests3from urllib.parse import urlparse, urljoin, quote, unquote4import re5import traceback6import json7import base648from urllib.parse import quote_plus9import os10import random11import time12from cachetools import TTLCache, LRUCache13from dotenv import load_dotenv14from requests.adapters import HTTPAdapter15from urllib3.util.retry import Retry16import psutil17from threading import Thread, Lock18import weakref19import hashlib20from functools import wraps21import logging22from logging.handlers import RotatingFileHandler23import subprocess24import concurrent.futures25import threading26from datetime import datetime, timedelta27import math28 29app = Flask(__name__)30 31load_dotenv()32 33# --- Classe VavooResolver per gestire i link Vavoo ---34class VavooResolver:35 def __init__(self):36 self.session = requests.Session()37 self.session.headers.update({38 'User-Agent': 'MediaHubMX/2'39 })40 41 def getAuthSignature(self):42 """Funzione che replica esattamente quella dell'addon utils.py"""43 headers = {44 "user-agent": "okhttp/4.11.0",45 "accept": "application/json", 46 "content-type": "application/json; charset=utf-8",47 "content-length": "1106",48 "accept-encoding": "gzip"49 }50 data = {51 "token": "tosFwQCJMS8qrW_AjLoHPQ41646J5dRNha6ZWHnijoYQQQoADQoXYSo7ki7O5-CsgN4CH0uRk6EEoJ0728ar9scCRQW3ZkbfrPfeCXW2VgopSW2FWDqPOoVYIuVPAOnXCZ5g",52 "reason": "app-blur",53 "locale": "de",54 "theme": "dark",55 "metadata": {56 "device": {57 "type": "Handset",58 "brand": "google",59 "model": "Nexus",60 "name": "21081111RG",61 "uniqueId": "d10e5d99ab665233"62 },63 "os": {64 "name": "android",65 "version": "7.1.2",66 "abis": ["arm64-v8a", "armeabi-v7a", "armeabi"],67 "host": "android"68 },69 "app": {70 "platform": "android",71 "version": "3.1.20",72 "buildId": "289515000",73 "engine": "hbc85",74 "signatures": ["6e8a975e3cbf07d5de823a760d4c2547f86c1403105020adee5de67ac510999e"],75 "installer": "app.revanced.manager.flutter"76 },77 "version": {78 "package": "tv.vavoo.app",79 "binary": "3.1.20",80 "js": "3.1.20"81 }82 },83 "appFocusTime": 0,84 "playerActive": False,85 "playDuration": 0,86 "devMode": False,87 "hasAddon": True,88 "castConnected": False,89 "package": "tv.vavoo.app",90 "version": "3.1.20",91 "process": "app",92 "firstAppStart": 1743962904623,93 "lastAppStart": 1743962904623,94 "ipLocation": "",95 "adblockEnabled": True,96 "proxy": {97 "supported": ["ss", "openvpn"],98 "engine": "ss", 99 "ssVersion": 1,100 "enabled": True,101 "autoServer": True,102 "id": "pl-waw"103 },104 "iap": {105 "supported": False106 }107 }108 try:109 resp = self.session.post("https://www.vavoo.tv/api/app/ping", json=data, headers=headers, timeout=10)110 resp.raise_for_status()111 return resp.json().get("addonSig")112 except Exception as e:113 app.logger.error(f"Errore nel recupero della signature Vavoo: {e}")114 return None115 116 def resolve_vavoo_link(self, link, verbose=False):117 """118 Risolve un link Vavoo usando solo il metodo principale (streammode=1)119 """120 if not "vavoo.to" in link:121 if verbose:122 app.logger.info("Il link non è un link Vavoo")123 return None124 125 # Solo metodo principale per il proxy126 signature = self.getAuthSignature()127 if not signature:128 app.logger.error("Impossibile ottenere la signature Vavoo")129 return None130 131 headers = {132 "user-agent": "MediaHubMX/2",133 "accept": "application/json",134 "content-type": "application/json; charset=utf-8", 135 "content-length": "115",136 "accept-encoding": "gzip",137 "mediahubmx-signature": signature138 }139 data = {140 "language": "de",141 "region": "AT", 142 "url": link,143 "clientVersion": "3.0.2"144 }145 146 try:147 resp = self.session.post("https://vavoo.to/mediahubmx-resolve.json", json=data, headers=headers, timeout=10)148 resp.raise_for_status()149 150 if verbose:151 app.logger.info(f"Vavoo response status: {resp.status_code}")152 app.logger.info(f"Vavoo response body: {resp.text}")153 154 result = resp.json()155 if isinstance(result, list) and result and result[0].get("url"):156 resolved_url = result[0]["url"]157 channel_name = result[0].get("name", "Unknown")158 app.logger.info(f"Vavoo risolto: {channel_name} -> {resolved_url}")159 return resolved_url160 elif isinstance(result, dict) and result.get("url"):161 app.logger.info(f"Vavoo risolto: {result['url']}")162 return result["url"]163 else:164 app.logger.warning("Nessun link valido trovato nella risposta Vavoo")165 return None166 167 except Exception as e:168 app.logger.error(f"Errore nella risoluzione Vavoo: {e}")169 return None170 171# Istanza globale del resolver Vavoo172vavoo_resolver = VavooResolver()173 174# --- Configurazione Cache ---175def setup_all_caches():176 global M3U8_CACHE, TS_CACHE, KEY_CACHE177 config = config_manager.load_config()178 if config.get('CACHE_ENABLED', True):179 M3U8_CACHE = TTLCache(maxsize=config['CACHE_MAXSIZE_M3U8'], ttl=config['CACHE_TTL_M3U8'])180 TS_CACHE = TTLCache(maxsize=config['CACHE_MAXSIZE_TS'], ttl=config['CACHE_TTL_TS'])181 KEY_CACHE = TTLCache(maxsize=config['CACHE_MAXSIZE_KEY'], ttl=config['CACHE_TTL_KEY'])182 app.logger.info("Cache ABILITATA su tutte le risorse.")183 else:184 M3U8_CACHE = {}185 TS_CACHE = {}186 KEY_CACHE = {}187 app.logger.warning("TUTTE LE CACHE DISABILITATE: stream diretto attivo.")188 189# Sistema di statistiche (senza WebSocket) - spostato dopo la definizione di pre_buffer_manager190 191# --- Configurazione Generale ---192VERIFY_SSL = os.environ.get('VERIFY_SSL', 'false').lower() not in ('false', '0', 'no')193if not VERIFY_SSL:194 print("ATTENZIONE: La verifica del certificato SSL è DISABILITATA. Questo potrebbe esporre a rischi di sicurezza.")195 import urllib3196 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)197 198# Timeout aumentato per gestire meglio i segmenti TS di grandi dimensioni199REQUEST_TIMEOUT = int(os.environ.get('REQUEST_TIMEOUT', 30))200print(f"Timeout per le richieste impostato a {REQUEST_TIMEOUT} secondi.")201 202# Configurazioni Keep-Alive203KEEP_ALIVE_TIMEOUT = int(os.environ.get('KEEP_ALIVE_TIMEOUT', 300)) # 5 minuti204MAX_KEEP_ALIVE_REQUESTS = int(os.environ.get('MAX_KEEP_ALIVE_REQUESTS', 1000))205POOL_CONNECTIONS = int(os.environ.get('POOL_CONNECTIONS', 20))206POOL_MAXSIZE = int(os.environ.get('POOL_MAXSIZE', 50))207 208print(f"Keep-Alive configurato: timeout={KEEP_ALIVE_TIMEOUT}s, max_requests={MAX_KEEP_ALIVE_REQUESTS}")209 210# --- Setup Logging System ---211def setup_logging():212 """Configura il sistema di logging solo su console"""213 formatter = logging.Formatter(214 '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'215 )216 217 # Handler solo per console218 console_handler = logging.StreamHandler()219 console_handler.setFormatter(formatter)220 console_handler.setLevel(logging.INFO)221 222 # Configura il logger principale223 app.logger.addHandler(console_handler)224 app.logger.setLevel(logging.INFO)225 226setup_logging()227 228# --- Configurazione Manager ---229class ConfigManager:230 def __init__(self):231 self.config_file = 'proxy_config.json'232 self.default_config = {233 'PROXY': '',234 'DADDY_PROXY': '',235 'REQUEST_TIMEOUT': 45,236 'VERIFY_SSL': False,237 'KEEP_ALIVE_TIMEOUT': 900,238 'MAX_KEEP_ALIVE_REQUESTS': 5000,239 'POOL_CONNECTIONS': 50,240 'POOL_MAXSIZE': 300,241 'CACHE_TTL_M3U8': 5,242 'CACHE_TTL_TS': 600,243 'CACHE_TTL_KEY': 600,244 'CACHE_MAXSIZE_M3U8': 500,245 'CACHE_MAXSIZE_TS': 8000,246 'CACHE_MAXSIZE_KEY': 1000,247 'CACHE_ENABLED' : True,248 'NO_PROXY_DOMAINS': 'github.com,raw.githubusercontent.com',249 'PREBUFFER_ENABLED': True,250 'PREBUFFER_MAX_SEGMENTS': 5,251 'PREBUFFER_MAX_SIZE_MB': 200,252 'PREBUFFER_CLEANUP_INTERVAL': 300,253 'PREBUFFER_MAX_MEMORY_PERCENT': 30.0,254 'PREBUFFER_EMERGENCY_THRESHOLD': 99.9,255 }256 257 def load_config(self):258 """Carica la configurazione combinando proxy da file e variabili d'ambiente"""259 # Inizia con i valori di default260 config = self.default_config.copy()261 262 # Carica dal file se esiste (seconda priorità)263 if os.path.exists(self.config_file):264 try:265 with open(self.config_file, 'r') as f:266 file_config = json.load(f)267 config.update(file_config)268 except Exception as e:269 app.logger.error(f"Errore nel caricamento della configurazione: {e}")270 271 # Gestione proxy unificata272 proxy_value = os.environ.get('PROXY', '')273 if proxy_value and proxy_value.strip():274 config['PROXY'] = proxy_value.strip()275 app.logger.info(f"Proxy generale configurato: {proxy_value}")276 277 # Gestione proxy DaddyLive specifico278 daddy_proxy_value = os.environ.get('DADDY_PROXY', '')279 if daddy_proxy_value and daddy_proxy_value.strip():280 config['DADDY_PROXY'] = daddy_proxy_value.strip()281 app.logger.info(f"Proxy DaddyLive configurato: {daddy_proxy_value}")282 283 # Per le altre variabili, mantieni la priorità alle env vars284 for key in config.keys():285 if key not in ['PROXY', 'DADDY_PROXY']: # Salta i proxy che abbiamo già gestito286 env_value = os.environ.get(key)287 if env_value is not None:288 # Converti il tipo appropriato289 if key in ['VERIFY_SSL', 'CACHE_ENABLED', 'PREBUFFER_ENABLED']:290 config[key] = env_value.lower() in ('true', '1', 'yes')291 elif key in ['REQUEST_TIMEOUT', 'KEEP_ALIVE_TIMEOUT', 'MAX_KEEP_ALIVE_REQUESTS', 292 'POOL_CONNECTIONS', 'POOL_MAXSIZE', 'CACHE_TTL_M3U8', 'CACHE_TTL_TS', 293 'CACHE_TTL_KEY', 'CACHE_MAXSIZE_M3U8', 'CACHE_MAXSIZE_TS', 'CACHE_MAXSIZE_KEY',294 'PREBUFFER_MAX_SEGMENTS', 'PREBUFFER_MAX_SIZE_MB', 'PREBUFFER_CLEANUP_INTERVAL']:295 try:296 config[key] = int(env_value)297 except ValueError:298 app.logger.warning(f"Valore non valido per {key}: {env_value}")299 elif key in ['PREBUFFER_MAX_MEMORY_PERCENT', 'PREBUFFER_EMERGENCY_THRESHOLD']:300 try:301 config[key] = float(env_value)302 except ValueError:303 app.logger.warning(f"Valore non valido per {key}: {env_value}")304 else:305 config[key] = env_value306 307 return config308 309 def save_config(self, config):310 """Salva la configurazione nel file JSON"""311 try:312 with open(self.config_file, 'w') as f:313 json.dump(config, f, indent=4)314 return True315 except Exception as e:316 app.logger.error(f"Errore nel salvataggio della configurazione: {e}")317 return False318 319 def apply_config_to_app(self, config):320 """Applica la configurazione all'app Flask"""321 for key, value in config.items():322 if hasattr(app, 'config'):323 app.config[key] = value324 os.environ[key] = str(value)325 return True326 327config_manager = ConfigManager()328 329# --- Sistema di Pre-Buffering per Evitare Buffering ---330class PreBufferManager:331 def __init__(self):332 self.pre_buffer = {} # {stream_id: {segment_url: content}}333 self.pre_buffer_lock = Lock()334 self.pre_buffer_threads = {} # {stream_id: thread}335 self.last_cleanup_time = time.time()336 self.update_config()337 338 def update_config(self):339 """Aggiorna la configurazione dal config manager"""340 try:341 config = config_manager.load_config()342 343 # Assicurati che tutti i valori numerici siano convertiti correttamente344 max_segments = config.get('PREBUFFER_MAX_SEGMENTS', 3)345 if isinstance(max_segments, str):346 max_segments = int(max_segments)347 348 max_size_mb = config.get('PREBUFFER_MAX_SIZE_MB', 50)349 if isinstance(max_size_mb, str):350 max_size_mb = int(max_size_mb)351 352 cleanup_interval = config.get('PREBUFFER_CLEANUP_INTERVAL', 300)353 if isinstance(cleanup_interval, str):354 cleanup_interval = int(cleanup_interval)355 356 max_memory_percent = config.get('PREBUFFER_MAX_MEMORY_PERCENT', 30)357 if isinstance(max_memory_percent, str):358 max_memory_percent = float(max_memory_percent)359 360 emergency_threshold = config.get('PREBUFFER_EMERGENCY_THRESHOLD', 90)361 if isinstance(emergency_threshold, str):362 emergency_threshold = float(emergency_threshold)363 364 self.pre_buffer_config = {365 'enabled': config.get('PREBUFFER_ENABLED', True),366 'max_segments': max_segments,367 'max_buffer_size': max_size_mb * 1024 * 1024, # Converti in bytes368 'cleanup_interval': cleanup_interval,369 'max_memory_percent': max_memory_percent, # Max RAM percent370 'emergency_cleanup_threshold': emergency_threshold # Cleanup se RAM > threshold%371 }372 app.logger.info(f"Configurazione pre-buffer aggiornata: {self.pre_buffer_config}")373 except Exception as e:374 app.logger.error(f"Errore nell'aggiornamento configurazione pre-buffer: {e}")375 # Configurazione di fallback376 self.pre_buffer_config = {377 'enabled': True,378 'max_segments': 3,379 'max_buffer_size': 50 * 1024 * 1024,380 'cleanup_interval': 300,381 'max_memory_percent': 30.0,382 'emergency_cleanup_threshold': 90.0383 }384 385 def check_memory_usage(self):386 """Controlla l'uso di memoria e attiva cleanup se necessario"""387 try:388 memory = psutil.virtual_memory()389 memory_percent = memory.percent390 391 # Calcola la dimensione totale del buffer392 with self.pre_buffer_lock:393 total_buffer_size = sum(394 sum(len(content) for content in segments.values())395 for segments in self.pre_buffer.values()396 )397 buffer_memory_percent = (total_buffer_size / memory.total) * 100398 399 app.logger.info(f"Memoria sistema: {memory_percent:.1f}%, Buffer: {buffer_memory_percent:.1f}%")400 401 # Cleanup di emergenza se la RAM supera la soglia402 emergency_threshold = self.pre_buffer_config['emergency_cleanup_threshold']403 app.logger.debug(f"Controllo memoria: {memory_percent:.1f}% vs soglia {emergency_threshold}")404 if memory_percent > emergency_threshold:405 app.logger.warning(f"RAM critica ({memory_percent:.1f}%), pulizia di emergenza del buffer")406 self.emergency_cleanup()407 return False408 409 # Cleanup se il buffer usa troppa memoria410 max_memory_percent = self.pre_buffer_config['max_memory_percent']411 app.logger.debug(f"Controllo buffer: {buffer_memory_percent:.1f}% vs limite {max_memory_percent}")412 if buffer_memory_percent > max_memory_percent:413 app.logger.warning(f"Buffer troppo grande ({buffer_memory_percent:.1f}%), pulizia automatica")414 self.cleanup_oldest_streams()415 return False416 417 return True418 419 except Exception as e:420 app.logger.error(f"Errore nel controllo memoria: {e}")421 return True422 423 def emergency_cleanup(self):424 """Pulizia di emergenza - rimuove tutti i buffer"""425 with self.pre_buffer_lock:426 streams_cleared = len(self.pre_buffer)427 total_size = sum(428 sum(len(content) for content in segments.values())429 for segments in self.pre_buffer.values()430 )431 self.pre_buffer.clear()432 self.pre_buffer_threads.clear()433 434 app.logger.warning(f"Pulizia di emergenza completata: {streams_cleared} stream, {total_size / (1024*1024):.1f}MB liberati")435 436 def cleanup_oldest_streams(self):437 """Rimuove gli stream più vecchi per liberare memoria"""438 with self.pre_buffer_lock:439 if len(self.pre_buffer) <= 1:440 return441 442 # Calcola la dimensione di ogni stream443 stream_sizes = {}444 for stream_id, segments in self.pre_buffer.items():445 stream_size = sum(len(content) for content in segments.values())446 stream_sizes[stream_id] = stream_size447 448 # Rimuovi gli stream più grandi fino a liberare abbastanza memoria449 target_reduction = self.pre_buffer_config['max_buffer_size'] * 0.5 # Riduci del 50%450 current_total = sum(stream_sizes.values())451 452 if current_total <= target_reduction:453 return454 455 # Ordina per dimensione (più grandi prima)456 sorted_streams = sorted(stream_sizes.items(), key=lambda x: x[1], reverse=True)457 458 freed_memory = 0459 streams_to_remove = []460 461 for stream_id, size in sorted_streams:462 if freed_memory >= target_reduction:463 break464 streams_to_remove.append(stream_id)465 freed_memory += size466 467 # Rimuovi gli stream selezionati468 for stream_id in streams_to_remove:469 if stream_id in self.pre_buffer:470 del self.pre_buffer[stream_id]471 if stream_id in self.pre_buffer_threads:472 del self.pre_buffer_threads[stream_id]473 474 app.logger.info(f"Pulizia automatica: {len(streams_to_remove)} stream rimossi, {freed_memory / (1024*1024):.1f}MB liberati")475 476 def get_stream_id_from_url(self, url):477 """Estrae un ID stream univoco dall'URL"""478 # Usa l'hash dell'URL come stream ID479 return hashlib.md5(url.encode()).hexdigest()[:12]480 481 def pre_buffer_segments(self, m3u8_content, base_url, headers, stream_id):482 """Pre-scarica i segmenti successivi in background"""483 # Controlla se il pre-buffering è abilitato484 if not self.pre_buffer_config.get('enabled', True):485 app.logger.info(f"Pre-buffering disabilitato per stream {stream_id}")486 return487 488 # Controlla l'uso di memoria prima di iniziare489 if not self.check_memory_usage():490 app.logger.warning(f"Memoria insufficiente, pre-buffering saltato per stream {stream_id}")491 return492 493 try:494 # Trova i segmenti nel M3U8495 segment_urls = []496 for line in m3u8_content.splitlines():497 line = line.strip()498 if line and not line.startswith('#'):499 segment_url = urljoin(base_url, line)500 segment_urls.append(segment_url)501 502 if not segment_urls:503 return504 505 # Pre-scarica i primi N segmenti506 max_segments = self.pre_buffer_config['max_segments']507 app.logger.info(f"Pre-buffering per stream {stream_id}: {len(segment_urls)} segmenti disponibili, max_segments={max_segments}")508 segments_to_buffer = segment_urls[:max_segments]509 510 def buffer_worker():511 try:512 current_buffer_size = 0513 514 for segment_url in segments_to_buffer:515 # Controlla memoria prima di ogni segmento516 if not self.check_memory_usage():517 app.logger.warning(f"Memoria insufficiente durante pre-buffering, interrotto per stream {stream_id}")518 break519 520 # Controlla se il segmento è già nel buffer521 with self.pre_buffer_lock:522 if stream_id in self.pre_buffer and segment_url in self.pre_buffer[stream_id]:523 continue524 525 try:526 # Scarica il segmento527 proxy_config = get_proxy_for_url(segment_url)528 proxy_key = proxy_config['http'] if proxy_config else None529 530 response = make_persistent_request(531 segment_url,532 headers=headers,533 timeout=get_dynamic_timeout(segment_url),534 proxy_url=proxy_key,535 allow_redirects=True536 )537 response.raise_for_status()538 539 segment_content = response.content540 segment_size = len(segment_content)541 542 # Controlla se il buffer non supera il limite543 if current_buffer_size + segment_size > self.pre_buffer_config['max_buffer_size']:544 app.logger.warning(f"Buffer pieno per stream {stream_id}, salto segmento {segment_url}")545 break546 547 # Aggiungi al buffer548 with self.pre_buffer_lock:549 if stream_id not in self.pre_buffer:550 self.pre_buffer[stream_id] = {}551 self.pre_buffer[stream_id][segment_url] = segment_content552 current_buffer_size += segment_size553 554 app.logger.info(f"Segmento pre-buffato: {segment_url} ({segment_size} bytes) per stream {stream_id}")555 556 except Exception as e:557 app.logger.error(f"Errore nel pre-buffering del segmento {segment_url}: {e}")558 continue559 560 app.logger.info(f"Pre-buffering completato per stream {stream_id}: {len(segments_to_buffer)} segmenti")561 562 except Exception as e:563 app.logger.error(f"Errore nel worker di pre-buffering per stream {stream_id}: {e}")564 finally:565 # Rimuovi il thread dalla lista566 with self.pre_buffer_lock:567 if stream_id in self.pre_buffer_threads:568 del self.pre_buffer_threads[stream_id]569 570 # Avvia il thread di pre-buffering571 buffer_thread = Thread(target=buffer_worker, daemon=True)572 buffer_thread.start()573 574 with self.pre_buffer_lock:575 self.pre_buffer_threads[stream_id] = buffer_thread576 577 except Exception as e:578 app.logger.error(f"Errore nell'avvio del pre-buffering per stream {stream_id}: {e}")579 580 def get_buffered_segment(self, segment_url, stream_id):581 """Recupera un segmento dal buffer se disponibile"""582 with self.pre_buffer_lock:583 if stream_id in self.pre_buffer and segment_url in self.pre_buffer[stream_id]:584 content = self.pre_buffer[stream_id][segment_url]585 # Rimuovi dal buffer dopo l'uso586 del self.pre_buffer[stream_id][segment_url]587 app.logger.info(f"Segmento servito dal buffer: {segment_url} per stream {stream_id}")588 return content589 return None590 591 def cleanup_old_buffers(self):592 """Pulisce i buffer vecchi"""593 while True:594 try:595 time.sleep(self.pre_buffer_config['cleanup_interval'])596 597 # Controlla memoria e pulisci se necessario598 self.check_memory_usage()599 600 with self.pre_buffer_lock:601 current_time = time.time()602 streams_to_remove = []603 604 for stream_id, segments in self.pre_buffer.items():605 # Rimuovi stream senza thread attivo e con buffer vecchio606 if stream_id not in self.pre_buffer_threads:607 streams_to_remove.append(stream_id)608 609 for stream_id in streams_to_remove:610 del self.pre_buffer[stream_id]611 app.logger.info(f"Buffer pulito per stream {stream_id}")612 613 except Exception as e:614 app.logger.error(f"Errore nella pulizia del buffer: {e}")615 616# Istanza globale del pre-buffer manager617pre_buffer_manager = PreBufferManager()618 619# Sistema di statistiche (senza WebSocket)620def get_system_stats():621 """Ottiene le statistiche di sistema"""622 stats = {}623 624 # Memoria RAM625 memory = psutil.virtual_memory()626 stats['ram_usage'] = memory.percent627 stats['ram_used_gb'] = memory.used / (1024**3) # GB628 stats['ram_total_gb'] = memory.total / (1024**3) # GB629 630 # Utilizzo di rete631 net_io = psutil.net_io_counters()632 stats['network_sent'] = net_io.bytes_sent / (1024**2) # MB633 stats['network_recv'] = net_io.bytes_recv / (1024**2) # MB634 635 # Statistiche pre-buffer636 try:637 with pre_buffer_manager.pre_buffer_lock:638 total_segments = sum(len(segments) for segments in pre_buffer_manager.pre_buffer.values())639 total_size = sum(640 sum(len(content) for content in segments.values())641 for segments in pre_buffer_manager.pre_buffer.values()642 )643 stats['prebuffer_streams'] = len(pre_buffer_manager.pre_buffer)644 stats['prebuffer_segments'] = total_segments645 stats['prebuffer_size_mb'] = round(total_size / (1024 * 1024), 2)646 stats['prebuffer_threads'] = len(pre_buffer_manager.pre_buffer_threads)647 except Exception as e:648 app.logger.error(f"Errore nel calcolo statistiche pre-buffer: {e}")649 stats['prebuffer_streams'] = 0650 stats['prebuffer_segments'] = 0651 stats['prebuffer_size_mb'] = 0652 stats['prebuffer_threads'] = 0653 654 return stats655 656# Avvia il thread di pulizia del buffer657cleanup_thread = Thread(target=pre_buffer_manager.cleanup_old_buffers, daemon=True)658cleanup_thread.start()659 660# --- Log Manager ---661class LogManager:662 def __init__(self):663 pass664 665 def get_log_files(self):666 """Log non salvati su file"""667 return []668 669 def read_log_file(self, filename, lines=100):670 """Log non salvati su file"""671 return ["Log non salvati su file - solo output console"]672 673 def stream_log_file(self, filename):674 """Log non salvati su file"""675 def generate():676 yield f"data: {json.dumps({'error': 'Log non salvati su file'})}\n\n"677 return generate()678 679log_manager = LogManager()680 681# --- Variabili globali per cache e sessioni ---682 683# Inizializza cache globali (verranno sovrascritte da setup_all_caches)684M3U8_CACHE = {}685TS_CACHE = {}686KEY_CACHE = {}687 688# Pool globale di sessioni per connessioni persistenti689SESSION_POOL = {}690SESSION_LOCK = Lock()691 692def connection_manager():693 """Thread per gestire le connessioni persistenti"""694 while True:695 try:696 time.sleep(300) # Controlla ogni 5 minuti697 698 # Statistiche connessioni699 with SESSION_LOCK:700 active_sessions = len(SESSION_POOL)701 app.logger.info(f"Sessioni attive nel pool: {active_sessions}")702 703 # Pulizia periodica delle sessioni inattive704 if active_sessions > 10: # Se troppe sessioni, pulisci705 cleanup_sessions()706 707 except Exception as e:708 app.logger.error(f"Errore nel connection manager: {e}")709 710def cleanup_sessions():711 """Pulisce le sessioni inattive dal pool"""712 global SESSION_POOL, SESSION_LOCK713 714 with SESSION_LOCK:715 for key, session in list(SESSION_POOL.items()):716 try:717 session.close()718 except:719 pass720 SESSION_POOL.clear()721 app.logger.info("Pool di sessioni pulito")722 723# Avvia il thread di gestione connessioni724connection_thread = Thread(target=connection_manager, daemon=True)725connection_thread.start()726 727# --- Configurazione Proxy ---728PROXY_LIST = []729 730def setup_proxies():731 """Carica la lista di proxy dalla variabile PROXY unificata."""732 global PROXY_LIST733 proxies_found = []734 735 # Carica configurazione736 config = config_manager.load_config()737 proxy_value = config.get('PROXY', '')738 739 if proxy_value and proxy_value.strip():740 # Separa i proxy se ce ne sono più di uno741 proxy_list = [p.strip() for p in proxy_value.split(',') if p.strip()]742 743 for proxy in proxy_list:744 # Gestione automatica del tipo di proxy745 if proxy.startswith('socks5://'):746 # Converti SOCKS5 in SOCKS5H per risoluzione DNS remota747 final_proxy_url = 'socks5h' + proxy[len('socks5'):]748 app.logger.info(f"Proxy SOCKS5 convertito: {proxy} -> {final_proxy_url}")749 elif proxy.startswith('socks5h://'):750 final_proxy_url = proxy751 app.logger.info(f"Proxy SOCKS5H configurato: {proxy}")752 elif proxy.startswith('http://') or proxy.startswith('https://'):753 final_proxy_url = proxy754 app.logger.info(f"Proxy HTTP/HTTPS configurato: {proxy}")755 else:756 # Se non ha protocollo, assume HTTP757 if not proxy.startswith('http://') and not proxy.startswith('https://'):758 final_proxy_url = f"http://{proxy}"759 app.logger.info(f"Proxy senza protocollo, convertito in HTTP: {proxy} -> {final_proxy_url}")760 else:761 final_proxy_url = proxy762 app.logger.info(f"Proxy configurato: {proxy}")763 764 proxies_found.append(final_proxy_url)765 766 app.logger.info(f"Trovati {len(proxies_found)} proxy generali. Verranno usati a rotazione per ogni richiesta.")767 768 # Avviso per SOCKS5769 if any('socks5' in proxy for proxy in proxies_found):770 app.logger.info("Assicurati di aver installato la dipendenza per SOCKS: 'pip install PySocks'")771 772 PROXY_LIST = proxies_found773 774 if PROXY_LIST:775 app.logger.info(f"Totale di {len(PROXY_LIST)} proxy generali configurati.")776 else:777 app.logger.info("Nessun proxy generale configurato.")778 779def get_daddy_proxy_list():780 """Carica la lista di proxy specifici per DaddyLive."""781 config = config_manager.load_config()782 daddy_proxy_value = config.get('DADDY_PROXY', '')783 daddy_proxies = []784 785 if daddy_proxy_value and daddy_proxy_value.strip():786 # Separa i proxy se ce ne sono più di uno787 proxy_list = [p.strip() for p in daddy_proxy_value.split(',') if p.strip()]788 789 for proxy in proxy_list:790 # Gestione automatica del tipo di proxy791 if proxy.startswith('socks5://'):792 # Converti SOCKS5 in SOCKS5H per risoluzione DNS remota793 final_proxy_url = 'socks5h' + proxy[len('socks5'):]794 app.logger.info(f"Proxy DaddyLive SOCKS5 convertito: {proxy} -> {final_proxy_url}")795 elif proxy.startswith('socks5h://'):796 final_proxy_url = proxy797 app.logger.info(f"Proxy DaddyLive SOCKS5H configurato: {proxy}")798 elif proxy.startswith('http://') or proxy.startswith('https://'):799 final_proxy_url = proxy800 app.logger.info(f"Proxy DaddyLive HTTP/HTTPS configurato: {proxy}")801 else:802 # Se non ha protocollo, assume HTTP803 if not proxy.startswith('http://') and not proxy.startswith('https://'):804 final_proxy_url = f"http://{proxy}"805 app.logger.info(f"Proxy DaddyLive senza protocollo, convertito in HTTP: {proxy} -> {final_proxy_url}")806 else:807 final_proxy_url = proxy808 app.logger.info(f"Proxy DaddyLive configurato: {proxy}")809 810 daddy_proxies.append(final_proxy_url)811 812 app.logger.info(f"Trovati {len(daddy_proxies)} proxy DaddyLive. Verranno usati a rotazione per contenuti DaddyLive.")813 814 # Avviso per SOCKS5815 if any('socks5' in proxy for proxy in daddy_proxies):816 app.logger.info("Assicurati di aver installato la dipendenza per SOCKS: 'pip install PySocks'")817 818 return daddy_proxies819 820def get_proxy_for_url(url):821 config = config_manager.load_config()822 no_proxy_domains = [d.strip() for d in config.get('NO_PROXY_DOMAINS', '').split(',') if d.strip()]823 824 # Controlla se è un URL DaddyLive825 is_daddylive = (826 'newkso.ru' in url.lower() or 827 '/stream-' in url.lower() or828 re.search(r'/premium(\d+)/mono\.m3u8$', url) is not None829 )830 831 # Se è DaddyLive, usa i proxy specifici832 if is_daddylive:833 daddy_proxies = get_daddy_proxy_list()834 if daddy_proxies:835 chosen_proxy = random.choice(daddy_proxies)836 app.logger.debug(f"Usando proxy DaddyLive per {url}: {chosen_proxy}")837 return {'http': chosen_proxy, 'https': chosen_proxy}838 839 # Altrimenti usa i proxy generali840 if not PROXY_LIST:841 return None842 843 try:844 parsed_url = urlparse(url)845 if any(domain in parsed_url.netloc for domain in no_proxy_domains):846 return None847 except Exception:848 pass849 850 chosen_proxy = random.choice(PROXY_LIST)851 app.logger.debug(f"Usando proxy generale per {url}: {chosen_proxy}")852 return {'http': chosen_proxy, 'https': chosen_proxy}853 854def get_proxy_with_fallback(url, max_retries=3):855 """Ottiene un proxy con fallback automatico in caso di errore"""856 if not PROXY_LIST:857 return None858 859 # Prova diversi proxy in caso di errore860 for attempt in range(max_retries):861 try:862 proxy_config = get_proxy_for_url(url)863 if proxy_config:864 return proxy_config865 except Exception:866 continue867 868 return None869 870def create_robust_session():871 """Crea una sessione con configurazione robusta e keep-alive per connessioni persistenti."""872 session = requests.Session()873 874 # Configurazione Keep-Alive875 session.headers.update({876 'Connection': 'keep-alive',877 'Keep-Alive': f'timeout={KEEP_ALIVE_TIMEOUT}, max={MAX_KEEP_ALIVE_REQUESTS}'878 })879 880 retry_strategy = Retry(881 total=3,882 read=2,883 connect=2,884 backoff_factor=1,885 status_forcelist=[429, 500, 502, 503, 504],886 allowed_methods=["HEAD", "GET", "OPTIONS"]887 )888 889 adapter = HTTPAdapter(890 max_retries=retry_strategy,891 pool_connections=POOL_CONNECTIONS,892 pool_maxsize=POOL_MAXSIZE,893 pool_block=False894 )895 896 session.mount("http://", adapter)897 session.mount("https://", adapter)898 899 return session900 901def get_persistent_session(proxy_url=None):902 """Ottiene una sessione persistente dal pool o ne crea una nuova"""903 global SESSION_POOL, SESSION_LOCK904 905 # Usa proxy_url come chiave, o 'default' se non c'è proxy906 pool_key = proxy_url if proxy_url else 'default'907 908 with SESSION_LOCK:909 if pool_key not in SESSION_POOL:910 session = create_robust_session()911 912 # Configura proxy se fornito913 if proxy_url:914 session.proxies.update({'http': proxy_url, 'https': proxy_url})915 916 SESSION_POOL[pool_key] = session917 app.logger.info(f"Nuova sessione persistente creata per: {pool_key}")918 919 return SESSION_POOL[pool_key]920 921def make_persistent_request(url, headers=None, timeout=None, proxy_url=None, **kwargs):922 """Effettua una richiesta usando connessioni persistenti"""923 session = get_persistent_session(proxy_url)924 925 # Headers per keep-alive926 request_headers = {927 'Connection': 'keep-alive',928 'Keep-Alive': f'timeout={KEEP_ALIVE_TIMEOUT}, max={MAX_KEEP_ALIVE_REQUESTS}'929 }930 931 if headers:932 request_headers.update(headers)933 934 try:935 response = session.get(936 url, 937 headers=request_headers, 938 timeout=timeout or REQUEST_TIMEOUT,939 verify=VERIFY_SSL,940 **kwargs941 )942 return response943 except Exception as e:944 app.logger.error(f"Errore nella richiesta persistente: {e}")945 # In caso di errore, rimuovi la sessione dal pool946 with SESSION_LOCK:947 if proxy_url in SESSION_POOL:948 del SESSION_POOL[proxy_url]949 raise950 951def get_dynamic_timeout(url, base_timeout=REQUEST_TIMEOUT):952 """Calcola timeout dinamico basato sul tipo di risorsa."""953 if '.ts' in url.lower():954 return base_timeout * 2 # Timeout doppio per segmenti TS955 elif '.m3u8' in url.lower():956 return base_timeout * 1.5 # Timeout aumentato per playlist957 else:958 return base_timeout959 960setup_proxies()961setup_all_caches()962 963# --- Dynamic DaddyLive URL Fetcher ---964DADDYLIVE_BASE_URL = None965LAST_FETCH_TIME = 0966FETCH_INTERVAL = 3600967 968def get_daddylive_base_url():969 """Fetches and caches the dynamic base URL for DaddyLive."""970 global DADDYLIVE_BASE_URL, LAST_FETCH_TIME971 current_time = time.time()972 973 if DADDYLIVE_BASE_URL and (current_time - LAST_FETCH_TIME < FETCH_INTERVAL):974 return DADDYLIVE_BASE_URL975 976 try:977 app.logger.info("Fetching dynamic DaddyLive base URL from GitHub...")978 github_url = 'https://raw.githubusercontent.com/thecrewwh/dl_url/refs/heads/main/dl.xml'979 980 # Force direct connection for GitHub (no proxy)981 response = requests.get(982 github_url,983 timeout=REQUEST_TIMEOUT,984 proxies=None, # Force direct connection985 verify=VERIFY_SSL986 )987 response.raise_for_status()988 content = response.text989 match = re.search(r'src\s*=\s*"([^"]*)"', content)990 if match:991 base_url = match.group(1)992 if not base_url.endswith('/'):993 base_url += '/'994 DADDYLIVE_BASE_URL = base_url995 LAST_FETCH_TIME = current_time996 app.logger.info(f"Dynamic DaddyLive base URL updated to: {DADDYLIVE_BASE_URL}")997 return DADDYLIVE_BASE_URL998 except requests.RequestException as e:999 app.logger.error(f"Error fetching dynamic DaddyLive URL: {e}. Using fallback.")1000 1001 DADDYLIVE_BASE_URL = "https://daddylive.sx/"1002 app.logger.info(f"Using fallback DaddyLive URL: {DADDYLIVE_BASE_URL}")1003 return DADDYLIVE_BASE_URL1004 1005get_daddylive_base_url()1006 1007# [Mantieni tutte le funzioni esistenti per il processing DaddyLive...]1008def detect_m3u_type(content):1009 """Rileva se è un M3U (lista IPTV) o un M3U8 (flusso HLS)"""1010 if "#EXTM3U" in content and "#EXTINF" in content:1011 return "m3u8"1012 return "m3u"1013 1014def replace_key_uri(line, headers_query):1015 """Sostituisce l'URI della chiave AES-128 con il proxy"""1016 match = re.search(r'URI="([^"]+)"', line)1017 if match:1018 key_url = match.group(1)1019 proxied_key_url = f"/proxy/key?url={quote(key_url)}&{headers_query}"1020 return line.replace(key_url, proxied_key_url)1021 return line1022 1023def extract_channel_id(url):1024 """Estrae l'ID del canale da vari formati URL"""1025 match_premium = re.search(r'/premium(\d+)/mono\.m3u8$', url)1026 if match_premium:1027 return match_premium.group(1)1028 1029 match_player = re.search(r'/(?:watch|stream|cast|player)/stream-(\d+)\.php', url)1030 if match_player:1031 return match_player.group(1)1032 1033 return None1034 1035def process_daddylive_url(url):1036 """Converte URL vecchi in formati compatibili con DaddyLive 2025"""1037 daddy_base_url = get_daddylive_base_url()1038 daddy_domain = urlparse(daddy_base_url).netloc1039 1040 match_premium = re.search(r'/premium(\d+)/mono\.m3u8$', url)1041 if match_premium:1042 channel_id = match_premium.group(1)1043 new_url = f"{daddy_base_url}watch/stream-{channel_id}.php"1044 app.logger.info(f"URL processato da {url} a {new_url}")1045 return new_url1046 1047 if daddy_domain in url and any(p in url for p in ['/watch/', '/stream/', '/cast/', '/player/']):1048 return url1049 1050 if url.isdigit():1051 return f"{daddy_base_url}watch/stream-{url}.php"1052 1053 return url1054 1055def resolve_m3u8_link(url, headers=None):1056 """1057 Risolve URL con una logica selettiva: processa solo i link riconosciuti come1058 DaddyLive, altrimenti li passa direttamente.1059 """1060 if not url:1061 app.logger.error("Errore: URL non fornito.")1062 return {"resolved_url": None, "headers": {}}1063 1064 current_headers = headers.copy() if headers else {}1065 1066 # 1. Estrazione degli header dall'URL (logica invariata)1067 clean_url = url1068 extracted_headers = {}1069 if '&h_' in url or '%26h_' in url:1070 app.logger.info("Rilevati parametri header nell'URL - Estrazione in corso...")1071 temp_url = url1072 if 'vavoo.to' in temp_url.lower() and '%26' in temp_url:1073 temp_url = temp_url.replace('%26', '&')1074 1075 if '%26h_' in temp_url:1076 temp_url = unquote(unquote(temp_url))1077 1078 url_parts = temp_url.split('&h_', 1)1079 clean_url = url_parts[0]1080 header_params = '&h_' + url_parts[1]1081 1082 for param in header_params.split('&'):1083 if param.startswith('h_'):1084 try:1085 key_value = param[2:].split('=', 1)1086 if len(key_value) == 2:1087 key = unquote(key_value[0]).replace('_', '-')1088 value = unquote(key_value[1])1089 extracted_headers[key] = value1090 except Exception as e:1091 app.logger.error(f"Errore nell'estrazione dell'header {param}: {e}")1092 1093 final_headers = {**current_headers, **extracted_headers}1094 1095 # --- NUOVA SEZIONE DI CONTROLLO ---1096 # 2. Verifica se l'URL deve essere processato come DaddyLive.1097 # La risoluzione speciale si attiva solo se l'URL contiene "newkso.ru"1098 # o "/stream-", altrimenti viene passato direttamente.1099 1100 is_daddylive_link = (1101 'newkso.ru' in clean_url.lower() or 1102 '/stream-' in clean_url.lower() or1103 # Aggiungiamo anche i pattern del vecchio estrattore per mantenere la compatibilità1104 re.search(r'/premium(\d+)/mono\.m3u8$', clean_url) is not None1105 )1106 1107 if not is_daddylive_link:1108 # --- GESTIONE VAVOO ---1109 # Controlla se è un link Vavoo e prova a risolverlo1110 # Supporta sia /vavoo-iptv/play/ che /play/ 1111 if 'vavoo.to' in clean_url.lower() and ('/vavoo-iptv/play/' in clean_url.lower() or '/play/' in clean_url.lower()):1112 app.logger.info(f"Rilevato link Vavoo, tentativo di risoluzione: {clean_url}")1113 1114 try:1115 resolved_vavoo = vavoo_resolver.resolve_vavoo_link(clean_url, verbose=True)1116 if resolved_vavoo:1117 app.logger.info(f"Vavoo risolto con successo: {resolved_vavoo}")1118 return {1119 "resolved_url": resolved_vavoo,1120 "headers": final_headers1121 }1122 else:1123 app.logger.warning(f"Impossibile risolvere il link Vavoo, passo l'originale: {clean_url}")1124 return {1125 "resolved_url": clean_url,1126 "headers": final_headers1127 }1128 except Exception as e:1129 app.logger.error(f"Errore nella risoluzione Vavoo: {e}")1130 return {1131 "resolved_url": clean_url,1132 "headers": final_headers1133 }1134 1135 # Per tutti gli altri link non-DaddyLive1136 app.logger.info(f"URL non riconosciuto come DaddyLive o Vavoo, verrà passato direttamente: {clean_url}")1137 return {1138 "resolved_url": clean_url,1139 "headers": final_headers1140 }1141 # --- FINE DELLA NUOVA SEZIONE ---1142 1143 # 3. Se il controllo è superato, procede con la logica di risoluzione DaddyLive (invariata)1144 app.logger.info(f"Tentativo di risoluzione URL (DaddyLive): {clean_url}")1145 1146 daddy_base_url = get_daddylive_base_url()1147 daddy_origin = urlparse(daddy_base_url).scheme + "://" + urlparse(daddy_base_url).netloc1148 1149 daddylive_headers = {1150 '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',1151 'Referer': daddy_base_url,1152 'Origin': daddy_origin1153 }1154 final_headers_for_resolving = {**final_headers, **daddylive_headers}1155 1156 try:1157 app.logger.info("Ottengo URL base dinamico...")1158 github_url = 'https://raw.githubusercontent.com/thecrewwh/dl_url/refs/heads/main/dl.xml'1159 main_url_req = requests.get(1160 github_url,1161 timeout=REQUEST_TIMEOUT,1162 proxies=get_proxy_for_url(github_url),1163 verify=VERIFY_SSL1164 )1165 main_url_req.raise_for_status()1166 main_url = main_url_req.text1167 baseurl = re.findall('(?s)src = "([^"]*)', main_url)[0]1168 app.logger.info(f"URL base ottenuto: {baseurl}")1169 1170 channel_id = extract_channel_id(clean_url)1171 if not channel_id:1172 app.logger.error(f"Impossibile estrarre ID canale da {clean_url}")1173 return {"resolved_url": clean_url, "headers": current_headers}1174 1175 app.logger.info(f"ID canale estratto: {channel_id}")1176 1177 stream_url = f"{baseurl}stream/stream-{channel_id}.php"1178 app.logger.info(f"URL stream costruito: {stream_url}")1179 1180 final_headers_for_resolving['Referer'] = baseurl + '/'1181 final_headers_for_resolving['Origin'] = baseurl1182 1183 app.logger.info(f"Passo 1: Richiesta a {stream_url}")1184 max_retries = 31185 for retry in range(max_retries):1186 try:1187 proxy_config = get_proxy_with_fallback(stream_url)1188 response = requests.get(stream_url, headers=final_headers_for_resolving, timeout=REQUEST_TIMEOUT, proxies=proxy_config, verify=VERIFY_SSL)1189 response.raise_for_status()1190 break # Success, exit retry loop1191 except requests.exceptions.ProxyError as e:1192 if "429" in str(e) and retry < max_retries - 1:1193 app.logger.warning(f"Proxy rate limited (429), retry {retry + 1}/{max_retries}: {stream_url}")1194 time.sleep(2 ** retry) # Exponential backoff1195 continue1196 else:1197 raise1198 except requests.RequestException as e:1199 if retry < max_retries - 1:1200 app.logger.warning(f"Request failed, retry {retry + 1}/{max_retries}: {stream_url}")