🔌

API & Data Overview

🔌 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

  1. App → API: User interage com a app, faz requests
  2. API → Database: Lê/escreve dados em MongoDB
  3. API → Worker: Envia jobs para processamento assíncrono
  4. Worker → External APIs: Busca dados de fontes externas
  5. Worker → Database: Atualiza dados processados
  6. API → App: Retorna dados atualizados

🌐 API Service (Port 3000)

Base URL

Documentação Interativa


📻 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-podcast e DELETE /api/schedules/batch-podcast/:podcastId
respondem com Deprecation: true e emitem o evento deprecated_endpoint
(App Insights). Usar /api/programming-plans. Detalhes e KQL em
docs/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:

  1. Busca rádios da Radio Browser API
  2. Valida streams (teste de conectividade)
  3. Extrai metadata ICY
  4. Salva/atualiza no MongoDB
  5. 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:

  1. Busca feed RSS do podcast
  2. Parse XML → JSON
  3. Compara com episódios existentes
  4. Download novos episódios
  5. Atualiza metadata
  6. Gera thumbnails se necessário

Frequência:

3. ICY Metadata Queue

Job: collect-icy-metadata

interface ICYCollectionJob {
  jobId: string;
  radioId: string;
  streamUrl: string;
  duration: number; // Tempo de coleta (segundos)
}

Processamento:

  1. Conecta ao stream
  2. Lê headers ICY
  3. Captura metadata (título, artista)
  4. Identifica padrões (programação)
  5. Salva histórico
  6. 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:

  1. Testa conectividade
  2. Valida formato (MP3/AAC/etc)
  3. Mede bitrate real
  4. Testa fallback URLs
  5. 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
  1. User pesquisa "tech podcast"
  2. API busca no MongoDB (cache)
  3. Se não encontrado, API envia job para Worker
  4. Worker busca no Podcast Index API
  5. Worker parse RSS feed
  6. Worker salva no MongoDB
  7. Worker notifica API (Redis pub/sub)
  8. 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
  1. User seleciona rádio "RFM"
  2. API busca dados da rádio (MongoDB)
  3. API busca EPG da rádio (se disponível)
  4. API busca metadata ICY atual (cache Redis)
  5. 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)
  1. User clica "Download" no episódio
  2. App inicia download do enclosureUrl
  3. App salva localmente (AsyncStorage/SQLite)
  4. App notifica API: episódio downloaded
  5. API atualiza user stats (MongoDB)

4. Sincronização de Novos Episódios

Worker (Cron) → RSS Feed → Worker → MongoDB → Redis (notify) → API → Push Notification → User
  1. Worker executa job scheduled (cada 2h)
  2. Worker busca RSS de podcasts subscritos
  3. Worker detecta novos episódios
  4. Worker salva no MongoDB
  5. Worker publica evento no Redis
  6. API recebe notificação
  7. API envia push notification aos subscribers
  8. 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:

Validação

Todos os inputs são validados com class-validator:


🧪 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

  1. Explorar Swagger UI: http://localhost:3000/api/api-docs

    • Ver todos os endpoints disponíveis
    • Testar requests diretamente
    • Ver schemas de resposta
  2. Entender Estrutura de Dados:

    • Quais campos estão disponíveis
    • Campos opcionais vs obrigatórios
    • Tipos de dados (string, number, array)
  3. Mapear UI Components:

    • Cada endpoint → Screen/Component
    • Cada campo → UI Element
    • Estados de loading, error, empty
  4. Criar Mockups com Dados Reais:

    • Usar responses da API para popular designs
    • Considerar edge cases (nomes longos, sem imagem, etc)
  5. Definir Estados:

    • Loading (skeleton screens)
    • Empty states (sem favoritos, sem resultados)
    • Error states (sem conexão, erro de servidor)
    • Success states


💡 Dicas para Designers

Ao Criar Screens

  1. Sempre considere paginação:

    • Infinite scroll vs load more button
    • Indicadores de página atual
    • Loading states entre páginas
  2. Artwork/Images:

    • Placeholder enquanto carrega
    • Fallback se imagem falhar
    • Diferentes resoluções (thumbnail vs full)
  3. Timestamps:

    • Formato user-friendly ("há 2 horas", "ontem")
    • Timezone local do user
    • Formato 24h vs 12h baseado em locale
  4. Duração:

    • Formato legível (1h 30m vs 5400s)
    • Progress bars visuais
    • Remaining time durante playback
  5. Arrays:

    • Limite de items visíveis
    • "Ver mais" ou horizontal scroll
    • Empty array state
  6. Booleans:

    • Badges visuais (LIVE, NEW, PREMIUM)
    • Icons (✓, ✗)
    • Toggle switches
  7. 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