🎶

Implementação ICY Metadata

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:

🏗️ 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

  1. Conectar a streams de rádio via HTTP/HTTPS
  2. Enviar header Icy-MetaData: 1 para solicitar metadata
  3. Extrair headers ICY do servidor
  4. Implementar máquina de estados para parsing do protocolo
  5. Extrair StreamTitle do metadata inline
  6. 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

Estado 2: Lendo Byte de Tamanho

Estado 3: Lendo Metadata

4. Formato do Metadata

Exemplo de metadata recebido:

StreamTitle='The Weeknd - Blinding Lights';StreamUrl='';

O parser extrai:

5. Parsing de Artista/Música

O serviço tenta vários separadores:

  1. - (espaço-hífen-espaço)
  2. – (espaço-en dash-espaço)
  3. — (espaço-em dash-espaço)
  4. : (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:

Quando usar cache:

Timeout e Redirects

Timeout de Conexão:

Máximo de Redirects:

Quando Parar de Ler

O parser para assim que obtém o primeiro metadata:

🔧 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

  1. Stream não responde

    • Timeout de 10 segundos
    • Retorna erro 500
  2. Stream sem metadata ICY

    • icy-metaint ausente
    • Retorna headers apenas (station, genre, bitrate)
    • currentTrack: "Sem informação"
  3. Metadata vazio

    • Byte de tamanho = 0
    • Continua lendo próximo ciclo (mas limitado por timeout)
  4. URL inválida

    • Validação antes de conectar
    • Erro: Invalid URL: ...
  5. 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:

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

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

Para enriquecer metadata com:

📚 Referências

Protocolo ICY/Shoutcast

Standards Relacionados

Ver Também