Remove proxys e ajusta qualidade dos videos
This commit is contained in:
@@ -1,268 +0,0 @@
|
|||||||
# 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
|
|
||||||
127
main.py
127
main.py
@@ -8,7 +8,6 @@ from youtube_transcript_api.formatters import SRTFormatter
|
|||||||
from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound
|
from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound
|
||||||
from yt_dlp import YoutubeDL
|
from yt_dlp import YoutubeDL
|
||||||
from utils import extract_video_id, sanitize_title
|
from utils import extract_video_id, sanitize_title
|
||||||
from proxy_manager import execute_with_proxy_retry, ProxyError
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="YouTube Transcript, Download and Metadata API",
|
title="YouTube Transcript, Download and Metadata API",
|
||||||
@@ -77,7 +76,7 @@ def get_video_metadata(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
def extract_metadata(ydl):
|
with YoutubeDL(ydl_opts) as ydl:
|
||||||
info = ydl.extract_info(target, download=False, process=False)
|
info = ydl.extract_info(target, download=False, process=False)
|
||||||
|
|
||||||
if not info or 'title' not in info:
|
if not info or 'title' not in info:
|
||||||
@@ -102,17 +101,6 @@ def get_video_metadata(
|
|||||||
|
|
||||||
if 'title' not in info:
|
if 'title' not in info:
|
||||||
info['title'] = f"Vídeo {videoId or 'desconhecido'}"
|
info['title'] = f"Vídeo {videoId or 'desconhecido'}"
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
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()
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=503,
|
|
||||||
detail=f"Erro com proxies: {error_msg}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e).replace('\n', ' ').strip()
|
error_msg = str(e).replace('\n', ' ').strip()
|
||||||
try:
|
try:
|
||||||
@@ -163,7 +151,7 @@ def download_video(
|
|||||||
quality_map = {
|
quality_map = {
|
||||||
"low": "bestvideo[height<=480]+bestaudio/best[height<=480]/bestvideo[height<=480]/best[height<=480]/best",
|
"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",
|
"medium": "bestvideo[height<=720]+bestaudio/best[height<=720]/bestvideo[height<=720]/best[height<=720]/best",
|
||||||
"high": "bestvideo+bestaudio/best"
|
"high": "bestvideo[height>=1080]+bestaudio/bestvideo+bestaudio/best"
|
||||||
}
|
}
|
||||||
qualidade = qualidade.lower()
|
qualidade = qualidade.lower()
|
||||||
if qualidade not in quality_map:
|
if qualidade not in quality_map:
|
||||||
@@ -175,24 +163,12 @@ def download_video(
|
|||||||
unique_id = str(uuid.uuid4())
|
unique_id = str(uuid.uuid4())
|
||||||
output_template = os.path.join(videos_dir, f"{unique_id}.%(ext)s")
|
output_template = os.path.join(videos_dir, f"{unique_id}.%(ext)s")
|
||||||
|
|
||||||
# Opções base para extração de metadados (operação rápida com proxy)
|
|
||||||
metadata_opts = {
|
metadata_opts = {
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
"no_warnings": True,
|
|
||||||
"skip_download": True,
|
"skip_download": True,
|
||||||
"nocheckcertificate": True,
|
"nocheckcertificate": True,
|
||||||
"socket_timeout": 8,
|
|
||||||
"retries": 0,
|
|
||||||
"force_ipv4": True,
|
|
||||||
"geo_bypass": True,
|
|
||||||
"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",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Opções para download (operação pesada - tentará com e sem proxy)
|
|
||||||
download_opts = {
|
download_opts = {
|
||||||
"format": quality_map[qualidade],
|
"format": quality_map[qualidade],
|
||||||
"outtmpl": output_template,
|
"outtmpl": output_template,
|
||||||
@@ -200,36 +176,13 @@ def download_video(
|
|||||||
"noplaylist": True,
|
"noplaylist": True,
|
||||||
"merge_output_format": "mp4",
|
"merge_output_format": "mp4",
|
||||||
"nocheckcertificate": True,
|
"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:
|
try:
|
||||||
# ETAPA 1: Extrair metadados COM PROXY (operação rápida)
|
with YoutubeDL(metadata_opts) as ydl:
|
||||||
def extract_metadata_operation(ydl):
|
metadata = ydl.extract_info(target, download=False)
|
||||||
info = ydl.extract_info(target, download=False)
|
if not metadata or 'title' not in metadata:
|
||||||
if not info or 'title' not in info:
|
|
||||||
raise Exception("Não foi possível extrair metadados do vídeo")
|
raise Exception("Não foi possível extrair metadados do vídeo")
|
||||||
return info
|
|
||||||
|
|
||||||
# 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
|
|
||||||
)
|
|
||||||
|
|
||||||
title = metadata.get("title", unique_id)
|
title = metadata.get("title", unique_id)
|
||||||
clean_title = sanitize_title(title)
|
clean_title = sanitize_title(title)
|
||||||
@@ -244,43 +197,12 @@ def download_video(
|
|||||||
"cached": True
|
"cached": True
|
||||||
}
|
}
|
||||||
|
|
||||||
# ETAPA 2: Download COM PROXY (tentativa rápida)
|
with YoutubeDL(download_opts) as ydl:
|
||||||
def download_with_proxy(ydl):
|
|
||||||
result = ydl.extract_info(target, download=True)
|
result = ydl.extract_info(target, download=True)
|
||||||
return result
|
|
||||||
|
|
||||||
download_success = False
|
if not result:
|
||||||
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")
|
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:
|
if "requested_downloads" in result and len(result["requested_downloads"]) > 0:
|
||||||
real_file_path = result["requested_downloads"][0]["filepath"]
|
real_file_path = result["requested_downloads"][0]["filepath"]
|
||||||
elif "filepath" in result:
|
elif "filepath" in result:
|
||||||
@@ -301,8 +223,6 @@ def download_video(
|
|||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except ProxyError as e:
|
|
||||||
raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Erro ao baixar vídeo: {e}")
|
raise HTTPException(status_code=500, detail=f"Erro ao baixar vídeo: {e}")
|
||||||
|
|
||||||
@@ -323,7 +243,7 @@ def search_youtube_yt_dlp(
|
|||||||
search_query = f"ytsearch{max_results}:{q}"
|
search_query = f"ytsearch{max_results}:{q}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
def search_operation(ydl):
|
with YoutubeDL(ydl_opts) as ydl:
|
||||||
search_result = ydl.extract_info(search_query, download=False)
|
search_result = ydl.extract_info(search_query, download=False)
|
||||||
entries = search_result.get("entries", [])[:max_results]
|
entries = search_result.get("entries", [])[:max_results]
|
||||||
|
|
||||||
@@ -339,49 +259,32 @@ def search_youtube_yt_dlp(
|
|||||||
})
|
})
|
||||||
return {"results": results}
|
return {"results": results}
|
||||||
|
|
||||||
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}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Erro ao buscar vídeos: {e}")
|
raise HTTPException(status_code=500, detail=f"Erro ao buscar vídeos: {e}")
|
||||||
|
|
||||||
@app.get("/list-formats")
|
@app.get("/list-formats")
|
||||||
def list_formats(url: str):
|
def list_formats(url: str):
|
||||||
opts = {
|
opts = {
|
||||||
"quiet": False,
|
"quiet": True,
|
||||||
"no_warnings": False,
|
"skip_download": True,
|
||||||
"noplaylist": True,
|
|
||||||
"nocheckcertificate": True,
|
"nocheckcertificate": True,
|
||||||
"force_ipv4": True,
|
"no_check_certificates": True,
|
||||||
"geo_bypass": True,
|
|
||||||
"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",
|
|
||||||
},
|
|
||||||
"socket_timeout": 8,
|
|
||||||
"retries": 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
def list_formats_operation(ydl):
|
with YoutubeDL(opts) as ydl:
|
||||||
info = ydl.extract_info(url, download=False)
|
info = ydl.extract_info(url, download=False)
|
||||||
fmts = info.get("formats") or []
|
fmts = info.get("formats") or []
|
||||||
brief = [{
|
brief = [{
|
||||||
"id": f.get("format_id"),
|
"id": f.get("format_id"),
|
||||||
"ext": f.get("ext"),
|
"ext": f.get("ext"),
|
||||||
"h": f.get("height"),
|
"height": f.get("height"),
|
||||||
"fps": f.get("fps"),
|
"fps": f.get("fps"),
|
||||||
"v": f.get("vcodec"),
|
"vcodec": f.get("vcodec"),
|
||||||
"a": f.get("acodec"),
|
"acodec": f.get("acodec"),
|
||||||
"tbr": f.get("tbr"),
|
"tbr": f.get("tbr"),
|
||||||
} for f in fmts]
|
} for f in fmts]
|
||||||
return {"total": len(brief), "formats": brief[:60]}
|
return {"total": len(brief), "formats": brief[:60]}
|
||||||
|
|
||||||
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}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Erro ao listar formatos: {e}")
|
raise HTTPException(status_code=500, detail=f"Erro ao listar formatos: {e}")
|
||||||
|
|||||||
129
proxy_manager.py
129
proxy_manager.py
@@ -1,129 +0,0 @@
|
|||||||
from typing import Callable, Any, Optional
|
|
||||||
from yt_dlp import YoutubeDL
|
|
||||||
from database import get_all_active_proxies, format_proxy_url, mark_proxy_success, mark_proxy_failure
|
|
||||||
|
|
||||||
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',
|
|
||||||
'bot', # YouTube bloqueando proxy como bot
|
|
||||||
'sign in to confirm', # YouTube pedindo verificação
|
|
||||||
'http error 429', # Too many requests
|
|
||||||
'failed to extract', # Falha ao extrair dados (pode ser proxy ruim)
|
|
||||||
'player response', # Problemas com resposta do player
|
|
||||||
'http error 403', # Forbidden (pode ser proxy bloqueado)
|
|
||||||
'http error 503', # Service unavailable (pode ser proxy)
|
|
||||||
'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 = [
|
|
||||||
'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],
|
|
||||||
retry_per_proxy: int = 2,
|
|
||||||
max_proxies_to_try: int = None
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
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.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
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()
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
# 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
|
|
||||||
|
|
||||||
proxy_failed = False
|
|
||||||
|
|
||||||
# Tenta N vezes com o MESMO proxy
|
|
||||||
for attempt in range(1, retry_per_proxy + 1):
|
|
||||||
try:
|
|
||||||
with YoutubeDL(ydl_opts_with_proxy) as ydl:
|
|
||||||
result = operation(ydl)
|
|
||||||
|
|
||||||
mark_proxy_success(proxy_data['id'])
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
last_error = e
|
|
||||||
|
|
||||||
# Se não for erro de proxy, lança imediatamente
|
|
||||||
if not is_proxy_error(error_msg):
|
|
||||||
raise e
|
|
||||||
|
|
||||||
proxy_failed = True
|
|
||||||
|
|
||||||
# Se chegou aqui, falhou todas as tentativas com este proxy
|
|
||||||
if proxy_failed:
|
|
||||||
mark_proxy_failure(proxy_data['id'])
|
|
||||||
|
|
||||||
# Se chegou aqui, todos os proxies falharam, tenta SEM proxy
|
|
||||||
try:
|
|
||||||
with YoutubeDL(ydl_opts) as ydl:
|
|
||||||
result = operation(ydl)
|
|
||||||
return result
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
|
|
||||||
raise ProxyError(
|
|
||||||
f"Falha após tentar {total_proxies} proxies + tentativa sem proxy. Último erro: {last_error}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
-- 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);
|
|
||||||
Reference in New Issue
Block a user