CoolFace
Apppublic

MikeRedemtion/Sumatran

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
app.py1767 linesDownload Raw Back to root
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}")

Showing the first 1,200 of 1767 lines. Download the file for the rest.