Compare commits
4 Commits
c48242ec04
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
171762b01f | ||
|
|
c6a0ca3c32 | ||
|
|
a18159f20e | ||
|
|
33574a62bd |
@@ -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
|
|
||||||
79
database.py
79
database.py
@@ -45,12 +45,17 @@ def get_latest_proxy() -> Optional[Dict]:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return dict(proxy) if proxy else None
|
return dict(proxy) if proxy else None
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"Erro ao buscar proxy: {e}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_all_active_proxies() -> list:
|
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:
|
try:
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -61,16 +66,23 @@ def get_all_active_proxies() -> list:
|
|||||||
country_code, country_name, city, is_active, is_anonymous,
|
country_code, country_name, city, is_active, is_anonymous,
|
||||||
response_time_ms, last_checked_at, last_successful_at,
|
response_time_ms, last_checked_at, last_successful_at,
|
||||||
failure_count, success_count, usage, source, notes,
|
failure_count, success_count, usage, source, notes,
|
||||||
created_at, updated_at
|
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
|
(CASE WHEN success_count + failure_count > 0
|
||||||
THEN CAST(success_count AS FLOAT) / (success_count + failure_count)
|
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
|
created_at DESC
|
||||||
|
LIMIT 50 -- Limita a 50 melhores proxies para não perder tempo
|
||||||
""")
|
""")
|
||||||
|
|
||||||
proxies = cursor.fetchall()
|
proxies = cursor.fetchall()
|
||||||
@@ -78,8 +90,7 @@ def get_all_active_proxies() -> list:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return [dict(proxy) for proxy in proxies] if proxies else []
|
return [dict(proxy) for proxy in proxies] if proxies else []
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"Erro ao buscar proxies: {e}")
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def delete_proxy(proxy_id: int) -> bool:
|
def delete_proxy(proxy_id: int) -> bool:
|
||||||
@@ -101,10 +112,8 @@ def delete_proxy(proxy_id: int) -> bool:
|
|||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
print(f"Proxy {proxy_id} desativado: {updated}")
|
|
||||||
return updated
|
return updated
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"Erro ao desativar proxy {proxy_id}: {e}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def format_proxy_url(proxy: Dict) -> str:
|
def format_proxy_url(proxy: Dict) -> str:
|
||||||
@@ -139,8 +148,42 @@ def mark_proxy_success(proxy_id: int) -> bool:
|
|||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
print(f"Proxy (id {proxy_id}) marcado como sucesso")
|
|
||||||
return updated
|
return updated
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"Erro ao marcar proxy {proxy_id} como sucesso: {e}")
|
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
|
return False
|
||||||
|
|||||||
@@ -8,20 +8,15 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3011:8000"
|
- "3011:8000"
|
||||||
volumes:
|
volumes:
|
||||||
# - /root/videos:/app/videos
|
- /root/videos:/app/videos # Linux
|
||||||
- ./videos:/app/videos
|
# - ./videos:/app/videos # Windows
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
# - DB_HOST=${DB_HOST:-postgres}
|
- DB_HOST=${DB_HOST:-postgres}
|
||||||
# - DB_PORT=${DB_PORT:-5432}
|
- DB_PORT=${DB_PORT:-5432}
|
||||||
# - DB_NAME=${DB_NAME:-clipperia}
|
- DB_NAME=${DB_NAME:-clipperia}
|
||||||
# - DB_USER=${DB_USER}
|
- DB_USER=${DB_USER}
|
||||||
# - DB_PASSWORD=${DB_PASSWORD}
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
- DB_HOST=154.12.229.181
|
|
||||||
- DB_PORT=5666
|
|
||||||
- DB_NAME=clipperia
|
|
||||||
- DB_USER=leolitas
|
|
||||||
- DB_PASSWORD=L@l321321321
|
|
||||||
networks:
|
networks:
|
||||||
- dokploy-network
|
- dokploy-network
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
134
main.py
134
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=2)
|
|
||||||
|
|
||||||
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,65 +163,66 @@ 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")
|
||||||
|
|
||||||
ydl_opts = {
|
metadata_opts = {
|
||||||
|
"quiet": True,
|
||||||
|
"skip_download": True,
|
||||||
|
"nocheckcertificate": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
download_opts = {
|
||||||
"format": quality_map[qualidade],
|
"format": quality_map[qualidade],
|
||||||
"outtmpl": output_template,
|
"outtmpl": output_template,
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
"noplaylist": True,
|
"noplaylist": True,
|
||||||
"merge_output_format": "mp4",
|
"merge_output_format": "mp4",
|
||||||
"nocheckcertificate": True,
|
"nocheckcertificate": True,
|
||||||
"socket_timeout": 60,
|
|
||||||
"retries": 0,
|
|
||||||
"extractor_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",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
def download_operation(ydl):
|
with YoutubeDL(metadata_opts) as ydl:
|
||||||
base = ydl.extract_info(target, download=False)
|
metadata = ydl.extract_info(target, download=False)
|
||||||
title = base.get("title", unique_id)
|
if not metadata or 'title' not in metadata:
|
||||||
clean_title = sanitize_title(title)
|
raise Exception("Não foi possível extrair metadados do vídeo")
|
||||||
filename = f"{clean_title}_{qualidade}.mp4"
|
|
||||||
final_path = os.path.join(videos_dir, filename)
|
|
||||||
|
|
||||||
print('Info ok')
|
title = metadata.get("title", unique_id)
|
||||||
|
clean_title = sanitize_title(title)
|
||||||
if os.path.exists(final_path):
|
filename = f"{clean_title}_{qualidade}.mp4"
|
||||||
return {
|
final_path = os.path.join(videos_dir, filename)
|
||||||
"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)
|
|
||||||
|
|
||||||
|
# Verifica cache
|
||||||
|
if os.path.exists(final_path):
|
||||||
return {
|
return {
|
||||||
"videoId": video_id,
|
"videoId": video_id,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"cached": False
|
"cached": True
|
||||||
}
|
}
|
||||||
|
|
||||||
return execute_with_proxy_retry(ydl_opts, download_operation, retry_per_proxy=3)
|
with YoutubeDL(download_opts) as ydl:
|
||||||
|
result = ydl.extract_info(target, download=True)
|
||||||
|
|
||||||
except ProxyError as e:
|
if not result:
|
||||||
raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}")
|
raise HTTPException(status_code=500, detail="Erro desconhecido no download")
|
||||||
|
|
||||||
|
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 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}")
|
||||||
|
|
||||||
@@ -254,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]
|
||||||
|
|
||||||
@@ -270,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=2)
|
|
||||||
|
|
||||||
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=2)
|
|
||||||
|
|
||||||
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}")
|
||||||
|
|||||||
133
proxy_manager.py
133
proxy_manager.py
@@ -1,133 +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
|
|
||||||
|
|
||||||
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
|
|
||||||
]
|
|
||||||
|
|
||||||
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
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
Tenta executar operação com todos os 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
|
|
||||||
"""
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
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}
|
|
||||||
|
|
||||||
print(f"\n[Proxy {proxy_index}/{total_proxies}] {proxy_url} (ID: {proxy_data['id']})")
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
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
|
|
||||||
|
|
||||||
# Se chegou aqui, falhou todas as tentativas com este proxy
|
|
||||||
print(f" ⨯ Proxy falhou {retry_per_proxy} vezes, pulando para o próximo...")
|
|
||||||
|
|
||||||
# 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(
|
|
||||||
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