Implementação de Metadata ICY/Shoutcast
📋 Visão Geral
Sistema completo para extrair metadata em tempo real de streams de rádio usando o protocolo ICY/Shoutcast. Permite obter informação sobre o que está a tocar (artist, track) diretamente dos streams de áudio.
✅ Status: Implementado (Worker-only desde Abril 2026)
Sistema funcional com:
- ✅ Extração de metadata ICY em tempo real (Worker/ICY collector apenas)
- ✅ Parsing automático de artista e música
- ✅ Cache de 15 minutos no API (read-only, sem fetch a streams)
- ✅ Suporte a redirects (até 3)
- ✅ Timeout de 10 segundos
- ✅ Máquina de estados robusta para parsing do protocolo
- ✅ Fallback stale cache (sem limite de idade) ou "Sem informação"
- ✅ Tratamento de erros e streams sem metadata
- ✅ Adapters removidos da API (vivem em
@audissey/radio-collector, usados pelo Worker)
🏗️ Arquitetura
Serviço: IcyMetadataService
Localização: radio-collector/src/services/icy-metadata.service.ts (shared lib @audissey/radio-collector)
Nota: O serviço foi removido da API (
apps/api/) em Abril 2026. Apenas o Worker o utiliza via a lib partilhada.
Responsabilidades
- Conectar a streams de rádio via HTTP/HTTPS
- Enviar header
Icy-MetaData: 1para solicitar metadata - Extrair headers ICY do servidor
- Implementar máquina de estados para parsing do protocolo
- Extrair StreamTitle do metadata inline
- Parsear artista e música do StreamTitle
Interface: IcyMetadata
export interface IcyMetadata {
station?: string; // Nome da estação (icy-name)
title?: string; // Título (StreamTitle ou icy-name)
genre?: string; // Género (icy-genre)
bitrate?: number; // Bitrate em kbps (icy-br)
description?: string; // Descrição (icy-description)
url?: string; // URL da estação (icy-url)
currentTrack?: string; // StreamTitle completo
timestamp: Date; // Timestamp da extração
}
Protocolo ICY: Funcionamento Detalhado
1. Handshake
Cliente → Servidor:
GET /stream HTTP/1.1
Icy-MetaData: 1
User-Agent: Audissey/1.0
Servidor → Cliente:
HTTP/1.0 200 OK
icy-name: Rádio Exemplo
icy-genre: Rock
icy-br: 128
icy-metaint: 16000
O header icy-metaint: 16000 indica que a cada 16000 bytes de áudio, há metadata inline.
2. Stream de Dados
O stream é composto por ciclos:
[Áudio de 16000 bytes] [1 byte de tamanho] [N bytes de metadata]
Repete indefinidamente...
3. Máquina de Estados
O parser implementa 3 estados:
Estado 1: Lendo Áudio
- Lê até
metadataIntervalbytes - Descarta dados de áudio (não precisamos decodificar)
- Avança para Estado 2
Estado 2: Lendo Byte de Tamanho
- Lê 1 byte
- Calcula tamanho real:
byte * 16 - Se tamanho = 0: volta para Estado 1 (sem metadata neste ciclo)
- Se tamanho > 0: avança para Estado 3
Estado 3: Lendo Metadata
- Lê
tamanhobytes - Decodifica UTF-8
- Extrai
StreamTitle='...' - Termina (já temos o que precisamos)
4. Formato do Metadata
Exemplo de metadata recebido:
StreamTitle='The Weeknd - Blinding Lights';StreamUrl='';
O parser extrai:
StreamTitle='The Weeknd - Blinding Lights'- Remove aspas e ponto-e-vírgula
- Resultado:
"The Weeknd - Blinding Lights"
5. Parsing de Artista/Música
O serviço tenta vários separadores:
-(espaço-hífen-espaço)–(espaço-en dash-espaço)—(espaço-em dash-espaço):(dois pontos-espaço)
Exemplo:
Input: "The Weeknd - Blinding Lights"
Output: {
artist: "The Weeknd",
track: "Blinding Lights"
}
Integração com RadiosService
O RadiosService.getNowPlaying() é puramente read-only (não faz fetch a streams):
async getNowPlaying(id: string): Promise<NowPlayingDto> {
const radio = await this.radioModel.findById(id).lean().exec();
// Layer 1: In-memory cache (30s TTL)
// Layer 2: Redis (NowPlayingCacheService, 120s TTL)
// Layer 3: MongoDB (radios.now_playing)
// Se cache fresh (< 15 min), devolve imediatamente
if (cachedDto && cacheAge < 15 * 60 * 1000) {
return cachedDto;
}
// Se stale (> 15 min), devolve stale sem limite de idade
if (cachedDto) {
return cachedDto;
}
// Sem dados: devolve fallback estático
return { currentTrack: 'Sem informação', ... };
}
A atualização de now_playing é responsabilidade exclusiva do Worker (ICY collector), que corre em background e persiste no Redis + MongoDB.
🎯 Campos na Base de Dados
Schema: Radio
Campos adicionados para metadata "now playing":
@Schema({ timestamps: true })
export class Radio {
// ... campos existentes ...
// Now Playing / ICY Metadata
@Prop()
now_playing?: string; // StreamTitle completo
@Prop()
now_playing_artist?: string; // Artista extraído
@Prop()
now_playing_track?: string; // Música extraída
@Prop()
now_playing_updated_at?: Date; // Última atualização
}
🚀 Fluxo de Dados
Nota (Abril 2026): A API já não faz fetch a streams ao vivo. O fluxo abaixo aplica-se ao Worker (ICY collector). A API
RadiosService.getNowPlaying()é puramente read-only (3 camadas de cache: memory → Redis → MongoDB).
1. Worker ICY collector corre em ciclos periódicos
↓
2. IcyMetadataService.getMetadata() (via radio-collector lib)
↓
3. Conecta ao stream com Icy-MetaData: 1
↓
4. Extrai headers ICY (station, genre, bitrate)
↓
5. Se icy-metaint presente → Parser de metadata
↓
6. Máquina de estados lê stream:
Estado 1: Lê áudio (metadataInterval bytes)
Estado 2: Lê byte de tamanho
Estado 3: Lê metadata (tamanho * 16 bytes)
↓
7. Extrai StreamTitle='...'
↓
8. Parseia artista e música (separadores: -, –, —, :)
↓
9. Persiste em Redis + MongoDB
↓
10. Emite evento SSE se a faixa mudou
📊 Performance e Otimizações
Sistema de Cache
Cache em Base de Dados:
- Duração: 5 minutos
- Campo:
now_playing_updated_at - Vantagem: Reduz carga nos servidores de streaming
Quando usar cache:
- Múltiplos clientes consultam a mesma rádio
- Metadata não muda com muita frequência
- Reduz latência (resposta instantânea)
Timeout e Redirects
Timeout de Conexão:
- 10 segundos
- Evita esperas indefinidas em streams lentos
Máximo de Redirects:
- 3 redirects
- Evita loops infinitos
- Comum em streams que redirecionam para balanceamento de carga
Quando Parar de Ler
O parser para assim que obtém o primeiro metadata:
- Não precisa continuar lendo o stream
- Destrói a conexão com
res.destroy() - Economiza largura de banda
🔧 Configurações
Constantes do Serviço
private readonly REQUEST_TIMEOUT = 10000; // 10 segundos
private readonly MAX_REDIRECTS = 3;
Cache no RadiosService
const cacheMaxAge = 5 * 60 * 1000; // 5 minutos
🐛 Tratamento de Erros
Erros Comuns
Stream não responde
- Timeout de 10 segundos
- Retorna erro 500
Stream sem metadata ICY
icy-metaintausente- Retorna headers apenas (station, genre, bitrate)
currentTrack: "Sem informação"
Metadata vazio
- Byte de tamanho = 0
- Continua lendo próximo ciclo (mas limitado por timeout)
URL inválida
- Validação antes de conectar
- Erro:
Invalid URL: ...
Muitos redirects
- Após 3 redirects
- Erro:
Too many redirects
Fallback para Cache
Quando a extração falha, o serviço retorna dados em cache se disponíveis:
catch (error) {
if (radio.now_playing && radio.now_playing_updated_at) {
// Retornar cache mesmo que tenha mais de 5 minutos
return cachedData;
}
throw error;
}
🧪 Testes
Testar Manualmente com cURL
Verificar se stream suporta ICY:
curl -H "Icy-MetaData: 1" -v "http://stream-url.com:8000/stream" 2>&1 | grep -i "icy"
Procurar por:
icy-name:icy-genre:icy-br:icy-metaint:← Mais importante
Se icy-metaint estiver presente, o stream suporta metadata inline.
Testar via API
curl http://localhost:3000/api/radios/694830c00e1febe2e0cbfe38/now-playing
Exemplo de Stream com ICY
# Rádio Paradise (conhecido por ter bom metadata)
curl -H "Icy-MetaData: 1" -v "http://stream.radioparadise.com/rp_192m.ogg"
📝 Limitações Conhecidas
1. Streams HLS (.m3u8)
Não suportado - HLS usa um protocolo diferente (playlists de chunks)
Solução alternativa: Alguns servidores HLS expõem metadata via JSON ou outros endpoints.
2. Streams HTTPS Antigos
Alguns streams HTTPS mais antigos não implementam corretamente o protocolo ICY.
3. Metadata Estático
Algumas rádios enviam sempre o mesmo StreamTitle (ex: nome da estação) em vez de atualizar.
Exemplo:
StreamTitle='Rádio Comercial - Top 40'
Isto é uma limitação do servidor, não da API.
4. Formato Não-Standard
Alguns streams usam formatos custom:
StreamTitle='NOW: Artist | Track | Album'
StreamTitle='[LIVE] DJ Name - Set @ Festival'
O parser pode não conseguir separar artista e música corretamente.
🔮 Melhorias Futuras
1. Suporte a Mais Formatos
- HLS metadata (via playlists)
- DASH (similar ao HLS)
- Outros protocolos de streaming
2. Machine Learning para Parsing
Treinar modelo para extrair artista/música de formatos não-standard.
3. Histórico de Músicas
Armazenar histórico do que tocou em cada rádio.
4. Websockets
Notificar clientes em tempo real quando música muda (evita polling).
5. Integração com APIs Musicais
- Spotify API
- Last.fm API
- MusicBrainz
Para enriquecer metadata com:
- Capas de álbum
- Informação do artista
- Links para streaming