🤖

Descoberta Automática de Podcasts

Descoberta Automática de Podcasts

🎯 Visão Geral

Sistema completo de descoberta automática de podcasts através da Podcast Index API, expandindo continuamente o catálogo de conteúdo disponível na plataforma.

📋 Características

Execução Automática

Vetores de Descoberta

O sistema usa 5 vetores complementares para maximizar a cobertura:

Vetor Endpoint Limite Idioma Descrição
1 – Trending PT /podcasts/trending 100 pt Podcasts populares em português europeu
2 – Trending BR /podcasts/trending 100 pt-br Podcasts populares em português brasileiro
3 – Categorias /podcasts/trending?cat=X 50 pt,pt-br Top 10 categorias (News, Society, Comedy, etc.)
4 – Pesquisa /search/byterm 50 — 5 termos de pesquisa em português
5 – Recentes /recent/feeds 100 pt,pt-br Feeds recentemente adicionados

Nota sobre paginação: A Podcast Index API não suporta offset/cursor na maioria dos endpoints (trending, search, recent). O max suporta até 1000. A estratégia de múltiplos vetores com limites elevados compensa essa limitação.

Funcionalidades

🔧 Configuração

Variáveis de Ambiente

Adicione ao ficheiro .env:

# Podcast Index API Credentials
# Obtenha em: https://podcastindex.org
PODCAST_INDEX_API_KEY=your_api_key_here
PODCAST_INDEX_API_SECRET=your_api_secret_here
PODCAST_INDEX_API_URL=https://api.podcastindex.org/api/1.0

⚠️ Importante: Se o API_SECRET contiver caracteres especiais ($, ^, !), escape-os com \:

# Exemplo com $ no final
PODCAST_INDEX_API_SECRET=mySecret123\$h

Obter Credenciais

  1. Acesse https://podcastindex.org
  2. Crie uma conta gratuita
  3. Acesse a área de desenvolvedores
  4. Gere suas credenciais API

🏗️ Arquitetura

Componentes

apps/worker/src/app/
├── jobs/
│   ├── services/
│   │   └── job-scheduler.service.ts    # Agendamento cron
│   └── processors/
│       └── sync-podcasts.processor.ts  # Processamento job
│
├── podcasts-sync/
│   ├── services/
│   │   ├── podcasts-sync.service.ts    # Lógica principal
│   │   └── rss-parser.service.ts       # Parsing RSS
│   └── schemas/
│       ├── podcast.schema.ts           # Schema MongoDB
│       └── episode.schema.ts

Tecnologias

🔄 Fluxo de Execução

1. Agendamento (Cron)

@Cron(CronExpression.EVERY_DAY_AT_5AM, {
  name: 'sync-podcasts-daily',
  timeZone: 'Europe/Lisbon',
})
async scheduleSyncPodcasts() {
  await this.syncPodcastsQueue.add('sync-podcasts-daily', {
    withDiscovery: true
  });
}

2. Descoberta Multi-Vetor

// Vector 1: Trending PT (100 podcasts)
const trendingPt = await fetchPodcastsFromAPI(100, 'pt');

// Vector 2: Trending BR (100 podcasts)
const trendingBr = await fetchPodcastsFromAPI(100, 'pt-br');

// Vector 3: By category (10 categorias × 50 cada, lang=pt,pt-br)
for (const category of DISCOVERY_CATEGORIES) {
  const byCategory = await fetchPodcastsByCategory(category, 'pt,pt-br', 50);
}

// Vector 4: By search term (5 termos × 50 cada)
for (const term of DISCOVERY_SEARCH_TERMS) {
  const bySearch = await fetchPodcastsBySearch(term, 50);
}

// Vector 5: Recent feeds (100 podcasts, lang=pt,pt-br)
const recentFeeds = await fetchRecentFeeds('pt,pt-br', 100);

Endpoints utilizados:

Autenticação:

3. Deduplicação e Processamento

// 1. Agregar todos os resultados dos 5 vetores
// 2. Deduplicar por Podcast Index ID (in-memory)
// 3. Para cada podcast único:
//    a. Verificar duplicação na DB (por RSS URL)
//    b. Se novo: validar, enriquecer via RSS, inserir
//    c. Se existe: verificar atualizações
//    d. Log de progresso a cada 10 podcasts

4. Resultado

{
  success: true,
  podcastsChecked: 1,
  newPodcastsAdded: 45,
  podcastsUpdated: 5,
  errors: [],
  duration: 12340, // ms
  timestamp: "2025-12-24T05:00:00.000Z"
}

📊 Monitorização

Logs do Worker

Durante a execução:

🔄 Iniciando sincronização de podcasts...
✨ Discovery mode enabled
🔍 Discovering new podcasts...
📡 API returned 50 podcasts
Processing 50 podcasts...
Progress: 10/50 - New: 8, Existing: 2
Progress: 20/50 - New: 16, Existing: 4
Progress: 30/50 - New: 24, Existing: 6
Progress: 40/50 - New: 32, Existing: 8
Progress: 50/50 - New: 40, Existing: 10
📊 Discovery summary: 40 new, 10 existing, 0 errors
✅ Discovery phase completed
📻 Total de podcasts a verificar: 41
📊 RESUMO DA SINCRONIZAÇÃO DE PODCASTS
✅ Podcasts verificados: 41
🔄 Podcasts atualizados: 2
❌ Erros: 0
⏱️  Duração: 12.3s

API de Status

Verificar estado das filas:

GET http://localhost:3001/api/jobs/stats

Resposta:

{
  "queues": {
    "sync-podcasts": {
      "waiting": 0,
      "active": 1,
      "completed": 145,
      "failed": 2
    }
  }
}

Swagger UI

Acesse a documentação interativa:

http://localhost:3001/api/docs

🎮 Execução Manual

Via API do Worker

Disparar job manualmente:

curl -X POST http://localhost:3001/api/jobs/trigger/sync-podcasts-with-discovery \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json"

Via Script

Executar diretamente:

# Build do worker
npm run build:worker

# Executar job específico
node dist/apps/worker/main.js --job=sync-podcasts

🗄️ Modelo de Dados

Podcast Schema

{
  _id: ObjectId,
  title: string,                    // Título do podcast
  description: string,              // Descrição
  rss_url: string,                 // URL do feed RSS (único)
  language: string,                // Idioma (ex: "pt")
  categories: string[],            // Categorias
  author: string,                  // Autor/Produtor
  logo: string,                    // URL da imagem
  podcast_index_id: number,        // ID na Podcast Index API
  createdAt: Date,
  updatedAt: Date
}

Índices:

Exemplo de Podcast

{
  "_id": "694c01480a4bc963385013eb",
  "title": "Bloco Podcast",
  "description": "Bloco Podcast: Bitcoin, Libertarianismo e Soberania...",
  "rss_url": "https://anchor.fm/s/e3ad2094/podcast/rss",
  "language": "pt",
  "categories": ["Technology", "Bitcoin"],
  "author": "Hugo Quinteiro",
  "logo": "https://example.com/logo.jpg",
  "podcast_index_id": 6425308,
  "createdAt": "2025-12-24T05:00:12.000Z",
  "updatedAt": "2025-12-24T05:00:12.000Z"
}

🔐 Segurança

Autenticação API

A Podcast Index API usa autenticação baseada em hash:

  1. Gera timestamp Unix atual
  2. Concatena: API_KEY + API_SECRET + timestamp
  3. Calcula SHA1 hash
  4. Envia nos headers

Implementação:

private generateAuthHeaders(): Record<string, string> {
  const apiHeaderTime = Math.floor(Date.now() / 1000);
  const dataToHash = this.API_KEY + this.API_SECRET + apiHeaderTime;
  const hash = crypto.createHash('sha1')
    .update(dataToHash)
    .digest('hex');

  return {
    'X-Auth-Date': apiHeaderTime.toString(),
    'X-Auth-Key': this.API_KEY,
    'Authorization': hash,
    'User-Agent': 'Audissey/1.0',
  };
}

Rate Limiting

Pipeline — Injeção de Secrets

As credenciais do Podcast Index são injetadas como Container App secrets pelo pipeline azure-pipelines-worker-release.yml:

⚠️ Troubleshooting

Erro 401: Authentication Failed

Causa: Hash de autenticação inválido

Soluções:

  1. Verificar escape de caracteres especiais no .env
  2. Verificar se credenciais são válidas
  3. Testar com script de autenticação:
node test-podcast-api-auth.js

Erro 404: Endpoint Not Found

Causa: Endpoint da API incorreto

Solução: Verificar URL base:

PODCAST_INDEX_API_URL=https://api.podcastindex.org/api/1.0

Nenhum Podcast Novo

Possíveis causas:

  1. Todos os podcasts descobertos já existem na DB (normal após muitas execuções)
  2. API temporariamente sem novos conteúdos
  3. Credenciais PODCAST_INDEX_API_KEY/PODCAST_INDEX_API_SECRET em falta no Container App

Verificação:

# Ver podcasts trending na API
curl "https://api.podcastindex.org/api/1.0/podcasts/trending?max=10&lang=pt" \
  -H "X-Auth-Date: $(date +%s)" \
  -H "X-Auth-Key: $PODCAST_INDEX_API_KEY" \
  -H "Authorization: $(echo -n "$PODCAST_INDEX_API_KEY$PODCAST_INDEX_API_SECRET$(date +%s)" | sha1sum | cut -d' ' -f1)"

Worker Não Executa Cron

Verificação:

  1. Worker está em execução?
ps aux | grep "nx serve worker"
  1. Timezone correto?
# Deve estar em Europe/Lisbon
echo $TZ
  1. Ver logs do scheduler:
tail -f worker-output.log | grep "scheduleSyncPodcasts"

📈 Métricas e KPIs

Métricas Rastreadas

Dashboard (Futuro)

Métricas a implementar:

🧪 Testes

Teste Manual

  1. Configurar ambiente:
cp .env.example .env
# Adicionar credenciais reais
  1. Iniciar worker:
npm run dev:worker
  1. Disparar job:
curl -X POST http://localhost:3001/api/jobs/trigger/sync-podcasts-with-discovery
  1. Verificar logs e base de dados:
# Logs
tail -f worker-output.log

# MongoDB
mongo audissey
> db.podcasts.count()
> db.podcasts.find().limit(5).pretty()

Teste de Autenticação

Use o script incluído:

node test-podcast-api-auth.js

Saída esperada:

🔐 Testing Podcast Index API Authentication
═══════════════════════════════════════════
Response status: 200
✅ SUCCESS! First 3 podcasts:
  1. Bloco Podcast
  2. O Assunto é Dinheiro
  3. Bem Mais Que Uma História

📚 Referências

🚀 Roadmap

Curto Prazo

Médio Prazo

Longo Prazo