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