Radio Programs & EPG Implementation
Visão Geral
Sistema completo de gestão de programação de rádios (EPG - Electronic Program Guide) usando a API do MyTuner Radio. Permite obter, armazenar e consultar a grade de programação de rádios portuguesas para os próximos 7 dias.
Arquitetura
Componentes Principais
- MyTunerRadioService - Integração com API MyTuner Radio
- RadioSchedule Schema - Modelo de dados para armazenamento EPG
- Scripts de Coleta - Automação de coleta de EPG
- Scripts de Validação - Verificação de qualidade dos dados
MyTuner Radio API
Processo de Autenticação (3 Passos)
A API do MyTuner requer um processo de 3 passos para obter EPG:
// 1. Pesquisar rádio e obter ID
const searchResult = await searchRadio("Antena 1");
// Retorna: { radioId, matchedName, distance }
// 2. Registar widget e obter access token
const accessToken = await registerWidget(searchResult.radioId);
// 3. Obter programação de 7 dias
const schedule = await getStationSchedule(accessToken, searchResult.radioId);
Algoritmo de Matching (Levenshtein Distance)
Para lidar com variações de nomes de rádios, o sistema utiliza:
Limpeza de Nomes:
- Remove sufixos: "(Portugal)", "(AAC)", "(Online)"
- Remove a palavra "rádio" / "Radio"
- Normaliza espaços
Cálculo de Similaridade:
- Algoritmo de Levenshtein Distance
- Retorna distância numérica (0 = match exato)
- Threshold configurável (padrão: 15)
Validação:
if (levenshteinDistance > THRESHOLD) { // Match considerado inválido // Rádio provavelmente não existe no MyTuner }
Exemplo de Matching
Nome Original: "Rádio Comercial (Portugal)"
Nome Limpo: "Comercial"
Resultados MyTuner: ["Rádio Comercial", "Comercial 90.8", ...]
Melhor Match: "Rádio Comercial" (distância: 6)
✅ VÁLIDO (< threshold de 15)
Nome Original: "Rádio Elvas"
Nome Limpo: "Elvas"
Resultados MyTuner: ["Antena 1", "RFM", "Mega Hits", ...]
Melhor Match: "Antena 1" (distância: 17)
❌ INVÁLIDO (> threshold de 15)
Schema de Dados
RadioSchedule
{
radio_id: ObjectId, // Referência à rádio
radio_name: string, // Nome da rádio
mytuner_id: string, // ID no MyTuner
schedule: ProgramEntry[][], // 7 dias de programação
fetched_at: Date, // Data de coleta
expires_at: Date, // Data de expiração (7 dias)
source: 'mytuner' // Fonte dos dados
}
ProgramEntry
{
time: string, // "07:00"
end_time: string, // "10:00"
title: string, // Nome do programa
subtitle: string, // Apresentador/Descrição
description: string, // Descrição adicional
duration: number, // Duração em minutos
// Dias da semana
days: boolean[], // [mon, tue, wed, thu, fri, sat, sun]
monday: boolean,
tuesday: boolean,
wednesday: boolean,
thursday: boolean,
friday: boolean,
saturday: boolean,
sunday: boolean
}
Scripts Disponíveis
1. collect-epg.ts
Propósito: Coletar EPG para todas as rádios portuguesas
Características:
- ✅ Processa apenas rádios sem EPG ou com EPG expirado
- ✅ Validação de matching com threshold
- ✅ Resiliente a erros (continua processamento)
- ✅ Estatísticas detalhadas
- ✅ Configurável
Configurações:
const CONFIG = {
LEVENSHTEIN_THRESHOLD: 15, // Distância máxima aceitável
REQUEST_DELAY_MS: 2000, // Delay entre requests
SCHEDULE_EXPIRATION_DAYS: 7, // Dias até expiração
FORCE_REFRESH: false, // Forçar atualização
};
Execução:
npm run collect:epg
Output:
🔍 Iniciando coleta de EPG para rádios portuguesas
⚙️ Configuração:
- Threshold Levenshtein: 15
- Delay entre requests: 2000ms
- Forçar atualização: NÃO
📻 Total de rádios portuguesas: 230
📋 Rádios a processar: 190
✅ Rádios com EPG válido: 40
[1/190] 📻 Antena 1
✅ EPG salvo com sucesso!
🎯 Match: "Antena 1" → "Antena 1 (Portugal)"
📏 Distância: 11
📊 MyTuner ID: 396103
📊 Programas: 227
⏱️ Expira em: 31/12/2025
...
============================================================
📊 ESTATÍSTICAS FINAIS
============================================================
Total de rádios processadas: 190
✅ Sucesso: 35 (18.4%)
⚠️ Não encontradas no MyTuner: 143 (75.3%)
❌ Matches inválidos (threshold): 10 (5.3%)
🔄 Já tinham EPG válido: 40
❌ Erros: 2
============================================================
✨ Total de rádios com EPG: 75/230 (32.6%)
2. validate-schedules.ts
Propósito: Validar qualidade dos dados armazenados
Verifica:
- Entradas duplicadas por radio_id
- Schedules partilhados entre rádios diferentes
- Estatísticas gerais
Execução:
npm run validate:schedules
Output:
📊 Total de schedules: 40
✅ Sem duplicados por radio_id
⚠️ Encontrados 7 schedules partilhados:
Schedule partilhado por 9 rádios:
- Antena 1
- Antena 1 (Portugal)
- Antena 1 (AAC)
...
📊 Estatísticas:
Total de schedules: 40
Rádios únicas: 40
Duplicados: 0
Schedules partilhados: 7
Total de programas: 4859
Média de programas por rádio: 121.5
3. reprocess-local-radios.ts
Propósito: Reprocessar rádios específicas (para debugging)
Execução:
npm run reprocess:local-radios
4. clean-local-schedules.ts
Propósito: Limpar schedules incorretos
Execução:
npm run clean:local-schedules
Problemas Conhecidos & Limitações
1. Rádios Locais sem EPG
Problema: Muitas rádios locais portuguesas não têm EPG no MyTuner
Comportamento:
- MyTuner retorna "Invalid radio" mesmo quando a rádio existe
- ~75% das rádios portuguesas não têm EPG disponível
Solução: Sistema marca como "não encontrada" e continua
2. Rádios com Variantes
Problema: Mesma rádio aparece múltiplas vezes na BD com nomes diferentes
Exemplos:
- "Antena 1", "Antena 1 (Portugal)", "Antena 1 (AAC)"
- "RFM", "RFM (Portugal)", "RFM AAC"
Impacto: Schedules duplicados mas legítimos
Solução Futura: Normalização de nomes ou merge de rádios duplicadas
3. Retransmissoras
Problema: Rádios locais que são retransmissoras de grandes rádios
Exemplos:
- "Rádio Castelo Branco" → retransmissora RFM
- "Rádio Portalegre" → retransmissora Rádio Comercial
Comportamento: Sistema atribui corretamente o schedule da rádio principal
Nota: Isto é tecnicamente correto, mas pode ser confuso
4. Rate Limiting
Problema: MyTuner pode bloquear requests excessivos
Solução:
- Delay de 2 segundos entre requests
- Cookies de sessão válidos incluídos
5. Cookies Expirados
Problema: Cookies do browser têm validade limitada
Sintoma: Erro 403 ou 401 no MyTuner
Solução:
- Abrir https://mytuner-radio.com em browser
- Copiar novos cookies do DevTools
- Atualizar
sessionCookiesno serviço
Melhorias Futuras
Alta Prioridade
Automatic Cookie Refresh
- Renovar cookies automaticamente
- Evitar necessidade de atualização manual
EPG from Alternative Sources
- RadioBrowser API
- Websites das próprias rádios
- Scraping quando MyTuner não tem dados
Radio Deduplication
- Base implementada na API: campos derivados para agrupamento rápido
- Backfill via endpoint admin para preencher docs antigos
- Fusão/merge final ainda depende de regras de negócio (não automático)
Média Prioridade
Scheduled EPG Collection
- Cron job diário para atualizar EPG expirado
- Notificações de falhas
EPG Quality Metrics
- Dashboard de cobertura
- Alertas para rádios sem EPG
Manual EPG Entry
- Interface para adicionar programação manualmente
- Para rádios sem EPG automático
Baixa Prioridade
EPG Prediction
- ML para prever programação baseado em histórico
- Para rádios com programação regular
Multi-Language Support
- EPG para rádios de outros países
- Suporte a diferentes fusos horários
API de Consulta
Deduplicação (Base)
Para suportar deduplicação sem impactar performance de search, a API pré-computa
campos derivados no Radio (por exemplo: name_normalized, name_tokens,brand_tokens, variant_tokens, stream_domain, homepage_domain,dedupe_fingerprint).
Backfill (Admin)
Como esses campos são computados em pre('save'), documentos antigos podem não
ter esses valores. O admin portal pode disparar o backfill via:
POST /admin/radios/backfill-dedupe-fields- Query params opcionais:
limit(number): limita quantos documentos processarbatchSize(number): tamanho do batch para cursor/bulkWritedryRun(true/1): executa sem persistir updates
- Query params opcionais:
O backfill cria entradas em job_execution_logs com jobType=backfill-radios-dedupe-fields
para visibilidade na página de ingestion status/history.
Obter EPG de uma Rádio
// No controller/service
const schedule = await radioScheduleModel.findOne({
radio_id: radioId,
expires_at: { $gt: new Date() } // Apenas válidos
});
if (!schedule) {
// Rádio não tem EPG ou expirou
return null;
}
// Retornar schedule.schedule (7 dias de programação)
Obter Programa Atual
const now = new Date();
const currentDay = now.getDay(); // 0 = Sunday
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
// Encontrar programa no dia atual
const todayPrograms = schedule.schedule[currentDay];
const currentProgram = todayPrograms.find(program => {
return program.time <= currentTime &&
(!program.end_time || program.end_time > currentTime);
});
Manutenção
Atualizar EPG Manualmente
# Forçar atualização de todas as rádios
# Modificar FORCE_REFRESH = true em collect-epg.ts
npm run collect:epg
Limpar EPG Expirado
// Via MongoDB
db.radioschedules.deleteMany({
expires_at: { $lt: new Date() }
});
Verificar Qualidade
# Executar validação
npm run validate:schedules
# Verificar logs para problemas
Troubleshooting
"Failed to search radio"
Causa: Cookies inválidos ou rate limiting
Solução:
- Verificar cookies no serviço
- Aumentar REQUEST_DELAY_MS
- Verificar conectividade com MyTuner
"Invalid radio" para rádio conhecida
Causa: Rádio não tem EPG no MyTuner (comportamento normal)
Solução: Isto é esperado para ~75% das rádios locais
Match incorreto (distância alta)
Causa: Rádio não existe no MyTuner
Solução:
- Sistema já rejeita automaticamente (threshold)
- Verificar se rádio realmente existe no MyTuner
- Ajustar nome da rádio na BD se necessário
Schedules partilhados suspeitos
Causa: Pode ser legítimo (retransmissoras) ou erro de matching
Solução:
- Executar
validate:schedules - Verificar distância Levenshtein nos logs
- Se inválido, executar
clean:local-schedules
Recursos
- MyTuner Radio: https://mytuner-radio.com
- Levenshtein Distance: https://en.wikipedia.org/wiki/Levenshtein_distance
- Mongoose Docs: https://mongoosejs.com
- NestJS Docs: https://docs.nestjs.com
Contato
Para questões ou problemas, consultar a documentação técnica ou abrir issue no repositório.