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
- Frequência: Diária às 5h da manhã (timezone Europe/Lisbon)
- Volume: ~300-500 podcasts únicos por execução (5 vetores de descoberta)
- Idioma: Português (PT) e Português Brasileiro (PT-BR)
- Fonte: Podcast Index API
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
maxsuporta até 1000. A estratégia de múltiplos vetores com limites elevados compensa essa limitação.
Funcionalidades
- ✅ 5 vetores de descoberta complementares
- ✅ Suporte a PT e PT-BR (português europeu e brasileiro)
- ✅ Descoberta por categorias via trending filtrado (10 categorias)
- ✅ Deduplicação automática por Podcast Index ID e RSS URL
- ✅ Enriquecimento de metadados via parsing RSS
- ✅ Validação de feeds antes da inserção
- ✅ Logging detalhado do processo
- ✅ Tratamento de erros robusto por vetor
- ✅ Retry automático em caso de falha
🔧 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
- Acesse https://podcastindex.org
- Crie uma conta gratuita
- Acesse a área de desenvolvedores
- 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
- Queue: BullMQ + Redis
- Scheduler: @nestjs/schedule (cron)
- HTTP: fetch API
- RSS: rss-parser
- Auth: SHA1 hash
🔄 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:
/podcasts/trending?max=N&lang=X— trending por idioma/podcasts/trending?cat=X&max=N&lang=X— trending filtrado por categoria/search/byterm?q=X&max=N— pesquisa por termo/recent/feeds?max=N&lang=X— feeds recentemente adicionados
Autenticação:
- Header
X-Auth-Date: Unix timestamp - Header
X-Auth-Key: API Key - Header
Authorization: SHA1(key + secret + timestamp)
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:
rss_url: Único (previne duplicados)title: Text searchcategories: Filtroslanguage: Filtrospodcast_index_id: Queries API
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:
- Gera timestamp Unix atual
- Concatena:
API_KEY + API_SECRET + timestamp - Calcula SHA1 hash
- 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
- Podcast Index API: ~100 requisições/hora
- Nossa implementação: ~18 requisições/dia (2 trending + 10 categorias + 5 pesquisas + 1 recentes)
- Bem abaixo do limite: Margem de segurança
Pipeline — Injeção de Secrets
As credenciais do Podcast Index são injetadas como Container App secrets pelo pipeline azure-pipelines-worker-release.yml:
- Task
Ensure Podcast Index secrets (dev/staging)— definepodcastindexapikeyepodcastindexapisecret - Env vars
PODCAST_INDEX_API_KEYePODCAST_INDEX_API_SECRETreferenciam os secrets - Os valores vêm dos Variable Groups
audissey-dev(id=2) eaudissey-staging(id=3)
⚠️ Troubleshooting
Erro 401: Authentication Failed
Causa: Hash de autenticação inválido
Soluções:
- Verificar escape de caracteres especiais no
.env - Verificar se credenciais são válidas
- 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:
- Todos os podcasts descobertos já existem na DB (normal após muitas execuções)
- API temporariamente sem novos conteúdos
- Credenciais
PODCAST_INDEX_API_KEY/PODCAST_INDEX_API_SECRETem 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:
- Worker está em execução?
ps aux | grep "nx serve worker"
- Timezone correto?
# Deve estar em Europe/Lisbon
echo $TZ
- Ver logs do scheduler:
tail -f worker-output.log | grep "scheduleSyncPodcasts"
📈 Métricas e KPIs
Métricas Rastreadas
- Podcasts descobertos/execução: ~300-500 únicos (antes da deduplicação com DB)
- Novos podcasts/dia: Variável, diminui à medida que o catálogo cresce
- Taxa de sucesso: >95%
- Duração média: ~30-60 segundos (5 vetores)
- Vetores ativos: 5 (trending PT, trending BR, categorias, pesquisa, recentes)
Dashboard (Futuro)
Métricas a implementar:
- Crescimento do catálogo
- Categorias mais populares
- Idiomas disponíveis
- Taxa de falha por período
- Performance da API externa
🧪 Testes
Teste Manual
- Configurar ambiente:
cp .env.example .env
# Adicionar credenciais reais
- Iniciar worker:
npm run dev:worker
- Disparar job:
curl -X POST http://localhost:3001/api/jobs/trigger/sync-podcasts-with-discovery
- 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
- Podcast Index API Documentation
- Podcast Index Website
- RSS 2.0 Specification
- iTunes Podcast RSS Extensions
🚀 Roadmap
Curto Prazo
- Dashboard de monitorização
- Alertas por email em caso de falha
- Retry exponencial melhorado
- Cache de resultados API
Médio Prazo
- Descoberta por múltiplos critérios (categorias, popularidade)
- Machine Learning para recomendações
- Análise de tendências
- Integração com outras APIs (Spotify, Apple Podcasts)
Longo Prazo
- Descoberta em tempo real via webhooks
- Sistema de curadoria colaborativa
- Multi-idioma com deteção automática
- Analytics avançados de consumo