API "Agora a Tocar" (Now Playing)
Visão Geral
Esta API fornece duas funcionalidades complementares para informar o utilizador sobre o que está a ser transmitido numa rádio:
- Metadata ICY - Informação sobre a música/conteúdo atual extraída do stream de áudio
- Programa Atual - Informação sobre o programa de rádio que está no ar baseado no EPG (Electronic Program Guide)
Endpoint: Now Playing (Metadata ICY)
GET /radios/:id/now-playing
Descrição
Retorna informação sobre o que está a tocar numa rádio específica.
O endpoint é read-only (não faz fetch a streams ao vivo):
- devolve imediatamente o
now_playingpersistido quando ainda está fresco (< 15 min), preservandosource(live-collected,fingerprintouclient) e oadapterque o produziu; - se o cache expirou, devolve o último valor persistido (stale, sem limite de idade) com
isFresh: falseesource: "lastKnown"— nunca bloqueia; - se não existir nenhum valor persistido, devolve
"Sem informação"comisFresh: false,source: "synthetic", e semupdatedAtfresco artificial.
O rádio expõe icy_metadata_valid; o REST/SSE de now-playing expõe a mesma
política como icyMetadataValid. false invalida apenas metadata do stream e
relatórios ICY do cliente, mantendo adapters dedicados e fingerprint ativos.
O campo ausente mantém o comportamento anterior. A app aplica a política à UI,
aos relatórios e à sessão nativa, sem conhecer nomes de rádios. Na Antena 1 São
Paulo, o backend força false pelo URL e o job de capability persiste a flag,
sem alterar enable_icy_epg nem a capability full. Payloads Redis antigos sem
política são reavaliados em MongoDB e voltam a ser publicados com a flag.ICY Valid no Data Quality continua a medir frescura, não esta política.
A atualização do now_playing pode vir do Worker (ICY collector ou fingerprint) ou de um relatório ICY do cliente autenticado. Todos os escritores persistem a mesma proveniência em Redis + MongoDB.
Na Antena 1 de São Paulo, o adapter antena1 usa o feed oficial de música atual,
preservando o timestamp fornecido pela rádio. A identificação ANTENA 1 - ANTENA 1
é ignorada no coletor e nos relatórios do cliente e não substitui uma música válida.
O reconhecimento existente cobre também esta rádio após dois probes vazios.
Ver routing, fallback, monitorização e custo.
Streams http(s)://<host>/listen/<shortcode>/<ficheiro> (AzuraCast) usam o adapterazuracast, que lê GET <origin>/api/nowplaying/<shortcode> e preserva played_at
como timestamp. Se a API falhar ou estiver desatualizada, cai no caminho ICY
genérico; uma resposta saudável sem música (jingle, anúncio, nome de ficheiro) dá um
probe vazio. O reconhecimento existente cobre estas rádios após dois probes vazios.
Ver routing, validação, fallback e custo.
Parâmetros
Path Parameters
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id |
string | Sim | ID MongoDB da rádio (24 caracteres hexadecimais) |
Resposta
Formato
{
"radioId": "string",
"radioName": "string",
"currentTrack": "string",
"isFresh": boolean,
"source": "live-collected | fingerprint | client | lastKnown | synthetic",
"adapter": "string",
"artist": "string",
"track": "string",
"station": "string",
"genre": "string",
"bitrate": number,
"updatedAt": "string (ISO 8601)",
"spotifyUrl": "string",
"youtubeUrl": "string",
"albumArtUrl": "string"
}
Campos
- radioId: Identificador único da rádio
- radioName: Nome da rádio na plataforma
- currentTrack: Título completo do que está a tocar (formato original do stream)
- isFresh:
trueapenas quando a observação é recente o suficiente para ser apresentada como live (< 15 min) - source: origem semântica do payload:
live-collectedpara collector/ICY,fingerprintpara reconhecimento de áudio,clientpara ICY observado durante a reprodução,lastKnownpara um valor stale esyntheticpara placeholder sem observação real - adapter: adapter que produziu o valor (
icy, adapter dedicado,audio-fingerprintouclient-icy); opcional para registos antigos durante a transição - artist: Artista da música (extraído automaticamente quando possível)
- track: Título da música (extraído automaticamente quando possível)
- station: Nome da estação reportado pelo stream
- genre: Género musical da rádio
- bitrate: Taxa de bits do stream em kbps
- updatedAt: Timestamp da última observação real. Pode estar ausente em respostas
source: "synthetic". - spotifyUrl: Link Spotify quando já existir enriquecimento persistido
- youtubeUrl: Link de pesquisa no YouTube Music para o artista/faixa
- albumArtUrl: Capa do álbum quando já existir enriquecimento persistido
Exemplos de Uso
Pedido Básico
curl -X GET "http://localhost:3000/api/radios/694830c00e1febe2e0cbfe38/now-playing"
Resposta de Sucesso
{
"radioId": "694830c00e1febe2e0cbfe38",
"radioName": "Rádio Comercial",
"currentTrack": "The Weeknd - Blinding Lights",
"isFresh": true,
"source": "live-collected",
"artist": "The Weeknd",
"track": "Blinding Lights",
"station": "Rádio Comercial",
"genre": "Pop",
"bitrate": 128,
"updatedAt": "2025-12-23T21:30:00.000Z"
}
Resposta Stale / Último Valor Conhecido
{
"radioId": "694830c00e1febe2e0cbfe38",
"radioName": "Cidade_fm",
"currentTrack": "Hugel, Topic, Arash, Daecolm - I Adore You",
"isFresh": false,
"source": "lastKnown",
"artist": "Hugel, Topic, Arash, Daecolm",
"track": "I Adore You",
"station": "Cidade_fm",
"genre": "Pop",
"bitrate": 128,
"updatedAt": "2026-03-28T21:13:12.725Z"
}
Clientes podem mostrar este valor como Última detecção, mas não devem rotulá-lo como A tocar agora nem mostrar ações de favorito de música como se fosse atual.
Resposta Quando Não Há Metadata Conhecida
{
"radioId": "694830c00e1febe2e0cbfe38",
"radioName": "Smooth FM",
"currentTrack": "Sem informação",
"isFresh": false,
"source": "synthetic",
"station": "Smooth FM",
"genre": "Jazz",
"bitrate": 128
}
Comportamento e Cache
Sistema de Cache (3 camadas)
A API nunca consulta streams ao vivo no GET. A metadata é populada pelo Worker ou pelo endpoint autenticado de relatório do cliente.
| Camada | Armazenamento | TTL | Propósito |
|---|---|---|---|
| 1 | In-memory Map |
30 s | Evitar queries MongoDB em polls consecutivos |
| 2 | Redis (NowPlayingCacheService) |
120 s | Cache partilhável, populado pelo ICY collector |
| 3 | MongoDB (radios.now_playing) |
15 min (freshness) | Fonte persistente; aceite stale sem limite de idade |
- Fonte primária:
radios.now_playingpersistido pelo collector, fingerprint ou cliente - TTL de freshness: 15 minutos (
NOW_PLAYING_CACHE_MAX_AGE_MS) - Fallback stale: Quando o cache expira, o último valor persistido é devolvido (sem limite de idade) com
isFresh: false,source: "lastKnown" - Fallback final: Se não existir nenhum valor, devolve
"Sem informação"comisFresh: false,source: "synthetic", semupdatedAt
Sem consulta ao stream
O endpoint GET /radios/:id/now-playing é puramente read-only. A responsabilidade de atualizar metadata está no Worker (ICY collector), que:
- Corre em ciclos periódicos (Container App)
- Liga-se aos streams de rádio e extrai metadata ICY
- Persiste em Redis + MongoDB
- Emite eventos SSE quando a faixa muda
Quando uma rádio devolve seis sondas ICY vazias consecutivas, o collector abre um circuit breaker e reduz novas sondas a um canário por hora. O reconhecimento por fingerprint continua elegível. Uma sonda ICY útil fecha imediatamente o circuito ao repor icy_empty_streak a zero.
Para observabilidade, now_playing_adapter identifica quem produziu o payload atual; now_playing_last_probe_adapter, now_playing_last_probe_at e now_playing_last_probe_status descrevem separadamente a última sonda do collector. Uma sonda ICY vazia nunca substitui a proveniência de um valor atual produzido por fingerprint.
Antes de comparar, persistir, calcular o hash de dedupe, preencher cache ou emitir SSE, os writers ICY/cliente/fingerprint canonizam entidades HTML, incluindo entidades numéricas e valores duplamente codificados. O collector ICY e o relatório do cliente comparam a tupla canónica completa (StreamTitle, artist, track), pelo que alterações ou remoções dos campos estruturados são propagadas mesmo quando o título não muda. Nessas transições, a atualização MongoDB usa $set para os campos presentes e $unset para os ausentes; assim, valores de uma faixa anterior não chegam ao contrato REST/SSE.
A continuidade das filas icy-collector e radio-recognition, incluindo heartbeat, recuperação e operação dos alertas, está documentada em ../operations/NOW_PLAYING_CONTINUITY.md.
Enriquecimento de Links
- O endpoint tenta anexar links já persistidos em
MusicLink - Se ainda não existir um
MusicLink, a criação/enriquecimento é disparada em background - O enriquecimento corre para observações reais com
isFresh: true(live-collected,fingerprintouclient); dados stale/sintéticos não criam links como se fossem música atual - O request path não espera por retries do Spotify nem faz re-enrichment síncrono
Tecnologia: Protocolo ICY
O que é ICY Metadata?
ICY (também conhecido como Shoutcast metadata) é um protocolo usado por servidores de streaming (Shoutcast, Icecast) para enviar informações sobre o que está a tocar em tempo real junto com o stream de áudio.
Como Funciona
- Cliente solicita metadata: Envia header
Icy-MetaData: 1 - Servidor responde: Inclui headers ICY (
icy-name,icy-genre, etc.) eicy-metaint - Stream de dados: Servidor envia blocos de áudio de tamanho
icy-metaint - Metadata inline: Após cada bloco de áudio:
- 1 byte indicando tamanho do metadata (multiplicado por 16)
- N bytes de metadata no formato
StreamTitle='Artista - Música';
Headers ICY Suportados
icy-name: Nome da estaçãoicy-genre: Género musicalicy-br: Bitrate em kbpsicy-description: Descrição da estaçãoicy-url: URL da estaçãoicy-metaint: Intervalo de metadata em bytes
Parsing do StreamTitle
A API suporta múltiplos formatos de separação entre artista e música:
Artist - TrackArtist – Track(en dash)Artist — Track(em dash)Artist: Track
Se nenhum separador for encontrado, todo o texto é considerado como título da música.
Códigos de Resposta
| Código | Descrição |
|---|---|
| 200 | Sucesso - Informação retornada (pode ser "Sem informação") |
| 404 | Rádio não encontrada |
Limitações
Streams sem Suporte ICY
Alguns streams não suportam metadata ICY:
- Streams HLS (.m3u8)
- Alguns streams HTTPS antigos
- Streams com configuração incorreta
Nestes casos, a API retorna currentTrack: "Sem informação".
Streams com Metadata Estático
Algumas rádios enviam sempre o mesmo StreamTitle (ex: nome da estação) em vez de atualizar com a música atual. Isto é uma limitação do servidor de streaming, não da API.
Formato Inconsistente
O formato do StreamTitle varia entre rádios. A API tenta extrair artista e música automaticamente, mas pode falhar se o formato for não-standard.
Integração
Frontend
async function getNowPlaying(radioId) {
try {
const response = await fetch(`/api/radios/${radioId}/now-playing`);
const data = await response.json();
if (data.isFresh && data.currentTrack && data.currentTrack !== 'Sem informação') {
return {
artist: data.artist || 'Desconhecido',
track: data.track || data.currentTrack,
station: data.station,
};
}
return null; // Sem informação disponível
} catch (error) {
console.error('Erro ao obter now playing:', error);
return null;
}
}
// Atualizar a cada 30 segundos (SSE é preferível a polling — ver SSE_NOW_PLAYING.md)
setInterval(() => {
const radioId = getCurrentRadioId();
getNowPlaying(radioId).then(updateUI);
}, 30000);
Polling Recomendado
- Mínimo: 30 segundos (para respeitar o cache)
- Recomendado: 60 segundos (1 minuto)
- Evitar: Polling < 10 segundos (desnecessário dado o cache)
Troubleshooting
currentTrack sempre "Sem informação"
Possíveis causas:
- Worker (ICY collector) não está a conseguir ligar-se ao stream da rádio
- Stream não suporta metadata ICY
- URL do stream está incorreta ou inativa
- Worker não está a correr / está parado
Verificação:
- Verificar logs do Worker/ICY collector
- Verificar se o stream responde manualmente:
curl -H "Icy-MetaData: 1" -v "http://stream-url.com/radio"
Procure por headers icy-* e icy-metaint na resposta.
Timeout frequente
Possíveis causas:
- Stream muito lento ou instável
- Servidor de streaming sobrecarregado
- Problemas de rede
Solução:
O sistema marca automaticamente rádios com falhas consecutivas como inativas após 3 tentativas falhadas.
Artista/Música não separados
Causa:
O StreamTitle não usa um formato padrão de separação.
Exemplo:
StreamTitle='Música Clássica'(sem separador)StreamTitle='COMERCIAL FM - TOP 40'(formato não-standard)
Nestes casos, todo o conteúdo fica em currentTrack e os campos artist/track ficam vazios.
Endpoint: Now Playing Program (EPG)
GET /radios/:id/now-playing-program
Descrição
Retorna informação sobre o programa de rádio que está atualmente no ar, baseado no EPG (Electronic Program Guide) da rádio. Calcula automaticamente qual programa está a ser transmitido com base no dia da semana e hora atual.
Parâmetros
Path Parameters
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id |
string | Sim | ID MongoDB da rádio (24 caracteres hexadecimais) |
Resposta
Formato
{
"title": "string",
"subtitle": "string",
"description": "string",
"time": "string (HH:mm)",
"end_time": "string (HH:mm)",
"duration": number,
"radio_name": "string",
"radio_id": "string"
}
Ou null se não existir EPG ou nenhum programa agendado para a hora atual.
Campos
- title: Título do programa (ex: "Café da Manhã")
- subtitle: Subtítulo ou apresentadores (ex: "Com João Silva") - opcional
- description: Descrição adicional do programa - opcional
- time: Horário de início no formato 24h (ex: "07:00")
- end_time: Horário de fim no formato 24h (ex: "10:00") - opcional
- duration: Duração do programa em minutos - opcional
- radio_name: Nome da rádio
- radio_id: ID da rádio
Exemplos de Uso
Pedido Básico
curl -X GET "http://localhost:3000/api/radios/694830c00e1febe2e0cbfe38/now-playing-program"
Resposta com Programa no Ar
{
"title": "Café da Manhã",
"subtitle": "Com João Silva",
"description": "O melhor programa matinal com notícias e entretenimento",
"time": "07:00",
"end_time": "10:00",
"duration": 180,
"radio_name": "Antena 1",
"radio_id": "694830c00e1febe2e0cbfe38"
}
Resposta Sem EPG
null
Comportamento
Cálculo do Programa Atual
O sistema:
- Obtém o dia da semana atual (0=Segunda, ..., 6=Domingo)
- Obtém a hora e minutos atuais no formato HH:mm
- Busca o EPG da rádio no banco de dados
- Procura no array de programas do dia atual
- Compara a hora atual com os horários de início e fim de cada programa
- Retorna o programa que está no ar neste momento
Sem Hora de Fim
Se um programa não tem end_time definido, o sistema assume que vai até o início do próximo programa ou até ao fim do dia.
Retorna `null` quando
- A rádio não tem EPG configurado
- O EPG expirou
- Não há nenhum programa agendado para a hora atual
- Rádio não foi encontrada (404)
Códigos de Resposta
| Código | Descrição |
|---|---|
| 200 | Sucesso - Retorna programa ou null |
| 404 | Rádio não encontrada |
Casos de Uso
Use Case 1: Exibir "No ar agora"
async function displayCurrentProgram(radioId) {
const response = await fetch(`/api/radios/${radioId}/now-playing-program`);
const program = await response.json();
if (program) {
document.getElementById('program-title').textContent = program.title;
document.getElementById('program-time').textContent = `${program.time} - ${program.end_time || ''}`;
if (program.subtitle) {
document.getElementById('program-subtitle').textContent = program.subtitle;
}
} else {
document.getElementById('program-title').textContent = 'Sem programação';
}
}
Use Case 2: Combinar com Metadata ICY
async function getCompleteNowPlaying(radioId) {
const [program, metadata] = await Promise.all([fetch(`/api/radios/${radioId}/now-playing-program`).then((r) => r.json()), fetch(`/api/radios/${radioId}/now-playing`).then((r) => r.json())]);
return {
program: program ? program.title : 'Sem programação',
currentTrack: metadata.currentTrack || 'Sem informação',
artist: metadata.artist,
time: program ? `${program.time} - ${program.end_time || ''}` : null,
};
}
// UI:
// "Café da Manhã (07:00 - 10:00)"
// "A tocar: The Weeknd - Blinding Lights"
Use Case 3: Atualização Periódica
// Atualizar a cada minuto (programas mudam menos frequentemente que músicas)
setInterval(() => {
const radioId = getCurrentRadioId();
displayCurrentProgram(radioId);
}, 60000); // 1 minuto
Diferenças: Now Playing vs Now Playing Program
| Aspecto | Now Playing (ICY) | Now Playing Program (EPG) |
|---|---|---|
| Fonte de dados | Stream de áudio (ICY metadata) | Base de dados (EPG) |
| Informação | Música/faixa atual | Programa de rádio |
| Atualização | Tempo real (~cada música) | Por programa (30min-3h) |
| Disponibilidade | Depende do stream | Depende do EPG |
| Cache | 5 minutos | Sem cache (cálculo instantâneo) |
| Use case | "A tocar: Artista - Música" | "No ar: Nome do Programa" |
Polling Recomendado
- Recomendado: 60 segundos (1 minuto)
- Máximo: 30 segundos
- Mínimo: Não necessário (programas mudam devagar)
Fontes de EPG
O EPG pode vir de várias fontes (ordem de prioridade):
- User-Submitted - Rádio submete a sua própria programação via API
- MyTuner - Obtido automaticamente da API MyTuner
- ICY Patterns - Gerado automaticamente com base em padrões históricos (Fase 4)
- Scraping - Sites oficiais das rádios (Fase 2)
Ver Radio Schedules & EPG para mais detalhes.
Endpoint: Today's Programs
GET /radios/:id/programs/today
Descrição
Retorna a lista completa de programas do dia de hoje para uma rádio específica, incluindo o programa atual (se estiver no ar) e todos os programas futuros. Permite ao utilizador planear o que vai ouvir ao longo do dia.
Parâmetros
Path Parameters
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id |
string | Sim | ID MongoDB da rádio (24 caracteres hexadecimais) |
Resposta
Formato
Array de objetos com a seguinte estrutura:
[
{
"title": "string",
"subtitle": "string",
"description": "string",
"time": "string (HH:mm)",
"end_time": "string (HH:mm)",
"duration": number
}
]
Ou array vazio [] se não existir EPG ou não houver programação para hoje.
Campos
- title: Título do programa (ex: "Café da Manhã")
- subtitle: Subtítulo ou apresentadores (ex: "Com João Silva") - opcional
- description: Descrição adicional do programa - opcional
- time: Horário de início no formato 24h (ex: "07:00")
- end_time: Horário de fim no formato 24h (ex: "10:00") - opcional
- duration: Duração do programa em minutos - opcional
Exemplos de Uso
Pedido Básico
curl -X GET "http://localhost:3000/api/radios/6948306f0e1febe2e0cbfb3a/programs/today"
Resposta com Programação Completa
[
{
"title": "Café da Manhã",
"subtitle": "Com João Silva",
"description": "O melhor programa matinal com notícias e entretenimento",
"time": "07:00",
"end_time": "10:00",
"duration": 180
},
{
"title": "Música ao Meio-Dia",
"time": "12:00",
"end_time": "13:00",
"duration": 60
},
{
"title": "Tarde em Grande",
"subtitle": "Com Ana Costa",
"description": "Entretenimento e boa disposição durante a tarde",
"time": "14:00",
"end_time": "17:00",
"duration": 180
},
{
"title": "Notícias da Noite",
"time": "20:00",
"end_time": "21:00",
"duration": 60
}
]
Resposta Sem EPG
[]
Comportamento
Filtragem de Programas
O sistema:
- Obtém o dia da semana atual (0=Segunda, ..., 6=Domingo)
- Busca todos os programas agendados para o dia atual
- Remove programas que já terminaram (baseado no
end_time) - Mantém o programa atual (se estiver no ar)
- Mantém todos os programas futuros
- Ordena por horário de início
Programa Atual Incluído
Se às 15:00 consultar a programação, e houver um programa das 14:00-17:00, este será incluído na lista (primeiro da lista).
Ordenação
Programas sempre retornam ordenados por time (horário de início), do mais cedo para o mais tarde.
Códigos de Resposta
| Código | Descrição |
|---|---|
| 200 | Sucesso - Retorna array de programas ou array vazio |
| 404 | Rádio não encontrada |
Casos de Uso
Use Case 1: Exibir Grade de Programação
async function displayTodaySchedule(radioId) {
const response = await fetch(`/api/radios/${radioId}/programs/today`);
const programs = await response.json();
if (programs.length === 0) {
document.getElementById('schedule').innerHTML = '<p>Programação não disponível</p>';
return;
}
const scheduleHTML = programs
.map(
(program) => `
<div class="program-item">
<div class="program-time">${program.time} - ${program.end_time || '—'}</div>
<div class="program-title">${program.title}</div>
${program.subtitle ? `<div class="program-subtitle">${program.subtitle}</div>` : ''}
${program.description ? `<div class="program-description">${program.description}</div>` : ''}
</div>
`
)
.join('');
document.getElementById('schedule').innerHTML = scheduleHTML;
}
Use Case 2: Identificar Programa Atual e Próximo
async function getCurrentAndNextProgram(radioId) {
const programs = await fetch(`/api/radios/${radioId}/programs/today`).then((r) => r.json());
if (programs.length === 0) {
return { current: null, next: null };
}
const now = new Date();
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
// Encontrar programa atual
const current = programs.find((p) => {
if (p.end_time) {
return currentTime >= p.time && currentTime < p.end_time;
}
return currentTime >= p.time;
});
// Encontrar próximo programa
const next = programs.find((p) => p.time > currentTime);
return { current, next };
}
// UI:
// "No ar: Café da Manhã (07:00 - 10:00)"
// "A seguir: Música ao Meio-Dia (12:00 - 13:00)"
Use Case 3: Calcular Tempo até Próximo Programa
async function getTimeUntilNextProgram(radioId) {
const programs = await fetch(`/api/radios/${radioId}/programs/today`).then((r) => r.json());
const now = new Date();
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
const nextProgram = programs.find((p) => p.time > currentTime);
if (!nextProgram) {
return null;
}
const [hours, minutes] = nextProgram.time.split(':').map(Number);
const nextStart = new Date();
nextStart.setHours(hours, minutes, 0, 0);
const diffMs = nextStart - now;
const diffMinutes = Math.floor(diffMs / 60000);
return {
program: nextProgram.title,
time: nextProgram.time,
minutesUntil: diffMinutes,
};
}
// UI: "Próximo programa: Tarde em Grande começa em 45 minutos"
Use Case 4: Notificações de Programa Favorito
async function checkFavoriteProgram(radioId, favoriteTitle) {
const programs = await fetch(`/api/radios/${radioId}/programs/today`).then((r) => r.json());
const favoriteProgram = programs.find((p) => p.title === favoriteTitle);
if (favoriteProgram) {
const now = new Date();
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
// Se o programa está para começar em breve (próximos 15 minutos)
const [hours, minutes] = favoriteProgram.time.split(':').map(Number);
const programStart = new Date();
programStart.setHours(hours, minutes, 0, 0);
const diffMs = programStart - now;
const diffMinutes = Math.floor(diffMs / 60000);
if (diffMinutes > 0 && diffMinutes <= 15) {
showNotification(`${favoriteProgram.title} começa em ${diffMinutes} minutos!`, radioId);
}
}
}
Polling Recomendado
- Recomendado: 5 minutos
- Contexto de grade de programação: Carregar uma vez ao abrir
- Com atualização de "no ar agora": Combinar com endpoint
now-playing-program(1 minuto)
Diferenças: Today Programs vs Now Playing Program
| Aspecto | Today Programs | Now Playing Program |
|---|---|---|
| Endpoint | /programs/today |
/now-playing-program |
| Retorna | Array de programas | Um programa ou null |
| Âmbito | Dia completo | Apenas programa atual |
| Filtragem | Exclui programas passados | Calcula programa no ar |
| Use case | Grade de programação | "No ar agora" |
| Atualização UI | Carregar uma vez | Polling 1 minuto |
Complementaridade com Outros Endpoints
Combinação Completa de Informação
async function getCompleteRadioInfo(radioId) {
const [radio, currentProgram, todayPrograms, nowPlaying] = await Promise.all([fetch(`/api/radios/${radioId}`).then((r) => r.json()), fetch(`/api/radios/${radioId}/now-playing-program`).then((r) => r.json()), fetch(`/api/radios/${radioId}/programs/today`).then((r) => r.json()), fetch(`/api/radios/${radioId}/now-playing`).then((r) => r.json())]);
return {
radio: radio.name,
currentProgram: currentProgram?.title || 'Sem programação',
currentTrack: nowPlaying.currentTrack || 'Sem informação',
todaySchedule: todayPrograms,
};
}
// Renderização completa:
// Rádio: "Antena 1"
// No ar: "Café da Manhã (07:00 - 10:00)"
// A tocar: "The Weeknd - Blinding Lights"
// Programação do dia: [lista de 8 programas]