The Problem

Self-hosting a podcast aggregator for YouTube and Bilibili means juggling yt-dlp, RSS generation, download queues, proxy routing, and auth—each a separate moving part. PigeonPod packages those into one deployable app with a web UI for managing subscriptions, downloads, and feeds.

What This Does

PigeonPod turns YouTube and Bilibili channels, playlists, or single videos into RSS feeds and downloadable audio/video files. The backend (backend/src/main/java/top/asimov/pigeon/) is a Spring Boot app handling subscriptions, sync, downloads, and feed generation. The frontend (frontend/src/) is a React app with pages for the dashboard, feed management, and settings.

The README documents features like cookie-based access for restricted content, proxy support for network isolation, per-feed filters, and OPML export. Eight UI languages are supported via locale files in the backend.

How It Is Wired

Execution starts in frontend/index.html, which loads frontend/src/App.jsx. That component sets up routing and pulls in GlobalPlayer for playback. The frontend calls the Spring backend through REST endpoints; the backend uses MyBatis Plus against SQLite (default) or another configured datasource.

The frontend import graph shows frontend/src/helpers/index as a hub — 8 modules import it. UserContext (6 importers) and useDateFormat (5 importers) are the next most connected. App imports 12 modules but is imported by only 1, so it is a leaf that wires everything together. No circular dependencies exist.

The backend's PlaylistService (1574 lines) carries the widest blast radius — it owns playlist and subscription logic, so most sync and feed operations route through it. BilibiliPlaylistHelper has deep nesting (indentation depth 9), making its control flow hard to follow. The database migration V38__Backfill_episode_duration_seconds.java and EpisodeDurationHelper.java duplicate the same 6-line blocks.

The wiring for download execution, proxy routing, and feed generation is not fully mapped in this analysis — the static import graph covers module dependencies but not the runtime call paths through yt-dlp or the S3 client.

How To Use It

Setup: The Dockerfile builds the full stack. Use Docker Compose as documented in the README:

version: '3.9'
services:
  pigeon-pod:
    image: 'ghcr.io/aizhimou/pigeon-pod:latest'
    restart: unless-stopped
    ports:
      - '8834:8080'
    environment:
      - SPRING_DATASOURCE_URL=jdbc:sqlite:/data/pigeon-pod.db
    volumes:
      - data:/data
volumes:
  data:

Configuration: Environment variables in docker-compose.yml — SPRING_DATASOURCE_URL for the database path, PIGEON_AUTH_ENABLED (defaults to true) to disable built-in auth behind a trusted proxy. The backend reads these via AuthProperties.java and StorageProperties.java.

Running it: docker compose up -d, then open http://localhost:8834. For development, backend/pom.xml is a Maven project (mvn spring-boot:run) and frontend/package.json is a Vite app (npm run dev).

Real-World Use

A team running a private podcast service for internal training videos: they subscribe to a YouTube playlist, enable cookie auth for restricted content, set a proxy for network isolation, and configure a retention filter to keep only videos under 30 minutes. The app syncs daily, downloads new episodes as audio, and exposes a protected RSS feed that their podcast client polls. Failed downloads trigger a webhook digest so an operator can retry or cancel in bulk from the dashboard.

Code Health & Issues

Static analysis found 26 findings (10 high, 16 medium) across 4 kinds:

  • High - Deep nesting (11 instances): frontend/src/pages/DashboardEpisodes/index.jsx, frontend/src/pages/Feed/index.jsx, backend/src/main/java/top/asimov/pigeon/helper/BilibiliPlaylistHelper.java — max indentation depth 9 makes control flow hard to follow. Fix: early returns, extract inner blocks.
  • High - Duplicated code blocks (145 repeated 6-line blocks across 62 files): V38__Backfill_episode_duration_seconds.java, EpisodeDurationHelper.java, AuthProperties.java — extract shared helpers.
  • High - Oversized files (11 instances): frontend/src/pages/Feed/index.jsx, Setting/index.jsx, PlaylistService.java (1574 lines) — split by responsibility.
  • Medium - High branching density (3 instances): OutboundProxyHolder.java, FeedService.java, FeedEpisodeVisibilityHelper.java — 30 branch points over 104 lines; decompose with strategy dispatch.

SDLC observations from the file structure:

  • High - CI doesn't run tests: 8 test files exist, but .github/workflows/build-and-publish.yml has no test command. The green check is meaningless.
  • High - Unpinned GitHub Actions: docker/login-action@v3, docker/setup-qemu-action@v3, docker/setup-buildx-action@v3, docker/build-push-action@v5 use mutable tags — pin to commit SHAs.
  • Medium - No dependency update bot: no Dependabot or Renovate config.
  • Medium - Mutable base images: node:22, maven:3.9.6-eclipse-temurin-17, cgr.dev/chainguard/wolfi-base:latest in the Dockerfile — pin by digest.
  • Medium - No dependency scan in CI: no dependency-review-action or osv-scanner.
  • Medium - Container runs as root: no USER directive in the Dockerfile.
  • Medium - Test coverage is thin: 8 test files against 220 source files (ratio 0.036).
  • Low - No job timeouts in the workflow; Low - missing .editorconfig, .gitattributes, formatter config.

The Bottom Line

PigeonPod is a functional, feature-complete podcast aggregator with a polished UI and solid deployment story. The backend has real complexity in PlaylistService and the feed helpers that will need refactoring before significant feature work. The CI gaps — no tests run, unpinned actions, no dependency scanning — are the first things to fix. Use it if you need a self-hosted YouTube/Bilibili-to-RSS pipeline; budget time for test coverage and build hardening before trusting it in production.