From 91f76cea6abb4843d951a28176e3c8bb2b5bdf51 Mon Sep 17 00:00:00 2001 From: LeoMortari Date: Thu, 4 Dec 2025 22:15:03 -0300 Subject: [PATCH] Add proxymanager --- .env.example | 6 + PROXY_INTEGRATION.md | 268 ++++++++++++++++++++++++++++++++++++++++ cookies.txt | 24 ++-- database.py | 113 +++++++++++++++++ docker-compose.yml | 24 ++-- dockerfile | 2 +- main.py | 114 +++++++++++------ proxy_manager.py | 98 +++++++++++++++ requirements.txt | 6 +- setup_proxies_table.sql | 55 +++++++++ 10 files changed, 651 insertions(+), 59 deletions(-) create mode 100644 .env.example create mode 100644 PROXY_INTEGRATION.md create mode 100644 database.py create mode 100644 proxy_manager.py create mode 100644 setup_proxies_table.sql diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b5117dd --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Configurações do Banco de Dados PostgreSQL +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=postgres +DB_USER=postgres +DB_PASSWORD=sua_senha_aqui diff --git a/PROXY_INTEGRATION.md b/PROXY_INTEGRATION.md new file mode 100644 index 0000000..be04b10 --- /dev/null +++ b/PROXY_INTEGRATION.md @@ -0,0 +1,268 @@ +# Integração do Sistema de Proxies + +Este documento descreve como o sistema de proxies foi integrado à API do YouTube. + +## Visão Geral + +O sistema de proxies foi implementado para melhorar a confiabilidade da API, usando proxies armazenados em um banco de dados PostgreSQL. Quando um proxy falha, ele é automaticamente removido e outro proxy é testado. + +## Arquitetura + +### Arquivos Criados + +1. **database.py**: Módulo de conexão e operações com PostgreSQL + - `get_db_connection()`: Cria conexão com o banco + - `get_latest_proxy()`: Busca o melhor proxy disponível (baseado em métricas) + - `delete_proxy(proxy_id)`: Marca um proxy como inativo e incrementa failure_count + - `mark_proxy_success(proxy_id)`: Marca sucesso e incrementa success_count + - `format_proxy_url(proxy)`: Formata proxy no padrão yt_dlp (com suporte a autenticação) + +2. **proxy_manager.py**: Lógica de retry automático com proxies + - `is_proxy_error(error_msg)`: Identifica se um erro é relacionado a proxy + - `execute_with_proxy_retry()`: Executa operações com retry automático + +### Estrutura da Tabela de Proxies + +```sql +CREATE TABLE proxies ( + id SERIAL PRIMARY KEY, + ip_address VARCHAR(255) NOT NULL, + port INTEGER NOT NULL, + protocol VARCHAR(10) NOT NULL DEFAULT 'http', + username VARCHAR(255), -- Autenticação (opcional) + password VARCHAR(255), -- Autenticação (opcional) + country_code VARCHAR(10), -- Código do país (ex: US, BR) + country_name VARCHAR(100), -- Nome do país + city VARCHAR(100), -- Cidade + is_active BOOLEAN DEFAULT TRUE, -- Proxy está ativo? + is_anonymous BOOLEAN DEFAULT FALSE, -- Proxy é anônimo? + response_time_ms INTEGER, -- Tempo de resposta em ms + last_checked_at TIMESTAMP, -- Última verificação + last_successful_at TIMESTAMP, -- Último sucesso + failure_count INTEGER DEFAULT 0, -- Contador de falhas + success_count INTEGER DEFAULT 0, -- Contador de sucessos + usage VARCHAR(50), -- Uso do proxy + source VARCHAR(100), -- Fonte do proxy + notes TEXT, -- Notas adicionais + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +## Configuração + +### 1. Variáveis de Ambiente + +Crie um arquivo `.env` baseado no `.env.example`: + +```bash +DB_HOST=seu_host_postgresql +DB_PORT=5432 +DB_NAME=seu_banco +DB_USER=seu_usuario +DB_PASSWORD=sua_senha +``` + +### 2. Docker Compose + +O `docker-compose.yml` já está configurado para carregar as variáveis de ambiente. Use: + +```bash +docker-compose up -d --build +``` + +## Como Funciona + +### Fluxo de Execução + +1. **Seleção Inteligente de Proxy**: A cada requisição, o sistema busca o melhor proxy disponível baseado em: + - Proxies ativos (`is_active = TRUE`) + - Último sucesso mais recente (`last_successful_at DESC`) + - Menor tempo de resposta (`response_time_ms ASC`) + - Maior taxa de sucesso (`success_count / (success_count + failure_count)`) + - Mais recentemente adicionado (`created_at DESC`) + +2. **Tentativa de Execução**: Tenta executar a operação usando o proxy selecionado + - Suporta autenticação automática se o proxy tiver `username` e `password` + - Formato: `protocol://username:password@ip_address:port` + - **Timeout configurado**: 8 segundos para conexão com proxy + +3. **Detecção de Erro**: Se houver erro relacionado a proxy (timeout, connection refused, etc.) + +4. **Desativação do Proxy**: O proxy com problema é marcado como inativo + - `is_active = FALSE` + - `failure_count` incrementado + - `last_checked_at` e `updated_at` atualizados + - **Nota**: O proxy NÃO é deletado, apenas desativado + +5. **Atualização de Sucesso**: Quando a operação é bem-sucedida + - `success_count` incrementado + - `last_successful_at` atualizado + - `is_active = TRUE` (reativa o proxy se estava inativo) + - `last_checked_at` e `updated_at` atualizados + +6. **Retry**: Busca outro proxy ativo e tenta novamente + +7. **Limite de Tentativas**: Máximo de 10 tentativas (configurável) + +### Performance e Timeout + +- **Socket Timeout**: 8 segundos por tentativa de proxy +- **Retry do yt_dlp**: Desabilitado (`retries: 0`) +- **Vantagem**: Ao detectar erro, troca IMEDIATAMENTE de proxy sem tentar novamente no mesmo +- **Comportamento**: + - ❌ **ANTES**: Proxy ruim → tenta 3x no mesmo → 24 segundos perdidos → troca + - ✅ **AGORA**: Proxy ruim → erro após 8s → remove → busca outro → 8 segundos +- **Tempo máximo de espera**: ~80 segundos (10 proxies × 8 segundos cada) +- **Nota**: Se precisar de timeout diferente, altere `socket_timeout` nas opções do yt_dlp + +### Palavras-chave de Erro de Proxy + +O sistema identifica erros de proxy por estas palavras-chave: +- proxy +- connection +- timeout +- timed out +- refused +- unreachable +- unable to connect +- network +- failed to connect +- connection reset +- read timed out +- http error 407 (proxy authentication) +- tunnel connection failed + +### Erros NÃO Tratados como Proxy + +Estes erros NÃO resultam em troca de proxy (são erros legítimos do vídeo): +- "requested format is not available" - Formato solicitado não existe +- "video unavailable" - Vídeo indisponível/removido +- "private video" - Vídeo privado +- "age restricted" - Vídeo com restrição de idade + +Quando esses erros ocorrem, o sistema **não** descarta o proxy e retorna o erro imediatamente. + +### Endpoints Integrados + +Todos os endpoints que usam yt_dlp foram integrados: + +1. **GET /get-video-metadata**: Obtém metadados de vídeos +2. **GET /download-video**: Download de vídeos +3. **GET /search**: Busca de vídeos +4. **GET /list-formats**: Lista formatos disponíveis + +### Tratamento de Erros + +- **ProxyError (503)**: Todos os proxies falharam ou não há proxies disponíveis +- **Exception (500)**: Erros não relacionados a proxy + +## Alimentando o Banco de Proxies + +Para adicionar proxies ao banco, você pode usar o serviço de scraper de proxies que já foi criado. + +### Exemplos de Inserção Manual + +**Proxies sem autenticação:** +```sql +INSERT INTO proxies (ip_address, port, protocol, is_active, is_anonymous, country_code) +VALUES + ('123.456.789.10', 8080, 'http', TRUE, FALSE, 'US'), + ('98.765.432.10', 3128, 'https', TRUE, TRUE, 'BR'), + ('45.67.89.100', 1080, 'socks5', TRUE, TRUE, 'DE'); +``` + +**Proxies com autenticação:** +```sql +INSERT INTO proxies (ip_address, port, protocol, username, password, is_active) +VALUES + ('premium-proxy.example.com', 8080, 'http', 'user123', 'pass456', TRUE), + ('secure-proxy.example.com', 3128, 'https', 'myuser', 'mypass', TRUE); +``` + +### Integração com Scraper + +Quando o serviço de scraper adicionar novos proxies, deve incluir: +- `ip_address`, `port`, `protocol` (obrigatórios) +- `country_code`, `country_name`, `city` (se disponível) +- `is_anonymous` (se detectado) +- `response_time_ms` (tempo de resposta inicial) +- `source` (fonte do scraper) +- `is_active = TRUE` por padrão + +## Monitoramento + +O sistema imprime logs úteis no console: + +``` +Tentativa 1: Usando proxy http://41.65.160.173:8080 (ID: 42) +Erro na tentativa 1: Connection to 41.65.160.173 timed out +Erro identificado como erro de proxy. Removendo proxy ID 42 +Proxy 42 desativado: True + +Tentativa 2: Usando proxy http://98.765.432.10:3128 (ID: 43) +Operação concluída com sucesso na tentativa 2 +Proxy (id 43) marcado como sucesso +``` + +**Importante**: Não haverá mais mensagens como "Retrying (1/3)..." porque desabilitamos o retry interno do yt_dlp. Cada erro resulta em troca imediata de proxy. + +## Vantagens + +1. **Alta Disponibilidade**: Se um proxy falhar, outro é usado automaticamente +2. **Seleção Inteligente**: Proxies são escolhidos baseado em performance e histórico +3. **Auto-recuperação**: Proxies são desativados quando falham, mas podem ser reativados em sucesso futuro +4. **Métricas Automáticas**: Sistema rastreia sucesso/falha e tempo de resposta automaticamente +5. **Suporte a Autenticação**: Proxies com username/password são suportados automaticamente +6. **Sem Intervenção Manual**: O sistema gerencia proxies de forma autônoma +7. **Preservação de Dados**: Proxies não são deletados, apenas desativados +8. **Fácil Integração**: Novo serviço de scraper pode adicionar proxies facilmente + +## Monitoramento e Análise + +### Queries Úteis + +**Ver estatísticas de proxies:** +```sql +SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE is_active = TRUE) as ativos, + COUNT(*) FILTER (WHERE is_active = FALSE) as inativos, + AVG(response_time_ms) as tempo_resposta_medio, + SUM(success_count) as total_sucessos, + SUM(failure_count) as total_falhas +FROM proxies; +``` + +**Ver top 10 melhores proxies:** +```sql +SELECT + ip_address, port, protocol, + success_count, failure_count, + ROUND((success_count::FLOAT / NULLIF(success_count + failure_count, 0) * 100), 2) as taxa_sucesso, + response_time_ms, + last_successful_at +FROM proxies +WHERE is_active = TRUE +ORDER BY + last_successful_at DESC NULLS LAST, + response_time_ms ASC NULLS LAST +LIMIT 10; +``` + +**Reativar proxies desativados há mais de 24h (útil para retry):** +```sql +UPDATE proxies +SET is_active = TRUE, updated_at = NOW() +WHERE is_active = FALSE + AND updated_at < NOW() - INTERVAL '24 hours'; +``` + +## Próximos Passos Sugeridos + +1. ✅ Integrar com o serviço de scraper de proxies +2. ✅ Implementar métricas de sucesso/falha por proxy (CONCLUÍDO) +3. ✅ Implementar sistema de priorização de proxies (CONCLUÍDO) +4. Criar endpoint de administração para visualizar estatísticas de proxies +5. Implementar job periódico para reativar proxies após período de cooldown +6. Adicionar alertas quando número de proxies ativos cair abaixo de threshold diff --git a/cookies.txt b/cookies.txt index 9ee5999..a72f892 100644 --- a/cookies.txt +++ b/cookies.txt @@ -1,15 +1,15 @@ # Netscape HTTP Cookie File # http://curl.haxx.se/rfc/cookie_spec.html # This file was generated by Cookie-Editor -#HttpOnly_.youtube.com TRUE / TRUE 1794924979 __Secure-3PSID g.a0002QjmbYOICQZUY3CXgS7icrDiHCr34OgvmLEhj_-WXtehGROz4rnUCL_CDzWgSy2_ajS8IQACgYKAWkSARcSFQHGX2Mit5xUvoyEpttnPkBJFNvQ1xoVAUF8yKqGstNnUYuRnzC1AUvqW5Uv0076 -#HttpOnly_.youtube.com TRUE / TRUE 1793275018 __Secure-1PSIDTS sidts-CjUBwQ9iIz39DARDr_l3s0hwi46rBOSD9QcsN2tXJ1-GZdvtWYcldB4RPFA3NF5ckRsJ159E0BAA -.youtube.com TRUE / TRUE 1794924979 SAPISID 4dtSnvuM-_s-Txuk/A3LZizab9t-0ZCn93 -#HttpOnly_.youtube.com TRUE / TRUE 1793275088 __Secure-1PSIDCC AKEyXzUcNGQ20nWvJsW57Y2LF_wPfp9E8en6jMMoWK17I-7oWfZzCbvoaYR8iaRnn1ORO8erTrs -#HttpOnly_.youtube.com TRUE / TRUE 1794924979 SSID AhjqEE_bF4hGXq1P- -.youtube.com TRUE / TRUE 1794924979 __Secure-1PAPISID 4dtSnvuM-_s-Txuk/A3LZizab9t-0ZCn93 -#HttpOnly_.youtube.com TRUE / TRUE 1794924979 __Secure-1PSID g.a0002QjmbYOICQZUY3CXgS7icrDiHCr34OgvmLEhj_-WXtehGROz9u8WcTYKHSvqICQAJXHPvAACgYKAeESARcSFQHGX2MiFeAX00Swv_sRptaPCRbzhhoVAUF8yKpk8bKZ6nn6V15r8RrvlKrx0076 -.youtube.com TRUE / TRUE 1794924979 __Secure-3PAPISID 4dtSnvuM-_s-Txuk/A3LZizab9t-0ZCn93 -#HttpOnly_.youtube.com TRUE / TRUE 1793275088 __Secure-3PSIDCC AKEyXzVbwfveYYQHlODM1tNHTQ8NWdYyoBcvFOv3pOOV6kKBaoxB89qhQNspxDaATfaErp9M5A -#HttpOnly_.youtube.com TRUE / TRUE 1793275018 __Secure-3PSIDTS sidts-CjUBwQ9iIz39DARDr_l3s0hwi46rBOSD9QcsN2tXJ1-GZdvtWYcldB4RPFA3NF5ckRsJ159E0BAA -#HttpOnly_.youtube.com TRUE / TRUE 1796299082 LOGIN_INFO AFmmF2swRAIgcxc9ZZDrNadKd07zRSMYJS8B93uU-w5BRRqxnhlRoQoCIFAOYFiGHoFvxjKDzsyv0z8MIGEuobBwzA7OW3XOSGuv:QUQ3MjNmeWxvQmdvdl8tc0RwZ0tINGgtRjlIclQ4aVJKVUcyWE1YU1VtZ1c5ZmtrNE9ITV9RaHEwbUVnZjdrM0VuRmJrOHhqcjJkVmxET2JmLXBuTGxPVXhJb3Ztd3U1WktTTWNOdkZqOWJobkc3ak0xLXBVMTNtUVV5SnZETVFndVoyT3Q1dUxUU2I0d2xUWVlqWWRoZk83UnVmY2FRXzhR -.youtube.com TRUE / TRUE 1796299084 PREF f6=40000000&f7=100&tz=America.Sao_Paulo&f5=20000 \ No newline at end of file +#HttpOnly_.youtube.com TRUE / TRUE 1798947026 __Secure-3PSID g.a0004AjmbYF2ni6VILuHtHbrNEPixsVplj3bhWjP4oLpg2x8ePHLCcU2f8rHGHnKZ6tF4WPjmgACgYKAQgSARcSFQHGX2MidnPkPGR5ZyfIdIkjhheGABoVAUF8yKqv-JnwQByHmqpL6P-MStXL0076 +#HttpOnly_.youtube.com TRUE / TRUE 1796308706 __Secure-1PSIDTS sidts-CjUBwQ9iI_LMheeOuxyo8H3j6AmW0pGwCfeJqiKxVBl-vDKqzSoxgDOgDl7VEIMbCw-bq9rQnxAA +.youtube.com TRUE / TRUE 1798947026 SAPISID 1ISOqtxFED1L7CBO/AaZdMtXj7bL0H53cJ +#HttpOnly_.youtube.com TRUE / TRUE 1796308862 __Secure-1PSIDCC AKEyXzV_o_gY0SWvxYhkPG18QY1K5WyWjSeu5UExJ3l3D5ACem1dAvnbOqtS1m57dpp59PR1Kvo +#HttpOnly_.youtube.com TRUE / TRUE 1798947026 SSID A2EhvNMdrjzSh34YS +.youtube.com TRUE / TRUE 1798947026 __Secure-1PAPISID 1ISOqtxFED1L7CBO/AaZdMtXj7bL0H53cJ +#HttpOnly_.youtube.com TRUE / TRUE 1798947026 __Secure-1PSID g.a0004AjmbYF2ni6VILuHtHbrNEPixsVplj3bhWjP4oLpg2x8ePHLYHzVH4aKkNrn1D3xFXfhrwACgYKAdcSARcSFQHGX2Mi9nbrk-Js3tiL4yHOOKyD5hoVAUF8yKpX_VpYmESBbDae6WG-HYMG0076 +.youtube.com TRUE / TRUE 1798947026 __Secure-3PAPISID 1ISOqtxFED1L7CBO/AaZdMtXj7bL0H53cJ +#HttpOnly_.youtube.com TRUE / TRUE 1796308862 __Secure-3PSIDCC AKEyXzV7gTV1bg7rnjBpMvdW3ltRc8z7brNlBiIeXmJ7la7leaaaxeWD4ZcMpXJNdEMvDf7QSw +#HttpOnly_.youtube.com TRUE / TRUE 1796308706 __Secure-3PSIDTS sidts-CjUBwQ9iI_LMheeOuxyo8H3j6AmW0pGwCfeJqiKxVBl-vDKqzSoxgDOgDl7VEIMbCw-bq9rQnxAA +#HttpOnly_.youtube.com TRUE / TRUE 1799332843 LOGIN_INFO AFmmF2swRAIgXQZ4D55iB15MXvFKEl2FeHTMZGXYWKDonpBPTUfJuFQCICFADaET7BCc2dcp3VLuevpNDv88MCgjbthYlhn2fnVL:QUQ3MjNmd1ZBM1ZCWkJjMkhxdFpicnRSZzFxNXVJWks4ZjBYZUpRYU11SGRER0RwNEFTZHBWSUVVdkc4T3BLMkdkZTRDVy05Vk11U1pYbGNWM291aUFWdkNiT240ZjZabEFLLVVSc2M0ZDhmWXFmM2I2UEVCWUcxQVNXbzNsSXE5OFRLemQwM0NkcVRRRHgzQkplSjl3cF9tS3AydWRYSXFR +.youtube.com TRUE / TRUE 1799332845 PREF f6=40000000&volume=1&f7=100&tz=America.Sao_Paulo&repeat=ALL&f5=20000 \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..787ca94 --- /dev/null +++ b/database.py @@ -0,0 +1,113 @@ +import os +import psycopg2 +from psycopg2.extras import RealDictCursor +from typing import Optional, Dict +from dotenv import load_dotenv + +load_dotenv() + +def get_db_connection(): + return psycopg2.connect( + host=os.getenv("DB_HOST", "localhost"), + port=os.getenv("DB_PORT", "5432"), + database=os.getenv("DB_NAME", "postgres"), + user=os.getenv("DB_USER", "postgres"), + password=os.getenv("DB_PASSWORD", ""), + cursor_factory=RealDictCursor + ) + +def get_latest_proxy() -> Optional[Dict]: + try: + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute(""" + SELECT + id, ip_address, port, protocol, username, password, + 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, + (CASE WHEN success_count + failure_count > 0 + THEN CAST(success_count AS FLOAT) / (success_count + failure_count) + ELSE 0 END) DESC, + created_at DESC + LIMIT 1 + """) + + proxy = cursor.fetchone() + cursor.close() + conn.close() + + return dict(proxy) if proxy else None + except Exception as e: + print(f"Erro ao buscar proxy: {e}") + return None + +def delete_proxy(proxy_id: int) -> bool: + try: + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute(""" + UPDATE proxies + SET is_active = FALSE, + failure_count = failure_count + 1, + last_checked_at = NOW(), + updated_at = NOW() + WHERE id = %s + """, (proxy_id,)) + conn.commit() + + updated = cursor.rowcount > 0 + 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}") + return False + +def format_proxy_url(proxy: Dict) -> str: + protocol = proxy.get('protocol', 'http').lower() + ip_address = proxy.get('ip_address') + port = proxy.get('port') + username = proxy.get('username') + password = proxy.get('password') + + if username and password: + return f"{protocol}://{username}:{password}@{ip_address}:{port}" + else: + return f"{protocol}://{ip_address}:{port}" + +def mark_proxy_success(proxy_id: int) -> bool: + try: + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute(""" + UPDATE proxies + SET success_count = success_count + 1, + last_successful_at = NOW(), + last_checked_at = NOW(), + updated_at = NOW(), + is_active = TRUE + WHERE id = %s + """, (proxy_id,)) + conn.commit() + + updated = cursor.rowcount > 0 + 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}") + return False diff --git a/docker-compose.yml b/docker-compose.yml index a9fef91..21212a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,12 +7,22 @@ services: ports: - "3011:8000" volumes: - - /root/videos:/app/videos - # - ./videos:/app/videos + # - /root/videos:/app/videos + - ./videos:/app/videos environment: - PYTHONUNBUFFERED=1 - networks: - - dokploy-network -networks: - dokploy-network: - external: true + # - DB_HOST=${DB_HOST:-postgres} + # - DB_PORT=${DB_PORT:-5432} + # - DB_NAME=${DB_NAME:-clipperia} + # - DB_USER=${DB_USER} + # - DB_PASSWORD=${DB_PASSWORD} + - DB_HOST=154.12.229.181 + - DB_PORT=5666 + - DB_NAME=clipperia + - DB_USER=leolitas + - DB_PASSWORD=L@l321321321 +# networks: +# - dokploy-network +# networks: +# dokploy-network: +# external: true diff --git a/dockerfile b/dockerfile index 876c0a2..71c8234 100644 --- a/dockerfile +++ b/dockerfile @@ -18,7 +18,7 @@ RUN apt-get update && \ COPY requirements.txt /app/requirements.txt RUN python -m pip install --upgrade pip setuptools wheel && \ - pip install -r /app/requirements.txt && \ + pip install -U -r /app/requirements.txt && \ pip install brotli brotlicffi mutagen certifi RUN printf "%s\n" \ diff --git a/main.py b/main.py index 30fdc0c..8167eb4 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ from youtube_transcript_api.formatters import SRTFormatter from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound from yt_dlp import YoutubeDL from utils import extract_video_id, sanitize_title +from proxy_manager import execute_with_proxy_retry, ProxyError app = FastAPI( title="YouTube Transcript, Download and Metadata API", @@ -71,15 +72,17 @@ def get_video_metadata( 'force_generic_extractor': True, 'format': 'best[ext=mp4]/best[ext=webm]/best', 'allow_unplayable_formats': True, + 'socket_timeout': 8, + 'retries': 0, } try: - with YoutubeDL(ydl_opts) as ydl: + def extract_metadata(ydl): info = ydl.extract_info(target, download=False, process=False) - + if not info or 'title' not in info: info = ydl.extract_info(target, download=False) - + if not info or 'title' not in info: simple_ydl_opts = { 'quiet': True, @@ -89,24 +92,33 @@ def get_video_metadata( } with YoutubeDL(simple_ydl_opts) as simple_ydl: info = simple_ydl.extract_info(target, download=False) - + if not info: raise Exception("Não foi possível extrair as informações do vídeo") - + if isinstance(info, dict): if 'title' not in info and 'url' in info: - with YoutubeDL(ydl_opts) as ydl_redirect: - info = ydl_redirect.extract_info(info['url'], download=False) - + info = ydl.extract_info(info['url'], download=False) + if 'title' not in info: info['title'] = f"Vídeo {videoId or 'desconhecido'}" - + + return info + + info = execute_with_proxy_retry(ydl_opts, extract_metadata) + + except ProxyError as e: + error_msg = str(e).replace('\n', ' ').strip() + raise HTTPException( + status_code=503, + detail=f"Erro com proxies: {error_msg}" + ) except Exception as e: error_msg = str(e).replace('\n', ' ').strip() try: import requests from bs4 import BeautifulSoup - + video_id = videoId or (url.split('v=')[-1].split('&')[0] if 'v=' in url else '') if video_id: response = requests.get(f'https://www.youtube.com/watch?v={video_id}', timeout=10) @@ -122,7 +134,7 @@ def get_video_metadata( } except Exception as fallback_error: pass - + raise HTTPException( status_code=500, detail=f"Erro ao processar o vídeo: {error_msg}" @@ -149,8 +161,8 @@ def download_video( video_id = videoId quality_map = { - "low": "bestvideo[height<=480]+bestaudio/best[height<=480]", - "medium": "bestvideo[height<=720]+bestaudio/best[height<=720]", + "low": "bestvideo[height<=480]+bestaudio/best[height<=480]/bestvideo[height<=480]/best[height<=480]/best", + "medium": "bestvideo[height<=720]+bestaudio/best[height<=720]/bestvideo[height<=720]/best[height<=720]/best", "high": "bestvideo+bestaudio/best" } qualidade = qualidade.lower() @@ -170,10 +182,13 @@ def download_video( "noplaylist": True, "merge_output_format": "mp4", "cookiefile": "/app/cookies.txt", + "socket_timeout": 8, + "retries": 0, + "extractor_retries": 0, } try: - with YoutubeDL(ydl_opts) as ydl: + def download_operation(ydl): base = ydl.extract_info(target, download=False) title = base.get("title", unique_id) clean_title = sanitize_title(title) @@ -181,15 +196,16 @@ def download_video( final_path = os.path.join(videos_dir, filename) print('Info ok') - + if os.path.exists(final_path): return { "videoId": video_id, - "filename": filename + "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: @@ -201,13 +217,18 @@ def download_video( os.rename(real_file_path, final_path) + return { + "videoId": video_id, + "filename": filename, + "cached": False + } + + return execute_with_proxy_retry(ydl_opts, download_operation) + + except ProxyError as e: + raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao baixar vídeo: {e}") - - return { - "videoId": video_id, - "filename": filename - } @app.get("/search") def search_youtube_yt_dlp( @@ -218,12 +239,14 @@ def search_youtube_yt_dlp( "quiet": True, "extract_flat": "in_playlist", "skip_download": True, + "socket_timeout": 8, + "retries": 0, } search_query = f"ytsearch{max_results}:{q}" try: - with YoutubeDL(ydl_opts) as ydl: + def search_operation(ydl): search_result = ydl.extract_info(search_query, download=False) entries = search_result.get("entries", [])[:max_results] @@ -237,8 +260,12 @@ def search_youtube_yt_dlp( "channel": item.get("uploader"), "thumbnail": item.get("thumbnail"), }) - return {"results": results} + return {"results": results} + return execute_with_proxy_retry(ydl_opts, search_operation) + + except ProxyError as e: + raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao buscar vídeos: {e}") @@ -255,17 +282,28 @@ def list_formats(url: str): "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", }, + "socket_timeout": 8, + "retries": 0, } - with YoutubeDL(opts) as y: - info = y.extract_info(url, download=False) - fmts = info.get("formats") or [] - brief = [{ - "id": f.get("format_id"), - "ext": f.get("ext"), - "h": f.get("height"), - "fps": f.get("fps"), - "v": f.get("vcodec"), - "a": f.get("acodec"), - "tbr": f.get("tbr"), - } for f in fmts] - return {"total": len(brief), "formats": brief[:60]} + + try: + def list_formats_operation(ydl): + info = ydl.extract_info(url, download=False) + fmts = info.get("formats") or [] + brief = [{ + "id": f.get("format_id"), + "ext": f.get("ext"), + "h": f.get("height"), + "fps": f.get("fps"), + "v": f.get("vcodec"), + "a": f.get("acodec"), + "tbr": f.get("tbr"), + } for f in fmts] + return {"total": len(brief), "formats": brief[:60]} + + return execute_with_proxy_retry(opts, list_formats_operation) + + except ProxyError as e: + raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erro ao listar formatos: {e}") diff --git a/proxy_manager.py b/proxy_manager.py new file mode 100644 index 0000000..2448465 --- /dev/null +++ b/proxy_manager.py @@ -0,0 +1,98 @@ +from typing import Callable, Any, Optional +from yt_dlp import YoutubeDL +from database import get_latest_proxy, delete_proxy, format_proxy_url, mark_proxy_success + +class ProxyError(Exception): + pass + +def is_proxy_error(error_msg: str) -> bool: + proxy_error_keywords = [ + 'proxy', + 'connection', + 'timeout', + 'timed out', + 'refused', + 'unreachable', + 'unable to connect', + 'network', + 'failed to connect', + 'connection reset', + 'connection aborted', + 'read timed out', + 'http error 407', # Proxy authentication required + 'tunnel connection failed', + 'connect to host', + ] + + non_proxy_error_keywords = [ + 'requested format is not available', + 'format not available', + 'no video formats', + 'video unavailable', + 'private video', + 'age restricted', + ] + + error_lower = error_msg.lower() + + if any(keyword in error_lower for keyword in non_proxy_error_keywords): + return False + + return any(keyword in error_lower for keyword in proxy_error_keywords) + +def execute_with_proxy_retry( + ydl_opts: dict, + operation: Callable[[YoutubeDL], Any], + max_retries: int = 10 +) -> Any: + attempts = 0 + last_error = None + + while attempts < max_retries: + attempts += 1 + proxy_data = None + + try: + proxy_data = get_latest_proxy() + + if proxy_data: + proxy_url = format_proxy_url(proxy_data) + ydl_opts_with_proxy = {**ydl_opts, 'proxy': proxy_url} + print(f"Tentativa {attempts}: Usando proxy {proxy_url} (ID: {proxy_data['id']})") + else: + if attempts == 1: + print(f"Tentativa {attempts}: Nenhum proxy disponível, tentando sem proxy") + ydl_opts_with_proxy = ydl_opts + else: + raise ProxyError("Não há mais proxies disponíveis no banco de dados") + + with YoutubeDL(ydl_opts_with_proxy) as ydl: + result = operation(ydl) + print(f"Operação concluída com sucesso na tentativa {attempts}") + + if proxy_data: + mark_proxy_success(proxy_data['id']) + + return result + + except Exception as e: + error_msg = str(e) + last_error = e + + print(f"Erro na tentativa {attempts}: {error_msg}") + + if is_proxy_error(error_msg): + if proxy_data: + print(f"Erro identificado como erro de proxy. Removendo proxy ID {proxy_data['id']}") + delete_proxy(proxy_data['id']) + else: + print("Erro de proxy mas nenhum proxy estava sendo usado") + + continue + else: + print(f"Erro não é relacionado a proxy, lançando exceção") + raise e + + raise ProxyError( + f"Falha após {max_retries} tentativas. Último erro: {last_error}" + ) diff --git a/requirements.txt b/requirements.txt index db7a9fe..8ac2173 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,8 @@ fastapi uvicorn[standard] youtube-transcript-api==1.2.1 yt-dlp -unidecode \ No newline at end of file +unidecode +psycopg2-binary +python-dotenv +beautifulsoup4 +requests \ No newline at end of file diff --git a/setup_proxies_table.sql b/setup_proxies_table.sql new file mode 100644 index 0000000..cc1a1b9 --- /dev/null +++ b/setup_proxies_table.sql @@ -0,0 +1,55 @@ +-- Script para criar a tabela de proxies +-- Execute este script no seu banco de dados PostgreSQL + +CREATE TABLE IF NOT EXISTS proxies ( + id SERIAL PRIMARY KEY, + ip_address VARCHAR(255) NOT NULL, + port INTEGER NOT NULL, + protocol VARCHAR(10) NOT NULL DEFAULT 'http', + username VARCHAR(255), + password VARCHAR(255), + country_code VARCHAR(10), + country_name VARCHAR(100), + city VARCHAR(100), + is_active BOOLEAN DEFAULT TRUE, + is_anonymous BOOLEAN DEFAULT FALSE, + response_time_ms INTEGER, + last_checked_at TIMESTAMP, + last_successful_at TIMESTAMP, + failure_count INTEGER DEFAULT 0, + success_count INTEGER DEFAULT 0, + usage VARCHAR(50), + source VARCHAR(100), + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT check_protocol CHECK (protocol IN ('http', 'https', 'socks5', 'socks4')) +); + +-- Índices para melhorar performance +CREATE INDEX IF NOT EXISTS idx_proxies_is_active ON proxies(is_active); +CREATE INDEX IF NOT EXISTS idx_proxies_last_successful_at ON proxies(last_successful_at DESC NULLS LAST); +CREATE INDEX IF NOT EXISTS idx_proxies_response_time ON proxies(response_time_ms ASC NULLS LAST); +CREATE INDEX IF NOT EXISTS idx_proxies_created_at ON proxies(created_at DESC); + +-- Trigger para atualizar updated_at automaticamente +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_proxies_updated_at BEFORE UPDATE ON proxies +FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- Exemplo de inserção de proxies (remova ou ajuste conforme necessário) +-- INSERT INTO proxies (ip_address, port, protocol, is_active, is_anonymous) VALUES +-- ('123.456.789.10', 8080, 'http', TRUE, FALSE), +-- ('98.765.432.10', 3128, 'https', TRUE, TRUE), +-- ('45.67.89.100', 1080, 'socks5', TRUE, TRUE); + +-- Exemplo com autenticação +-- INSERT INTO proxies (ip_address, port, protocol, username, password, is_active) VALUES +-- ('proxy.example.com', 8080, 'http', 'user123', 'pass456', TRUE);