Add proxymanager

This commit is contained in:
LeoMortari
2025-12-04 22:15:03 -03:00
parent a1eebaf8e1
commit 91f76cea6a
10 changed files with 651 additions and 59 deletions

6
.env.example Normal file
View File

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

268
PROXY_INTEGRATION.md Normal file
View File

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

View File

@@ -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
#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

113
database.py Normal file
View File

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

View File

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

View File

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

92
main.py
View File

@@ -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,10 +72,12 @@ 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:
@@ -95,12 +98,21 @@ def get_video_metadata(
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:
@@ -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)
@@ -185,7 +200,8 @@ def download_video(
if os.path.exists(final_path):
return {
"videoId": video_id,
"filename": filename
"filename": filename,
"cached": True
}
print('Lets download')
@@ -201,14 +217,19 @@ 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(
q: str = Query(..., description="Termo de busca"),
@@ -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}")

98
proxy_manager.py Normal file
View File

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

View File

@@ -3,3 +3,7 @@ uvicorn[standard]
youtube-transcript-api==1.2.1
yt-dlp
unidecode
psycopg2-binary
python-dotenv
beautifulsoup4
requests

55
setup_proxies_table.sql Normal file
View File

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