Remove proxys e ajusta qualidade dos videos

This commit is contained in:
LeoMortari
2026-01-10 16:37:25 -03:00
parent c6a0ca3c32
commit 171762b01f
4 changed files with 16 additions and 565 deletions

View File

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

129
main.py
View File

@@ -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=1, max_proxies_to_try=3)
except ProxyError as e:
error_msg = str(e).replace('\n', ' ').strip()
raise HTTPException(
status_code=503,
detail=f"Erro com proxies: {error_msg}"
)
except Exception as e:
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,24 +163,12 @@ def download_video(
unique_id = str(uuid.uuid4())
output_template = os.path.join(videos_dir, f"{unique_id}.%(ext)s")
# Opções base para extração de metadados (operação rápida com proxy)
metadata_opts = {
"quiet": True,
"no_warnings": True,
"skip_download": True,
"nocheckcertificate": True,
"socket_timeout": 8,
"retries": 0,
"force_ipv4": True,
"geo_bypass": True,
"extractor_args": {"youtube": {"player_client": ["android"], "player_skip": ["webpage"]}},
"http_headers": {
"Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
"User-Agent": "com.google.android.youtube/19.17.36 (Linux; U; Android 13) gzip",
},
}
# Opções para download (operação pesada - tentará com e sem proxy)
download_opts = {
"format": quality_map[qualidade],
"outtmpl": output_template,
@@ -200,36 +176,13 @@ def download_video(
"noplaylist": True,
"merge_output_format": "mp4",
"nocheckcertificate": True,
"socket_timeout": 45, # Timeout menor para detectar falha de proxy rapidamente
"retries": 0,
"extractor_retries": 0,
"force_ipv4": True,
"geo_bypass": True,
"fragment_retries": 3,
"file_access_retries": 3,
"extractor_args": {"youtube": {"player_client": ["android"], "player_skip": ["webpage"]}},
"http_headers": {
"Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
"User-Agent": "com.google.android.youtube/19.17.36 (Linux; U; Android 13) gzip",
},
"http_chunk_size": 1048576, # 1MB chunks
}
try:
# ETAPA 1: Extrair metadados COM PROXY (operação rápida)
def extract_metadata_operation(ydl):
info = ydl.extract_info(target, download=False)
if not info or 'title' not in info:
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")
return info
# Tenta apenas 3 proxies para metadados antes de fallback
metadata = execute_with_proxy_retry(
metadata_opts,
extract_metadata_operation,
retry_per_proxy=1,
max_proxies_to_try=3
)
title = metadata.get("title", unique_id)
clean_title = sanitize_title(title)
@@ -244,43 +197,12 @@ def download_video(
"cached": True
}
# ETAPA 2: Download COM PROXY (tentativa rápida)
def download_with_proxy(ydl):
with YoutubeDL(download_opts) as ydl:
result = ydl.extract_info(target, download=True)
return result
download_success = False
result = None
try:
# Tenta apenas 2 proxies para download antes de fallback
result = execute_with_proxy_retry(
download_opts,
download_with_proxy,
retry_per_proxy=1,
max_proxies_to_try=2
)
download_success = True
except ProxyError as proxy_err:
# ETAPA 3: Download SEM PROXY (fallback)
# Aumenta timeout para download sem proxy
download_opts_no_proxy = {**download_opts, "socket_timeout": 180}
try:
with YoutubeDL(download_opts_no_proxy) as ydl:
result = ydl.extract_info(target, download=True)
download_success = True
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Falha no download com e sem proxy: {str(e)}"
)
if not download_success or not result:
if not result:
raise HTTPException(status_code=500, detail="Erro desconhecido no download")
# Renomear arquivo para nome final
if "requested_downloads" in result and len(result["requested_downloads"]) > 0:
real_file_path = result["requested_downloads"][0]["filepath"]
elif "filepath" in result:
@@ -301,8 +223,6 @@ def download_video(
except HTTPException:
raise
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}")
@@ -323,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]
@@ -339,49 +259,32 @@ def search_youtube_yt_dlp(
})
return {"results": results}
return execute_with_proxy_retry(ydl_opts, search_operation, retry_per_proxy=1, max_proxies_to_try=3)
except ProxyError as e:
raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}")
except Exception as e:
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=1, max_proxies_to_try=3)
except ProxyError as e:
raise HTTPException(status_code=503, detail=f"Erro com proxies: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao listar formatos: {e}")

View File

@@ -1,129 +0,0 @@
from typing import Callable, Any, Optional
from yt_dlp import YoutubeDL
from database import get_all_active_proxies, format_proxy_url, mark_proxy_success, mark_proxy_failure
class ProxyError(Exception):
pass
def is_proxy_error(error_msg: str) -> bool:
proxy_error_keywords = [
'proxy',
'connection',
'timeout',
'timed out',
'refused',
'unreachable',
'unable to connect',
'network',
'failed to connect',
'connection reset',
'connection aborted',
'read timed out',
'http error 407', # Proxy authentication required
'tunnel connection failed',
'connect to host',
'bot', # YouTube bloqueando proxy como bot
'sign in to confirm', # YouTube pedindo verificação
'http error 429', # Too many requests
'failed to extract', # Falha ao extrair dados (pode ser proxy ruim)
'player response', # Problemas com resposta do player
'http error 403', # Forbidden (pode ser proxy bloqueado)
'http error 503', # Service unavailable (pode ser proxy)
'ssl', # Erros SSL causados por proxy
'certificate', # Erros de certificado causados por proxy
'certificate_verify_failed', # Verificação de certificado SSL falhou
'failed to parse json', # Erros de parsing JSON (proxy retornando HTML)
'jsondecode', # Erros de decodificação JSON
'remote end closed', # Conexão fechada pelo proxy
'remotedisconnected', # Desconexão remota
'connection/parsing error', # Erros de conexão/parsing do download
]
non_proxy_error_keywords = [
'requested format is not available',
'format not available',
'no video formats',
'video unavailable',
'private video',
'age restricted',
]
error_lower = error_msg.lower()
if any(keyword in error_lower for keyword in non_proxy_error_keywords):
return False
return any(keyword in error_lower for keyword in proxy_error_keywords)
def execute_with_proxy_retry(
ydl_opts: dict,
operation: Callable[[YoutubeDL], Any],
retry_per_proxy: int = 2,
max_proxies_to_try: int = None
) -> Any:
"""
Tenta executar operação com proxies disponíveis.
Cada proxy é tentado N vezes antes de passar para o próximo.
Proxies NÃO são removidos do banco, apenas pulados.
Args:
ydl_opts: Opções do YoutubeDL
operation: Função a ser executada
retry_per_proxy: Número de tentativas por proxy antes de pular para o próximo
max_proxies_to_try: Número máximo de proxies a tentar (None = todos)
"""
# Busca TODOS os proxies ativos
all_proxies = get_all_active_proxies()
# Limita número de proxies se especificado
if max_proxies_to_try is not None and max_proxies_to_try > 0:
all_proxies = all_proxies[:max_proxies_to_try]
total_proxies = len(all_proxies)
last_error = None
# Tenta cada proxy da lista
for proxy_index, proxy_data in enumerate(all_proxies, 1):
proxy_url = format_proxy_url(proxy_data)
# Proxy desativado temporariamente - Para reativar, descomente a linha abaixo:
# ydl_opts_with_proxy = {**ydl_opts, 'proxy': proxy_url}
ydl_opts_with_proxy = {**ydl_opts} # Sem proxy por enquanto
proxy_failed = False
# Tenta N vezes com o MESMO proxy
for attempt in range(1, retry_per_proxy + 1):
try:
with YoutubeDL(ydl_opts_with_proxy) as ydl:
result = operation(ydl)
mark_proxy_success(proxy_data['id'])
return result
except Exception as e:
error_msg = str(e)
last_error = e
# Se não for erro de proxy, lança imediatamente
if not is_proxy_error(error_msg):
raise e
proxy_failed = True
# Se chegou aqui, falhou todas as tentativas com este proxy
if proxy_failed:
mark_proxy_failure(proxy_data['id'])
# Se chegou aqui, todos os proxies falharam, tenta SEM proxy
try:
with YoutubeDL(ydl_opts) as ydl:
result = operation(ydl)
return result
except Exception as e:
last_error = e
raise ProxyError(
f"Falha após tentar {total_proxies} proxies + tentativa sem proxy. Último erro: {last_error}"
)

View File

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