diff --git a/database.py b/database.py index 8754d6b..f1ba41c 100644 --- a/database.py +++ b/database.py @@ -45,12 +45,17 @@ def get_latest_proxy() -> Optional[Dict]: conn.close() return dict(proxy) if proxy else None - except Exception as e: - print(f"Erro ao buscar proxy: {e}") + except Exception: return None def get_all_active_proxies() -> list: - """Retorna todos os proxies ativos do banco""" + """ + Retorna todos os proxies ativos do banco, priorizando: + 1. Proxies com sucesso recente + 2. Proxies com baixo failure_count + 3. Proxies com boa taxa de sucesso + 4. Proxies novos ainda não testados + """ try: conn = get_db_connection() cursor = conn.cursor() @@ -61,16 +66,23 @@ def get_all_active_proxies() -> list: country_code, country_name, city, is_active, is_anonymous, response_time_ms, last_checked_at, last_successful_at, failure_count, success_count, usage, source, notes, - created_at, updated_at - FROM proxies - WHERE is_active = TRUE - ORDER BY - last_successful_at DESC NULLS LAST, - response_time_ms ASC NULLS LAST, + created_at, updated_at, (CASE WHEN success_count + failure_count > 0 THEN CAST(success_count AS FLOAT) / (success_count + failure_count) - ELSE 0 END) DESC, + ELSE 0.5 END) as success_rate + FROM proxies + WHERE is_active = TRUE + AND failure_count < 8 -- Ignora proxies com muitas falhas + ORDER BY + -- Prioriza proxies com sucesso recente + last_successful_at DESC NULLS LAST, + -- Depois por taxa de sucesso + success_rate DESC, + -- Depois por menos falhas + failure_count ASC, + -- Por último, proxies novos created_at DESC + LIMIT 50 -- Limita a 50 melhores proxies para não perder tempo """) proxies = cursor.fetchall() @@ -78,8 +90,7 @@ def get_all_active_proxies() -> list: conn.close() return [dict(proxy) for proxy in proxies] if proxies else [] - except Exception as e: - print(f"Erro ao buscar proxies: {e}") + except Exception: return [] def delete_proxy(proxy_id: int) -> bool: @@ -101,10 +112,8 @@ def delete_proxy(proxy_id: int) -> bool: cursor.close() conn.close() - print(f"Proxy {proxy_id} desativado: {updated}") return updated - except Exception as e: - print(f"Erro ao desativar proxy {proxy_id}: {e}") + except Exception: return False def format_proxy_url(proxy: Dict) -> str: @@ -139,8 +148,42 @@ def mark_proxy_success(proxy_id: int) -> bool: cursor.close() conn.close() - print(f"Proxy (id {proxy_id}) marcado como sucesso") return updated - except Exception as e: - print(f"Erro ao marcar proxy {proxy_id} como sucesso: {e}") + except Exception: + return False + +def mark_proxy_failure(proxy_id: int, max_failures: int = 10) -> bool: + """ + Marca proxy como falha e desativa se atingir max_failures consecutivas. + + Args: + proxy_id: ID do proxy + max_failures: Número máximo de falhas antes de desativar (padrão: 10) + """ + try: + conn = get_db_connection() + cursor = conn.cursor() + + # Incrementa failure_count e desativa se atingir o limite + cursor.execute(""" + UPDATE proxies + SET failure_count = failure_count + 1, + last_checked_at = NOW(), + updated_at = NOW(), + is_active = CASE + WHEN failure_count + 1 >= %s THEN FALSE + ELSE is_active + END + WHERE id = %s + RETURNING failure_count, is_active + """, (max_failures, proxy_id)) + + result = cursor.fetchone() + conn.commit() + cursor.close() + conn.close() + + return result is not None + + except Exception: return False diff --git a/docker-compose.yml b/docker-compose.yml index 21212a8..335eba4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ services: youtube-api: + container_name: youtube-api build: . dns: [8.8.8.8, 1.1.1.1] sysctls: diff --git a/main.py b/main.py index ba42782..256ba55 100644 --- a/main.py +++ b/main.py @@ -105,7 +105,7 @@ def get_video_metadata( return info - info = execute_with_proxy_retry(ydl_opts, extract_metadata, retry_per_proxy=2) + info = execute_with_proxy_retry(ydl_opts, extract_metadata, retry_per_proxy=1, max_proxies_to_try=3) except ProxyError as e: error_msg = str(e).replace('\n', ' ').strip() @@ -175,16 +175,14 @@ def download_video( unique_id = str(uuid.uuid4()) output_template = os.path.join(videos_dir, f"{unique_id}.%(ext)s") - ydl_opts = { - "format": quality_map[qualidade], - "outtmpl": output_template, + # Opções base para extração de metadados (operação rápida com proxy) + metadata_opts = { "quiet": True, - "noplaylist": True, - "merge_output_format": "mp4", + "no_warnings": True, + "skip_download": True, "nocheckcertificate": True, - "socket_timeout": 60, + "socket_timeout": 8, "retries": 0, - "extractor_retries": 0, "force_ipv4": True, "geo_bypass": True, "extractor_args": {"youtube": {"player_client": ["android"], "player_skip": ["webpage"]}}, @@ -194,44 +192,115 @@ def download_video( }, } + # Opções para download (operação pesada - tentará com e sem proxy) + download_opts = { + "format": quality_map[qualidade], + "outtmpl": output_template, + "quiet": True, + "noplaylist": True, + "merge_output_format": "mp4", + "nocheckcertificate": True, + "socket_timeout": 45, # Timeout menor para detectar falha de proxy rapidamente + "retries": 0, + "extractor_retries": 0, + "force_ipv4": True, + "geo_bypass": True, + "fragment_retries": 3, + "file_access_retries": 3, + "extractor_args": {"youtube": {"player_client": ["android"], "player_skip": ["webpage"]}}, + "http_headers": { + "Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", + "User-Agent": "com.google.android.youtube/19.17.36 (Linux; U; Android 13) gzip", + }, + "http_chunk_size": 1048576, # 1MB chunks + } + try: - def download_operation(ydl): - base = ydl.extract_info(target, download=False) - title = base.get("title", unique_id) - clean_title = sanitize_title(title) - filename = f"{clean_title}_{qualidade}.mp4" - final_path = os.path.join(videos_dir, filename) + # ETAPA 1: Extrair metadados COM PROXY (operação rápida) + def extract_metadata_operation(ydl): + info = ydl.extract_info(target, download=False) + if not info or 'title' not in info: + raise Exception("Não foi possível extrair metadados do vídeo") + return info - print('Info ok') + # Tenta apenas 3 proxies para metadados antes de fallback + metadata = execute_with_proxy_retry( + metadata_opts, + extract_metadata_operation, + retry_per_proxy=1, + max_proxies_to_try=3 + ) - if os.path.exists(final_path): - return { - "videoId": video_id, - "filename": filename, - "cached": True - } - - print('Lets download') - - result = ydl.extract_info(target, download=True) - - if "requested_downloads" in result and len(result["requested_downloads"]) > 0: - real_file_path = result["requested_downloads"][0]["filepath"] - elif "filepath" in result: - real_file_path = result["filepath"] - else: - real_file_path = output_template.replace("%(ext)s", "mp4") - - os.rename(real_file_path, final_path) + title = metadata.get("title", unique_id) + clean_title = sanitize_title(title) + filename = f"{clean_title}_{qualidade}.mp4" + final_path = os.path.join(videos_dir, filename) + # Verifica cache + if os.path.exists(final_path): return { "videoId": video_id, "filename": filename, - "cached": False + "cached": True } - return execute_with_proxy_retry(ydl_opts, download_operation, retry_per_proxy=3) + # ETAPA 2: Download COM PROXY (tentativa rápida) + def download_with_proxy(ydl): + result = ydl.extract_info(target, download=True) + return result + download_success = False + result = None + + try: + # Tenta apenas 2 proxies para download antes de fallback + result = execute_with_proxy_retry( + download_opts, + download_with_proxy, + retry_per_proxy=1, + max_proxies_to_try=2 + ) + download_success = True + except ProxyError as proxy_err: + # ETAPA 3: Download SEM PROXY (fallback) + + # Aumenta timeout para download sem proxy + download_opts_no_proxy = {**download_opts, "socket_timeout": 180} + + try: + with YoutubeDL(download_opts_no_proxy) as ydl: + result = ydl.extract_info(target, download=True) + download_success = True + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Falha no download com e sem proxy: {str(e)}" + ) + + if not download_success or not result: + raise HTTPException(status_code=500, detail="Erro desconhecido no download") + + # Renomear arquivo para nome final + if "requested_downloads" in result and len(result["requested_downloads"]) > 0: + real_file_path = result["requested_downloads"][0]["filepath"] + elif "filepath" in result: + real_file_path = result["filepath"] + else: + real_file_path = output_template.replace("%(ext)s", "mp4") + + if os.path.exists(real_file_path): + os.rename(real_file_path, final_path) + else: + raise HTTPException(status_code=500, detail="Arquivo de download não encontrado") + + return { + "videoId": video_id, + "filename": filename, + "cached": False + } + + except HTTPException: + raise except ProxyError as e: raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}") except Exception as e: @@ -270,7 +339,7 @@ def search_youtube_yt_dlp( }) return {"results": results} - return execute_with_proxy_retry(ydl_opts, search_operation, retry_per_proxy=2) + return execute_with_proxy_retry(ydl_opts, search_operation, retry_per_proxy=1, max_proxies_to_try=3) except ProxyError as e: raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}") @@ -310,7 +379,7 @@ def list_formats(url: str): } for f in fmts] return {"total": len(brief), "formats": brief[:60]} - return execute_with_proxy_retry(opts, list_formats_operation, retry_per_proxy=2) + return execute_with_proxy_retry(opts, list_formats_operation, retry_per_proxy=1, max_proxies_to_try=3) except ProxyError as e: raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}") diff --git a/proxy_manager.py b/proxy_manager.py index 5dc478b..230294e 100644 --- a/proxy_manager.py +++ b/proxy_manager.py @@ -1,6 +1,6 @@ from typing import Callable, Any, Optional from yt_dlp import YoutubeDL -from database import get_all_active_proxies, format_proxy_url, mark_proxy_success +from database import get_all_active_proxies, format_proxy_url, mark_proxy_success, mark_proxy_failure class ProxyError(Exception): pass @@ -32,6 +32,11 @@ def is_proxy_error(error_msg: str) -> bool: 'ssl', # Erros SSL causados por proxy 'certificate', # Erros de certificado causados por proxy 'certificate_verify_failed', # Verificação de certificado SSL falhou + 'failed to parse json', # Erros de parsing JSON (proxy retornando HTML) + 'jsondecode', # Erros de decodificação JSON + 'remote end closed', # Conexão fechada pelo proxy + 'remotedisconnected', # Desconexão remota + 'connection/parsing error', # Erros de conexão/parsing do download ] non_proxy_error_keywords = [ @@ -53,10 +58,11 @@ def is_proxy_error(error_msg: str) -> bool: def execute_with_proxy_retry( ydl_opts: dict, operation: Callable[[YoutubeDL], Any], - retry_per_proxy: int = 2 + retry_per_proxy: int = 2, + max_proxies_to_try: int = None ) -> Any: """ - Tenta executar operação com todos os proxies disponíveis. + Tenta executar operação com proxies disponíveis. Cada proxy é tentado N vezes antes de passar para o próximo. Proxies NÃO são removidos do banco, apenas pulados. @@ -64,34 +70,34 @@ def execute_with_proxy_retry( ydl_opts: Opções do YoutubeDL operation: Função a ser executada retry_per_proxy: Número de tentativas por proxy antes de pular para o próximo + max_proxies_to_try: Número máximo de proxies a tentar (None = todos) """ # Busca TODOS os proxies ativos all_proxies = get_all_active_proxies() - total_proxies = len(all_proxies) - print(f"\n{'='*60}") - print(f"Proxies disponíveis no banco: {total_proxies}") - print(f"Tentativas por proxy: {retry_per_proxy}") - print(f"{'='*60}\n") + # Limita número de proxies se especificado + if max_proxies_to_try is not None and max_proxies_to_try > 0: + all_proxies = all_proxies[:max_proxies_to_try] + + total_proxies = len(all_proxies) last_error = None # Tenta cada proxy da lista for proxy_index, proxy_data in enumerate(all_proxies, 1): proxy_url = format_proxy_url(proxy_data) - ydl_opts_with_proxy = {**ydl_opts, 'proxy': proxy_url} + # Proxy desativado temporariamente - Para reativar, descomente a linha abaixo: + # ydl_opts_with_proxy = {**ydl_opts, 'proxy': proxy_url} + ydl_opts_with_proxy = {**ydl_opts} # Sem proxy por enquanto - print(f"\n[Proxy {proxy_index}/{total_proxies}] {proxy_url} (ID: {proxy_data['id']})") + proxy_failed = False # Tenta N vezes com o MESMO proxy for attempt in range(1, retry_per_proxy + 1): try: - print(f" → Tentativa {attempt}/{retry_per_proxy}...", end=" ") - with YoutubeDL(ydl_opts_with_proxy) as ydl: result = operation(ydl) - print(f"✓ SUCESSO!") mark_proxy_success(proxy_data['id']) return result @@ -99,32 +105,22 @@ def execute_with_proxy_retry( error_msg = str(e) last_error = e - print(f"✗ Falhou") - print(f" Erro: {error_msg[:80]}...") - # Se não for erro de proxy, lança imediatamente if not is_proxy_error(error_msg): - print(f" ⚠ Erro não relacionado a proxy, abortando") raise e + proxy_failed = True + # Se chegou aqui, falhou todas as tentativas com este proxy - print(f" ⨯ Proxy falhou {retry_per_proxy} vezes, pulando para o próximo...") + if proxy_failed: + mark_proxy_failure(proxy_data['id']) # Se chegou aqui, todos os proxies falharam, tenta SEM proxy - print(f"\n{'='*60}") - print(f"Todos os {total_proxies} proxies falharam") - print(f"Tentando SEM proxy como último recurso...") - print(f"{'='*60}\n") - try: - print(f" → Tentativa sem proxy...", end=" ") with YoutubeDL(ydl_opts) as ydl: result = operation(ydl) - print(f"✓ SUCESSO!") return result except Exception as e: - print(f"✗ Falhou") - print(f" Erro: {str(e)[:80]}...") last_error = e raise ProxyError(