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()
|
||||
|
||||
return dict(proxy) if proxy else None
|
||||
except Exception as e:
|
||||
print(f"Erro ao buscar proxy: {e}")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_all_active_proxies() -> list:
|
||||
"""Retorna todos os proxies ativos do banco"""
|
||||
"""
|
||||
Retorna todos os proxies ativos do banco, priorizando:
|
||||
1. Proxies com sucesso recente
|
||||
2. Proxies com baixo failure_count
|
||||
3. Proxies com boa taxa de sucesso
|
||||
4. Proxies novos ainda não testados
|
||||
"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
@@ -61,16 +66,23 @@ def get_all_active_proxies() -> list:
|
||||
country_code, country_name, city, is_active, is_anonymous,
|
||||
response_time_ms, last_checked_at, last_successful_at,
|
||||
failure_count, success_count, usage, source, notes,
|
||||
created_at, updated_at
|
||||
FROM proxies
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY
|
||||
last_successful_at DESC NULLS LAST,
|
||||
response_time_ms ASC NULLS LAST,
|
||||
created_at, updated_at,
|
||||
(CASE WHEN success_count + failure_count > 0
|
||||
THEN CAST(success_count AS FLOAT) / (success_count + failure_count)
|
||||
ELSE 0 END) DESC,
|
||||
ELSE 0.5 END) as success_rate
|
||||
FROM proxies
|
||||
WHERE is_active = TRUE
|
||||
AND failure_count < 8 -- Ignora proxies com muitas falhas
|
||||
ORDER BY
|
||||
-- Prioriza proxies com sucesso recente
|
||||
last_successful_at DESC NULLS LAST,
|
||||
-- Depois por taxa de sucesso
|
||||
success_rate DESC,
|
||||
-- Depois por menos falhas
|
||||
failure_count ASC,
|
||||
-- Por último, proxies novos
|
||||
created_at DESC
|
||||
LIMIT 50 -- Limita a 50 melhores proxies para não perder tempo
|
||||
""")
|
||||
|
||||
proxies = cursor.fetchall()
|
||||
@@ -78,8 +90,7 @@ def get_all_active_proxies() -> list:
|
||||
conn.close()
|
||||
|
||||
return [dict(proxy) for proxy in proxies] if proxies else []
|
||||
except Exception as e:
|
||||
print(f"Erro ao buscar proxies: {e}")
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def delete_proxy(proxy_id: int) -> bool:
|
||||
@@ -101,10 +112,8 @@ def delete_proxy(proxy_id: int) -> bool:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print(f"Proxy {proxy_id} desativado: {updated}")
|
||||
return updated
|
||||
except Exception as e:
|
||||
print(f"Erro ao desativar proxy {proxy_id}: {e}")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def format_proxy_url(proxy: Dict) -> str:
|
||||
@@ -139,8 +148,42 @@ def mark_proxy_success(proxy_id: int) -> bool:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print(f"Proxy (id {proxy_id}) marcado como sucesso")
|
||||
return updated
|
||||
except Exception as e:
|
||||
print(f"Erro ao marcar proxy {proxy_id} como sucesso: {e}")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def mark_proxy_failure(proxy_id: int, max_failures: int = 10) -> bool:
|
||||
"""
|
||||
Marca proxy como falha e desativa se atingir max_failures consecutivas.
|
||||
|
||||
Args:
|
||||
proxy_id: ID do proxy
|
||||
max_failures: Número máximo de falhas antes de desativar (padrão: 10)
|
||||
"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Incrementa failure_count e desativa se atingir o limite
|
||||
cursor.execute("""
|
||||
UPDATE proxies
|
||||
SET failure_count = failure_count + 1,
|
||||
last_checked_at = NOW(),
|
||||
updated_at = NOW(),
|
||||
is_active = CASE
|
||||
WHEN failure_count + 1 >= %s THEN FALSE
|
||||
ELSE is_active
|
||||
END
|
||||
WHERE id = %s
|
||||
RETURNING failure_count, is_active
|
||||
""", (max_failures, proxy_id))
|
||||
|
||||
result = cursor.fetchone()
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return result is not None
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -8,20 +8,15 @@ services:
|
||||
ports:
|
||||
- "3011:8000"
|
||||
volumes:
|
||||
# - /root/videos:/app/videos
|
||||
- ./videos:/app/videos
|
||||
- /root/videos:/app/videos # Linux
|
||||
# - ./videos:/app/videos # Windows
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
# - 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
|
||||
- DB_HOST=${DB_HOST:-postgres}
|
||||
- DB_PORT=${DB_PORT:-5432}
|
||||
- DB_NAME=${DB_NAME:-clipperia}
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
networks:
|
||||
- dokploy-network
|
||||
networks:
|
||||
|
||||
94
main.py
94
main.py
@@ -8,7 +8,6 @@ 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",
|
||||
@@ -77,7 +76,7 @@ def get_video_metadata(
|
||||
}
|
||||
|
||||
try:
|
||||
def extract_metadata(ydl):
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(target, download=False, process=False)
|
||||
|
||||
if not info or 'title' not in info:
|
||||
@@ -102,17 +101,6 @@ def get_video_metadata(
|
||||
|
||||
if 'title' not in info:
|
||||
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:
|
||||
error_msg = str(e).replace('\n', ' ').strip()
|
||||
try:
|
||||
@@ -163,7 +151,7 @@ def download_video(
|
||||
quality_map = {
|
||||
"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"
|
||||
"high": "bestvideo[height>=1080]+bestaudio/bestvideo+bestaudio/best"
|
||||
}
|
||||
qualidade = qualidade.lower()
|
||||
if qualidade not in quality_map:
|
||||
@@ -175,35 +163,33 @@ def download_video(
|
||||
unique_id = str(uuid.uuid4())
|
||||
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],
|
||||
"outtmpl": output_template,
|
||||
"quiet": True,
|
||||
"noplaylist": True,
|
||||
"merge_output_format": "mp4",
|
||||
"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:
|
||||
def download_operation(ydl):
|
||||
base = ydl.extract_info(target, download=False)
|
||||
title = base.get("title", unique_id)
|
||||
with YoutubeDL(metadata_opts) as ydl:
|
||||
metadata = ydl.extract_info(target, download=False)
|
||||
if not metadata or 'title' not in metadata:
|
||||
raise Exception("Não foi possível extrair metadados do vídeo")
|
||||
|
||||
title = metadata.get("title", unique_id)
|
||||
clean_title = sanitize_title(title)
|
||||
filename = f"{clean_title}_{qualidade}.mp4"
|
||||
final_path = os.path.join(videos_dir, filename)
|
||||
|
||||
print('Info ok')
|
||||
|
||||
# Verifica cache
|
||||
if os.path.exists(final_path):
|
||||
return {
|
||||
"videoId": video_id,
|
||||
@@ -211,10 +197,12 @@ def download_video(
|
||||
"cached": True
|
||||
}
|
||||
|
||||
print('Lets download')
|
||||
|
||||
with YoutubeDL(download_opts) as ydl:
|
||||
result = ydl.extract_info(target, download=True)
|
||||
|
||||
if not result:
|
||||
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:
|
||||
@@ -222,7 +210,10 @@ def download_video(
|
||||
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,
|
||||
@@ -230,10 +221,8 @@ def download_video(
|
||||
"cached": False
|
||||
}
|
||||
|
||||
return execute_with_proxy_retry(ydl_opts, download_operation, retry_per_proxy=3)
|
||||
|
||||
except ProxyError as e:
|
||||
raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as 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}"
|
||||
|
||||
try:
|
||||
def search_operation(ydl):
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
search_result = ydl.extract_info(search_query, download=False)
|
||||
entries = search_result.get("entries", [])[:max_results]
|
||||
|
||||
@@ -270,49 +259,32 @@ def search_youtube_yt_dlp(
|
||||
})
|
||||
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:
|
||||
raise HTTPException(status_code=500, detail=f"Erro ao buscar vídeos: {e}")
|
||||
|
||||
@app.get("/list-formats")
|
||||
def list_formats(url: str):
|
||||
opts = {
|
||||
"quiet": False,
|
||||
"no_warnings": False,
|
||||
"noplaylist": True,
|
||||
"quiet": True,
|
||||
"skip_download": True,
|
||||
"nocheckcertificate": True,
|
||||
"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",
|
||||
},
|
||||
"socket_timeout": 8,
|
||||
"retries": 0,
|
||||
"no_check_certificates": True,
|
||||
}
|
||||
|
||||
try:
|
||||
def list_formats_operation(ydl):
|
||||
with YoutubeDL(opts) as 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"),
|
||||
"height": f.get("height"),
|
||||
"fps": f.get("fps"),
|
||||
"v": f.get("vcodec"),
|
||||
"a": f.get("acodec"),
|
||||
"vcodec": f.get("vcodec"),
|
||||
"acodec": 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, retry_per_proxy=2)
|
||||
|
||||
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}")
|
||||
|
||||
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