Sincronização Automática de Rádios - Radio Browser API
✅ Implementação Completa
Sistema de ingestão automática de rádios usando a Radio Browser API (https://radio-browser.info).
📊 Resultado do Teste
📻 Rádios encontradas na API: 798 (PT + BR)
✅ Rádios adicionadas: 619
⏱️ Duração: 417 segundos (~7 minutos)
❌ Erros: 0
🏗️ Arquitetura
apps/worker/src/app/radios-sync/
├── interfaces/
│ ├── radio-browser.interface.ts # Tipos da Radio Browser API
│ └── sync.interface.ts # Tipos de sincronização
├── schemas/
│ ├── radio.schema.ts # Schema MongoDB
│ └── radios-sync-cursor.schema.ts # Cursor/checkpoint (paginação)
├── services/
│ ├── radio-browser.service.ts # Cliente API Radio Browser
│ ├── stream-validator.service.ts # Validação de streams
│ └── radios-sync.service.ts # Lógica principal
├── schedulers/
│ └── radios-sync.scheduler.ts # Job diário (cron)
├── radios-sync.controller.ts # Endpoints HTTP
└── radios-sync.module.ts # Módulo NestJS
🚀 Funcionalidades
✅ Critérios de Aceitação Atendidos
-
Worker executa diariamente job syncRadios()
- Cron: Todos os dias às 3:00 AM (timezone Lisboa)
- Uma execução de cada vez: lease no cursor (
lease_owner,lease_until),
tomado porsyncRadios()e por isso válido para todos os caminhos (jobs
BullMQ em qualquer réplica, fallback direto do trigger manual, `POST
/radios-sync/run). Renovado antes de cada página e de cada lote; expira após RADIOS_SYNC_LEASE_MSsem renovação (execução parada ou morta). Uma execução que encontra o lease ocupado não sincroniza nada: um job BullMQ fica registadocompleted com o aviso "Skipped: another radio sync run (...) holds the lease" e é adiado (moveToDelayed) para 30 s depois do fim desse lease, sem gastar tentativas; o fallback direto e o HTTP apenas registam o skip. Uma execução que perde o lease para outra para no lote seguinte (superseded`) e não desativa nada.
Atualiza metadados
- Nome, descrição, géneros, logo
- Bitrate, codec, votos, clicks
- Homepage, favicon
Marca rádios offline como is_active = false
- Validação com retry (2 tentativas)
- Timeout de 8 segundos por stream
- Processamento em lotes de 10
Logs claros para debugging
- Níveis: DEBUG, INFO, WARN, ERROR
- Sumário completo após execução
- Contadores e estatísticas
Job configurado com retry/backoff
- Retry automático com RxJS
- Backoff de 1 segundo
- Timeout configurável
🎯 Radio Browser API
Países Sincronizados
- 🇵🇹 Portugal (PT)
- 🇧🇷 Brasil (BR)
Dados Extraídos
{
name: string
stream_url: string
logo: string
genres: string[]
language: string
country: string
city: string
is_active: boolean
radio_browser_uuid: string // UUID único para tracking
homepage: string
favicon: string
bitrate: number
codec: string
votes: number
clickcount: number
}
Stream Quality (probed)
Além dos campos vindos da Radio Browser API (bitrate, codec), o Worker agora persiste um snapshot de qualidade derivado da validação do stream.
Objetivo: ter um sinal consistente (0..1) para usar em ranking (search + popular) e desempate entre duplicados.
Fonte dos dados
- Probe online via
HEAD; quando o servidor rejeitaHEAD, o Worker faz
fallback paraGETem modo stream, pede apenas o primeiro byte e fecha o
corpo assim que recebe os headers. Isto evita desativar streams Icecast
válidos (por exemplo, TSF) sem descarregar áudio continuamente.content-type→ detecção de HLS e codec aproximadoicy-br(quando presente) → bitrate em kbps reportado pelo servidor
- Fallback para Radio Browser (
bitrate/codec) quando não háicy-br
Campos persistidos no Mongo (Radio.stream_quality)
stream_quality?: {
bitrate_kbps?: number
codec?: string
hls?: boolean
content_type?: string
source?: 'icy-br' | 'radio-browser' | 'content-type'
score?: number // 0..1 (normalizado via log1p(bitrate))
last_checked?: Date
}
Atualização
- No sync diário (
syncRadios) — para rádios novas/atualizadas - No
stream-health-check(cron*/15 * * * *por default) — para todas as rádios ativas
As pesquisas que precisam do fallback Radio Browser convertem códigos ISO da
aplicação (por exemplo, pt/pt-PT) para o nome de idioma esperado pelo
serviço (portuguese).
📝 Como Usar
Execução Manual
# Via script
npm run sync:radios
# Via API (se worker estiver rodando)
curl -X POST http://localhost:3001/radios-sync/run
# Status
curl http://localhost:3001/radios-sync/status
Execução Automática
O job executa automaticamente todos os dias às 3:00 AM sem intervenção.
⚙️ Configuração
Scheduler
@Cron(CronExpression.EVERY_DAY_AT_3AM, {
name: 'syncRadios',
timeZone: 'Europe/Lisbon',
})
Parâmetros Ajustáveis
Env vars (Worker)
# Tamanho de página (limit) para paginação na Radio Browser
RADIOS_SYNC_PAGE_SIZE=500
# Orçamento de tempo por execução (ms). Quando atinge o budget, o job grava cursor e continua na próxima execução.
RADIOS_SYNC_TIME_BUDGET_MS=600000
# Duração do lease de execução única (ms), renovado antes de cada página e lote.
# Um job que encontra o lease ocupado é adiado até ao seu fim (+30 s).
RADIOS_SYNC_LEASE_MS=600000
apps/worker/src/app/radios-sync/services/radios-sync.service.ts:
SYNC_COUNTRIES = ['PT', 'BR']; // Países a sincronizar
BATCH_SIZE = 10; // Streams processados em paralelo
apps/worker/src/app/radios-sync/services/stream-validator.service.ts:
REQUEST_TIMEOUT = 8000; // 8 segundos
MAX_RETRIES = 2; // 2 tentativas
🔄 Fluxo de Sincronização
- Buscar rádios da Radio Browser API
- Por país: PT e BR
- Sem limite por país: paginação via
limit+offset PAGE_SIZEconfigurável (default 500)- Cursor/checkpoint persistido no Mongo (
radios-sync-cursor) para retomar execuções longas
Validar streams
- Verifica se stream está online
- Timeout de 8s com 2 retries
- Processa 10 em paralelo
Atualizar base de dados
- Adiciona novas rádios
- Atualiza metadados existentes
- Marca streams offline como inativos
Desativar rádios obsoletas
- Apenas após completar um ciclo completo de paginação (todos os países)
- Baseado em marcador de “last seen” no ciclo (
radio_browser_last_seen_at),
sempre escrito com$max: uma execução de um ciclo antigo nunca baixa a
marca de um ciclo mais recente - Avisos no job log: ciclo abandonado (a lista
SYNC_COUNTRIESmudou sob o
ciclo aberto) e ciclo lento (iniciado há mais de 4 dias; em staging um
ciclo leva ~3 execuções diárias, ~2 dias) - Mantém dados históricos
- Apenas marca is_active = false
📊 Logging
🔄 Iniciando sincronização de rádios...
🌍 Buscando rádios de PT...
✅ Encontradas 398 rádios de PT
🌍 Buscando rádios de BR...
✅ Encontradas 400 rádios de BR
📻 Total de rádios encontradas na API: 798
Processando lote 1/80
🔍 Validando stream: https://...
✅ Nova rádio adicionada: Rádio Comercial
⏭️ Stream offline, ignorando: RFM
═══════════════════════════════════════
📊 SUMÁRIO DA SINCRONIZAÇÃO
═══════════════════════════════════════
📻 Rádios encontradas na API: 798
✅ Rádios adicionadas: 619
📝 Rádios atualizadas: 0
⚠️ Rádios desativadas: 0
❌ Erros: 0
⏱️ Duração: 417.57s
═══════════════════════════════════════
🔧 Troubleshooting
Muitas rádios marcadas offline
- Aumentar timeout:
REQUEST_TIMEOUT = 10000 - Aumentar retries:
MAX_RETRIES = 3
Sincronização muito lenta
- Reduzir países:
SYNC_COUNTRIES = ['PT'] - Aumentar paralelismo:
BATCH_SIZE = 15
Adicionar mais países
SYNC_COUNTRIES = ['PT', 'BR', 'ES', 'FR'];
🎉 Resultado
619 rádios adicionadas de Portugal e Brasil em ~7 minutos, todas validadas e prontas para uso!