🔌 Audissey - API & Data Overview
📋 Visão Geral da Arquitetura
A plataforma Audissey é composta por dois serviços principais que trabalham em conjunto:
┌─────────────────────────────────────────────────────────────┐
│ Mobile/Web App │
│ (React Native / Web) │
└────────────────────────┬────────────────────────────────────┘
│
HTTP/REST
│
┌────────────────────────▼────────────────────────────────────┐
│ API Service │
│ (NestJS/Node.js) │
│ - REST Endpoints │
│ - Authentication │
│ - Real-time data │
│ - EPG (Electronic Program Guide) │
└────────────────────────┬────────────────────────────────────┘
│
MongoDB + Redis
│
┌────────────────────────▼────────────────────────────────────┐
│ Worker Service │
│ (NestJS/Node.js) │
│ - Background Jobs (BullMQ) │
│ - Data Sync (Podcasts, Radios) │
│ - Metadata Collection │
│ - ICY Stream Processing │
└──────────────────────────────────────────────────────────────┘
Fluxo de Dados
- App → API: User interage com a app, faz requests
- API → Database: Lê/escreve dados em MongoDB
- API → Worker: Envia jobs para processamento assíncrono
- Worker → External APIs: Busca dados de fontes externas
- Worker → Database: Atualiza dados processados
- API → App: Retorna dados atualizados
🌐 API Service (Port 3000)
Base URL
- Development:
http://localhost:3000/api - Staging:
https://audissey-api-staging.azurewebsites.net/api - Production:
https://audissey-api.azurewebsites.net/api
Documentação Interativa
- Swagger UI:
{BASE_URL}/api-docs - Docs Portal:
{BASE_URL}/docs
📻 Rádios - Endpoints
1. Listar Rádios
GET /api/radios
Query Parameters:
{
search?: string; // Pesquisa por nome
country?: string; // Código país (ex: "PT")
city?: string; // Cidade
genre?: string; // Género musical
language?: string; // Idioma
state?: string; // Estado/Região
tags?: string[]; // Tags (sports, news, music)
page?: number; // Paginação (default: 1)
limit?: number; // Items por página (default: 20)
}
Response (200 OK):
{
"data": [
{
"_id": "507f1f77bcf86cd799439011",
"radioId": "rtp-antena1",
"name": "RTP Antena 1",
"description": "Rádio generalista portuguesa",
"url": "https://streaming.example.com/antena1",
"urlResolved": "https://direct-stream.example.com/antena1.mp3",
"homepage": "https://www.rtp.pt/antena1",
"favicon": "https://cdn.example.com/logos/antena1.png",
"country": "PT",
"countryCode": "PT",
"state": "Lisboa",
"city": "Lisboa",
"language": "portuguese",
"genres": ["news", "talk", "culture"],
"tags": ["public", "national", "talk"],
"votes": 15234,
"codec": "MP3",
"bitrate": 128,
"hasExtendedInfo": true,
"lastCheckOk": true,
"lastCheckTime": "2025-12-26T10:30:00Z",
"clickCount": 45230,
"clickTrend": 1250,
"sslError": false,
"geoLat": 38.7223,
"geoLong": -9.1393,
"hasScheduleInfo": true,
"currentProgram": {
"title": "Jornal da Manhã",
"startTime": "2025-12-26T08:00:00Z",
"endTime": "2025-12-26T10:00:00Z",
"description": "Informação nacional e internacional"
},
"nowPlaying": {
"title": "Entrevista com João Silva",
"artist": "",
"timestamp": "2025-12-26T09:45:00Z"
}
}
],
"pagination": {
"currentPage": 1,
"totalPages": 45,
"totalItems": 892,
"itemsPerPage": 20
}
}
2. Detalhes de Rádio
GET /api/radios/:id
Response (200 OK):
{
"_id": "507f1f77bcf86cd799439011",
"radioId": "rtp-antena1",
"name": "RTP Antena 1",
"description": "Rádio generalista portuguesa com programação variada",
"url": "https://streaming.example.com/antena1",
"urlResolved": "https://direct-stream.example.com/antena1.mp3",
"alternativeUrls": [
"https://backup1.example.com/antena1.mp3",
"https://backup2.example.com/antena1.aac"
],
"homepage": "https://www.rtp.pt/antena1",
"favicon": "https://cdn.example.com/logos/antena1.png",
"country": "PT",
"countryCode": "PT",
"state": "Lisboa",
"city": "Lisboa",
"language": "portuguese",
"genres": ["news", "talk", "culture"],
"tags": ["public", "national", "talk", "portuguese"],
"votes": 15234,
"codec": "MP3",
"bitrate": 128,
"sampling": 44100,
"hasExtendedInfo": true,
"lastCheckOk": true,
"lastCheckTime": "2025-12-26T10:30:00Z",
"clickCount": 45230,
"clickTrend": 1250,
"sslError": false,
"geoLat": 38.7223,
"geoLong": -9.1393,
"hasScheduleInfo": true,
"schedule": {
"programs": [...],
"lastUpdated": "2025-12-26T00:00:00Z"
},
"statistics": {
"totalListens": 125430,
"uniqueListeners": 45230,
"averageListenDuration": 3600,
"peakListeners": 8500
}
}
3. Pesquisa Unificada
GET /api/search/unified
Query Parameters:
{
q: string; // Query de pesquisa (obrigatório)
type?: 'all' | 'radios' | 'podcasts' | 'episodes' | 'programs';
limit?: number; // Default: 20
}
Response (200 OK):
{
"query": "notícias",
"results": {
"radios": [
{
"_id": "...",
"name": "TSF Rádio Notícias",
"type": "radio",
"description": "Rádio de notícias 24h",
"artwork": "https://...",
"relevanceScore": 0.95
}
],
"podcasts": [
{
"_id": "...",
"title": "Notícias do Dia",
"type": "podcast",
"description": "Resumo diário de notícias",
"artwork": "https://...",
"episodeCount": 450,
"relevanceScore": 0.88
}
],
"episodes": [
{
"_id": "...",
"title": "Notícias 26 Dezembro",
"type": "episode",
"podcastTitle": "Notícias do Dia",
"description": "...",
"duration": 1800,
"pubDate": "2025-12-26T06:00:00Z",
"relevanceScore": 0.92
}
],
"programs": [
{
"_id": "...",
"title": "Jornal da Tarde",
"type": "program",
"radioName": "RTP Antena 1",
"description": "...",
"schedule": {...},
"relevanceScore": 0.75
}
]
},
"totalResults": 145
}
4. Now Playing (Metadados ICY)
GET /api/radios/:id/now-playing
Response (200 OK):
{
"radioId": "rtp-antena1",
"nowPlaying": {
"title": "Entrevista com João Silva",
"artist": "RTP",
"album": "Jornal da Manhã",
"timestamp": "2025-12-26T09:45:00Z",
"genre": "Talk",
"bitrate": 128,
"codec": "MP3"
},
"currentProgram": {
"title": "Jornal da Manhã",
"startTime": "2025-12-26T08:00:00Z",
"endTime": "2025-12-26T10:00:00Z",
"description": "Informação nacional e internacional",
"hosts": ["Maria Santos", "Pedro Costa"]
},
"nextProgram": {
"title": "Música e Companhia",
"startTime": "2025-12-26T10:00:00Z",
"endTime": "2025-12-26T12:00:00Z"
}
}
5. EPG (Guia de Programação 7 dias)
GET /api/radios/:id/epg
Query Parameters:
{
date?: string; // YYYY-MM-DD (default: hoje)
days?: number; // Número de dias (default: 7, max: 7)
}
Response (200 OK):
{
"radioId": "rtp-antena1",
"radioName": "RTP Antena 1",
"schedule": [
{
"date": "2025-12-26",
"programs": [
{
"title": "Madrugada",
"startTime": "00:00",
"endTime": "06:00",
"duration": 360,
"description": "Música ambiente e notícias",
"hosts": [],
"genre": "music",
"isLive": false
},
{
"title": "Jornal da Manhã",
"startTime": "06:00",
"endTime": "10:00",
"duration": 240,
"description": "Informação e atualidade",
"hosts": ["Maria Santos", "Pedro Costa"],
"genre": "news",
"isLive": true,
"hasReminder": true
},
{
"title": "Música e Companhia",
"startTime": "10:00",
"endTime": "14:00",
"duration": 240,
"description": "Música variada",
"hosts": ["Ana Silva"],
"genre": "music",
"isLive": false
}
]
},
{
"date": "2025-12-27",
"programs": [...]
}
],
"timezone": "Europe/Lisbon"
}
🎙️ Podcasts - Endpoints
1. Listar Podcasts
GET /api/podcasts
Query Parameters:
{
search?: string; // Pesquisa por título
category?: string; // Categoria (ex: "Technology", "True Crime")
language?: string; // Idioma
country?: string; // País
featured?: boolean; // Podcasts em destaque
page?: number;
limit?: number;
}
Response (200 OK):
{
"data": [
{
"_id": "507f1f77bcf86cd799439012",
"podcastIndexId": 123456,
"title": "Tech Talks Portugal",
"description": "Conversas sobre tecnologia e inovação",
"author": "João Tech",
"email": "joao@techtalks.pt",
"link": "https://techtalks.pt",
"feedUrl": "https://techtalks.pt/feed.xml",
"artwork": "https://cdn.example.com/podcasts/techtalks.jpg",
"categories": ["Technology", "Business"],
"language": "pt",
"country": "PT",
"explicit": false,
"episodeCount": 125,
"latestEpisode": {
"title": "IA em 2025",
"pubDate": "2025-12-20T08:00:00Z"
},
"lastBuildDate": "2025-12-20T08:00:00Z",
"updateFrequency": "weekly",
"subscriptions": 15420,
"rating": 4.7,
"reviewCount": 342,
"featured": true,
"trendingScore": 0.89
}
],
"pagination": {
"currentPage": 1,
"totalPages": 23,
"totalItems": 456,
"itemsPerPage": 20
}
}
2. Detalhes de Podcast
GET /api/podcasts/:id
Response (200 OK):
{
"_id": "507f1f77bcf86cd799439012",
"podcastIndexId": 123456,
"title": "Tech Talks Portugal",
"description": "Conversas sobre tecnologia e inovação em Portugal",
"longDescription": "...",
"author": "João Tech",
"email": "joao@techtalks.pt",
"link": "https://techtalks.pt",
"feedUrl": "https://techtalks.pt/feed.xml",
"artwork": "https://cdn.example.com/podcasts/techtalks.jpg",
"categories": ["Technology", "Business", "Innovation"],
"language": "pt",
"country": "PT",
"explicit": false,
"episodeCount": 125,
"totalDuration": 562800,
"averageEpisodeDuration": 4502,
"lastBuildDate": "2025-12-20T08:00:00Z",
"firstEpisodeDate": "2023-01-15T10:00:00Z",
"updateFrequency": "weekly",
"subscriptions": 15420,
"rating": 4.7,
"reviewCount": 342,
"featured": true,
"trendingScore": 0.89,
"statistics": {
"totalPlays": 345230,
"uniqueListeners": 15420,
"averageCompletion": 0.78,
"topCountries": ["PT", "BR", "US"]
},
"recentEpisodes": [
{
"_id": "...",
"title": "IA em 2025",
"pubDate": "2025-12-20T08:00:00Z",
"duration": 3600
}
]
}
3. Episódios de Podcast
GET /api/podcasts/:id/episodes
Query Parameters:
{
page?: number;
limit?: number;
sortBy?: 'date' | 'duration' | 'plays';
order?: 'asc' | 'desc';
}
Response (200 OK):
{
"podcastId": "507f1f77bcf86cd799439012",
"podcastTitle": "Tech Talks Portugal",
"episodes": [
{
"_id": "507f1f77bcf86cd799439013",
"title": "IA em 2025: Oportunidades e Desafios",
"description": "Discussão sobre inteligência artificial",
"longDescription": "...",
"pubDate": "2025-12-20T08:00:00Z",
"duration": 3600,
"durationFormatted": "1h 00m",
"enclosureUrl": "https://cdn.example.com/episodes/ep125.mp3",
"enclosureType": "audio/mpeg",
"enclosureLength": 57600000,
"explicit": false,
"episodeNumber": 125,
"seasonNumber": 3,
"episodeType": "full",
"artwork": "https://cdn.example.com/episodes/ep125.jpg",
"chapters": [
{
"startTime": 0,
"title": "Introdução",
"url": ""
},
{
"startTime": 300,
"title": "História da IA",
"url": ""
}
],
"transcript": "...",
"plays": 5430,
"likes": 234,
"comments": 45,
"downloaded": true,
"progress": 0.35
}
],
"pagination": {
"currentPage": 1,
"totalPages": 7,
"totalItems": 125,
"itemsPerPage": 20
}
}
4. Categorias de Podcasts
GET /api/podcasts/categories
Response (200 OK):
{
"categories": [
{
"name": "Technology",
"slug": "technology",
"podcastCount": 145,
"icon": "💻",
"description": "Podcasts sobre tecnologia e inovação"
},
{
"name": "True Crime",
"slug": "true-crime",
"podcastCount": 89,
"icon": "🔍",
"description": "Crimes reais e investigações"
},
{
"name": "Business",
"slug": "business",
"podcastCount": 123,
"icon": "💼",
"description": "Negócios e empreendedorismo"
}
]
}
🏠 Home/Highlights - Endpoint
GET /api/home/highlights
Query Parameters:
{
location?: string; // Localização do user (ex: "Lisboa, PT")
timeOfDay?: 'morning' | 'afternoon' | 'evening' | 'night';
userId?: string; // Para personalização
}
Response (200 OK):
{
"hero": {
"type": "featured-podcast",
"title": "Em Destaque",
"content": {
"_id": "...",
"title": "Tech Talks Portugal",
"description": "...",
"artwork": "https://...",
"cta": "Ouvir Agora"
}
},
"sections": [
{
"id": "quick-access",
"title": "Acesso Rápido",
"type": "favorites",
"items": [
{
"type": "radio",
"id": "...",
"name": "RFM",
"artwork": "...",
"isLive": true
}
],
"limit": 6
},
{
"id": "local-radios",
"title": "Rádios Locais",
"subtitle": "Em Lisboa",
"type": "radios",
"items": [...],
"viewAllUrl": "/radios?city=Lisboa"
},
{
"id": "trending-podcasts",
"title": "Podcasts em Alta",
"type": "podcasts",
"items": [...],
"viewAllUrl": "/podcasts?sort=trending"
},
{
"id": "recommended",
"title": "Recomendado para Ti",
"subtitle": "Baseado no teu gosto",
"type": "mixed",
"items": [
{
"type": "podcast",
"id": "...",
"title": "...",
"reason": "Porque ouviste Tech Talks"
}
]
},
{
"id": "live-now",
"title": "Ao Vivo Agora",
"type": "programs",
"items": [
{
"type": "program",
"radioId": "...",
"radioName": "TSF",
"programTitle": "Jornal da Tarde",
"startTime": "14:00",
"endTime": "16:00",
"isLive": true
}
]
},
{
"id": "new-episodes",
"title": "Novos Episódios",
"subtitle": "Das tuas subscrições",
"type": "episodes",
"items": [...]
}
],
"metadata": {
"generatedAt": "2025-12-26T10:00:00Z",
"location": "Lisboa, PT",
"timeOfDay": "morning",
"personalized": true
}
}
👤 Autenticação
1. Registo
POST /api/auth/register
Body:
{
"email": "user@example.com",
"password": "SecurePass123!",
"name": "João Silva",
"termsAccepted": true
}
Response (201 Created):
{
"user": {
"id": "507f1f77bcf86cd799439014",
"email": "user@example.com",
"name": "João Silva",
"createdAt": "2025-12-26T10:00:00Z"
},
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600
}
}
2. Login
POST /api/auth/login
Body:
{
"email": "user@example.com",
"password": "SecurePass123!"
}
Response (200 OK):
{
"user": {
"id": "507f1f77bcf86cd799439014",
"email": "user@example.com",
"name": "João Silva",
"avatar": "https://...",
"preferences": {
"language": "pt",
"theme": "dark",
"notifications": true
}
},
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600
}
}
3. Refresh Token
POST /api/auth/refresh
Body:
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
Headers de Autenticação
Todos os endpoints protegidos requerem:
Authorization: Bearer {accessToken}
⭐ Favorites - Endpoints
Documentação detalhada: docs/api/FAVORITES_API.md
1. Listar favoritos
GET /api/favorites?type=radio
Query Parameters:
{
type?: 'radio' | 'podcast' | 'episode';
}
2. Adicionar favorito
POST /api/favorites
Body:
{
"type": "radio",
"targetId": "65f1c2a0d4b6a3b0c1234567"
}
3. Remover favorito
DELETE /api/favorites/:id
⏰ User Schedules - Endpoints
Documentação detalhada: docs/api/USER_SCHEDULES_API.md
Deprecated:
POST /api/schedules,PATCH|PUT|DELETE /api/schedules/:id,POST /api/schedules/batch-podcasteDELETE /api/schedules/batch-podcast/:podcastId
respondem comDeprecation: truee emitem o eventodeprecated_endpoint
(App Insights). Usar/api/programming-plans. Detalhes e KQL emdocs/api/USER_SCHEDULES_API.md.
1. Criar schedule (deprecated)
POST /api/schedules
Body:
{
"content_id": "65f1c2a0d4b6a3b0c1234567",
"content_type": "radio",
"start_time": "2026-02-17T08:30:00.000Z",
"repeat": "daily"
}
2. Listar schedules do utilizador
GET /api/schedules/my
GET /api/schedules/my?status=active,paused
status (opcional) filtra por active, paused e/ou cancelled (separados por
vírgulas; valores inválidos → 400). Sem o parâmetro devolve todos os estados.
3. Atualizar schedule (deprecated)
PATCH /api/schedules/:id
(Também existe PUT /api/schedules/:id com o mesmo payload.)
4. Apagar schedule (deprecated)
DELETE /api/schedules/:id
5. Trigger events (pending + consume)
GET /api/schedules/my/trigger-events/pending
POST /api/schedules/my/trigger-events/:id/consume
📦 Modelos de Dados (Schemas)
Radio Schema
interface Radio {
_id: ObjectId;
radioId: string; // Unique identifier
name: string; // Display name
description?: string;
url: string; // Stream URL
urlResolved: string; // Direct stream URL
alternativeUrls?: string[];
homepage?: string;
favicon?: string;
country: string;
countryCode: string;
state?: string;
city?: string;
language: string;
genres: string[]; // ["news", "music", "talk"]
tags: string[]; // ["public", "sports"]
votes: number;
codec: string; // "MP3", "AAC", "OGG"
bitrate: number; // kbps
sampling?: number; // Hz
hasExtendedInfo: boolean;
lastCheckOk: boolean;
lastCheckTime: Date;
clickCount: number;
clickTrend: number;
sslError: boolean;
geoLat?: number;
geoLong?: number;
hasScheduleInfo: boolean;
// ICY Metadata
nowPlaying?: {
title: string;
artist?: string;
timestamp: Date;
};
// EPG
currentProgram?: {
title: string;
startTime: Date;
endTime: Date;
description?: string;
};
// Timestamps
createdAt: Date;
updatedAt: Date;
}
Podcast Schema
interface Podcast {
_id: ObjectId;
podcastIndexId: number;
title: string;
description: string;
longDescription?: string;
author: string;
email?: string;
link: string;
feedUrl: string;
artwork: string;
categories: string[];
language: string;
country?: string;
explicit: boolean;
episodeCount: number;
lastBuildDate: Date;
firstEpisodeDate?: Date;
updateFrequency: string; // "daily", "weekly", "monthly"
// Engagement
subscriptions: number;
rating: number; // 0-5
reviewCount: number;
featured: boolean;
trendingScore: number; // 0-1
// Timestamps
createdAt: Date;
updatedAt: Date;
lastSyncedAt?: Date;
}
Episode Schema
interface Episode {
_id: ObjectId;
podcastId: ObjectId;
title: string;
description: string;
longDescription?: string;
pubDate: Date;
duration: number; // seconds
enclosureUrl: string;
enclosureType: string; // "audio/mpeg"
enclosureLength: number; // bytes
explicit: boolean;
episodeNumber?: number;
seasonNumber?: number;
episodeType: 'full' | 'trailer' | 'bonus';
artwork?: string;
// Enhanced metadata
chapters?: Array<{
startTime: number;
title: string;
url?: string;
}>;
transcript?: string;
// Engagement
plays: number;
likes: number;
comments: number;
// Timestamps
createdAt: Date;
updatedAt: Date;
}
Schedule/Program Schema
interface RadioSchedule {
_id: ObjectId;
radioId: ObjectId;
programs: Array<{
date: string; // "YYYY-MM-DD"
slots: Array<{
title: string;
startTime: string; // "HH:mm"
endTime: string;
duration: number; // minutes
description?: string;
hosts?: string[];
genre?: string;
isLive: boolean;
}>;
}>;
timezone: string;
lastUpdated: Date;
source: 'manual' | 'scraped' | 'api' | 'user-submitted';
}
User Schema
interface User {
_id: ObjectId;
email: string;
passwordHash: string;
name: string;
avatar?: string;
preferences: {
language: string;
theme: 'light' | 'dark' | 'auto';
notifications: boolean;
autoDownload: boolean;
downloadQuality: 'low' | 'medium' | 'high';
};
// Favorites & Library
favoriteRadios: ObjectId[];
favoritePodcasts: ObjectId[];
downloadedEpisodes: ObjectId[];
// History
listeningHistory: Array<{
type: 'radio' | 'podcast' | 'episode';
itemId: ObjectId;
timestamp: Date;
duration: number;
progress: number;
}>;
// Subscription
isPremium: boolean;
subscriptionExpiry?: Date;
// Timestamps
createdAt: Date;
updatedAt: Date;
lastLoginAt: Date;
}
⚙️ Worker Service (Port 3001)
Visão Geral
O Worker processa tarefas assíncronas em background usando BullMQ.
┌──────────────────────────────────────────────────────┐
│ Worker Service (Port 3001) │
├──────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ Radios │ │ Podcasts │ │ ICY │ │
│ │ Sync Queue │ │ Sync Queue │ │ Stats Queue │ │
│ └────────────┘ └─────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ BullMQ Workers (Redis) │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ External APIs (Radio Browser, Podcast │ │
│ │ Index, ICY Metadata) │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ MongoDB Database │ │
│ └────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
Queues (Filas de Jobs)
1. Radio Sync Queue
Job: sync-radios
interface RadioSyncJob {
jobId: string;
filters?: {
country?: string;
language?: string;
limit?: number;
};
priority: number; // 1-10 (10 = highest)
}
Processamento:
- Busca rádios da Radio Browser API
- Valida streams (teste de conectividade)
- Extrai metadata ICY
- Salva/atualiza no MongoDB
- Notifica API via Redis pub/sub
Frequência: Diariamente às 03:00 (Europe/Lisbon). Se o worker estiver indisponível às 03:00, existe um catch-up (*/30min) que enfileira o job uma vez por dia assim que o worker estiver ativo depois das 03:00.
2. Podcast Sync Queue
Job: sync-podcasts
interface PodcastSyncJob {
jobId: string;
podcastId?: string; // Sync específico
syncType: 'full' | 'incremental';
priority: number;
}
Processamento:
- Busca feed RSS do podcast
- Parse XML → JSON
- Compara com episódios existentes
- Download novos episódios
- Atualiza metadata
- Gera thumbnails se necessário
Frequência:
- Full sync: Diariamente
- Incremental: A cada 2 horas para podcasts ativos
3. ICY Metadata Queue
Job: collect-icy-metadata
interface ICYCollectionJob {
jobId: string;
radioId: string;
streamUrl: string;
duration: number; // Tempo de coleta (segundos)
}
Processamento:
- Conecta ao stream
- Lê headers ICY
- Captura metadata (título, artista)
- Identifica padrões (programação)
- Salva histórico
- Gera EPG se possível
Frequência: Contínuo para rádios selecionadas
4. Stream Validation Queue
Job: validate-stream
interface StreamValidationJob {
jobId: string;
radioId: string;
streamUrl: string;
timeout: number;
}
Processamento:
- Testa conectividade
- Valida formato (MP3/AAC/etc)
- Mede bitrate real
- Testa fallback URLs
- Atualiza status (lastCheckOk)
Frequência: Diariamente para todas as rádios
Endpoints Worker
1. Status das Queues
GET /api/jobs/stats
Response (200 OK):
{
"queues": [
{
"name": "radio-sync",
"waiting": 5,
"active": 2,
"completed": 1234,
"failed": 12,
"delayed": 0,
"paused": false
},
{
"name": "podcast-sync",
"waiting": 15,
"active": 5,
"completed": 4567,
"failed": 23,
"delayed": 3,
"paused": false
}
],
"system": {
"uptime": 345678,
"memory": {
"used": 512000000,
"total": 2048000000
},
"cpu": 35.5
}
}
2. Criar Job Manual
POST /api/jobs/create
Body:
{
"queue": "podcast-sync",
"data": {
"podcastId": "507f1f77bcf86cd799439012",
"syncType": "full"
},
"priority": 8
}
3. Logs de Jobs
GET /api/jobs/:jobId/logs
Response (200 OK):
{
"jobId": "abc123",
"queue": "radio-sync",
"status": "completed",
"progress": 100,
"logs": [
{
"timestamp": "2025-12-26T10:00:00Z",
"level": "info",
"message": "Starting radio sync..."
},
{
"timestamp": "2025-12-26T10:00:15Z",
"level": "info",
"message": "Fetched 150 radios from API"
},
{
"timestamp": "2025-12-26T10:05:30Z",
"level": "success",
"message": "Sync completed: 145 updated, 5 new"
}
],
"startedAt": "2025-12-26T10:00:00Z",
"completedAt": "2025-12-26T10:05:30Z",
"duration": 330
}
🔄 Fluxos de Dados Principais
1. Descoberta de Novo Podcast
User (App) → API → Worker → Podcast Index API → Worker → MongoDB → API → User
- User pesquisa "tech podcast"
- API busca no MongoDB (cache)
- Se não encontrado, API envia job para Worker
- Worker busca no Podcast Index API
- Worker parse RSS feed
- Worker salva no MongoDB
- Worker notifica API (Redis pub/sub)
- API retorna resultados para User
2. Reprodução de Rádio com EPG
User → API → MongoDB (radio + schedule) → API → User
│
└──→ ICY Metadata Stream → Now Playing
- User seleciona rádio "RFM"
- API busca dados da rádio (MongoDB)
- API busca EPG da rádio (se disponível)
- API busca metadata ICY atual (cache Redis)
- API retorna:
- Stream URL
- Programa atual
- Próximo programa
- Now playing (música/fala)
3. Download de Episódio
User → API → CDN/Podcast Server → App (Local Storage)
↓
MongoDB (mark as downloaded)
- User clica "Download" no episódio
- App inicia download do
enclosureUrl - App salva localmente (AsyncStorage/SQLite)
- App notifica API: episódio downloaded
- API atualiza user stats (MongoDB)
4. Sincronização de Novos Episódios
Worker (Cron) → RSS Feed → Worker → MongoDB → Redis (notify) → API → Push Notification → User
- Worker executa job scheduled (cada 2h)
- Worker busca RSS de podcasts subscritos
- Worker detecta novos episódios
- Worker salva no MongoDB
- Worker publica evento no Redis
- API recebe notificação
- API envia push notification aos subscribers
- User recebe notificação "Novo episódio disponível!"
📊 Estatísticas e Analytics
Métricas Disponíveis
Por Rádio
{
radioId: string;
totalListens: number;
uniqueListeners: number;
averageListenDuration: number; // segundos
peakListeners: number;
listensByHour: number[]; // 24 positions
listensByDay: number[]; // 7 positions
topCountries: string[];
topCities: string[];
}
Por Podcast
{
podcastId: string;
totalPlays: number;
totalDownloads: number;
uniqueListeners: number;
averageCompletion: number; // 0-1
subscriberGrowth: number[]; // últimos 30 dias
topEpisodes: Array<{
episodeId: string;
plays: number;
}>;
demographics: {
countries: Record<string, number>;
ageGroups: Record<string, number>;
};
}
🔐 Segurança
Rate Limiting
Todos os endpoints têm rate limiting:
{
public: 100 requests / 15min,
authenticated: 1000 requests / 15min,
premium: unlimited
}
CORS
Permitido para:
http://localhost:*(dev)https://*.audissey.pt(production)- Apps móveis (via Origin header validation)
Validação
Todos os inputs são validados com class-validator:
- Email format
- Password strength (min 8 chars, 1 uppercase, 1 number)
- URL validation
- Enum values
🧪 Testing
Testar API Localmente
# Iniciar serviços
docker-compose up -d
# Iniciar API
npx nx serve api
# Testar endpoint
curl http://localhost:3000/api/health
# Ver Swagger
open http://localhost:3000/api/api-docs
Testar Worker Localmente
# Iniciar Worker
npx nx serve worker
# Ver status das queues
curl http://localhost:3001/api/jobs/stats
# Criar job manual
curl -X POST http://localhost:3001/api/jobs/create \
-H "Content-Type: application/json" \
-d '{"queue":"radio-sync","data":{},"priority":5}'
📝 Próximos Passos para Designers
Explorar Swagger UI:
http://localhost:3000/api/api-docs- Ver todos os endpoints disponíveis
- Testar requests diretamente
- Ver schemas de resposta
Entender Estrutura de Dados:
- Quais campos estão disponíveis
- Campos opcionais vs obrigatórios
- Tipos de dados (string, number, array)
Mapear UI Components:
- Cada endpoint → Screen/Component
- Cada campo → UI Element
- Estados de loading, error, empty
Criar Mockups com Dados Reais:
- Usar responses da API para popular designs
- Considerar edge cases (nomes longos, sem imagem, etc)
Definir Estados:
- Loading (skeleton screens)
- Empty states (sem favoritos, sem resultados)
- Error states (sem conexão, erro de servidor)
- Success states
🔗 Links Úteis
- API Swagger:
http://localhost:3000/api/api-docs - Worker Status:
http://localhost:3001/api/jobs/stats - Docs Portal:
http://localhost:3000/api/docs - MongoDB Compass:
mongodb://localhost:27017/audissey - Redis Commander:
http://localhost:8081
💡 Dicas para Designers
Ao Criar Screens
Sempre considere paginação:
- Infinite scroll vs load more button
- Indicadores de página atual
- Loading states entre páginas
Artwork/Images:
- Placeholder enquanto carrega
- Fallback se imagem falhar
- Diferentes resoluções (thumbnail vs full)
Timestamps:
- Formato user-friendly ("há 2 horas", "ontem")
- Timezone local do user
- Formato 24h vs 12h baseado em locale
Duração:
- Formato legível (1h 30m vs 5400s)
- Progress bars visuais
- Remaining time durante playback
Arrays:
- Limite de items visíveis
- "Ver mais" ou horizontal scroll
- Empty array state
Booleans:
- Badges visuais (LIVE, NEW, PREMIUM)
- Icons (✓, ✗)
- Toggle switches
Números grandes:
- Formatação (15.430 vs 15430)
- Abreviação (15.4K vs 15430)
- Gráficos para comparação
Última atualização: 26 Dezembro 2025