A self-hosted, mobile-first podcast app with AI-powered features: transcription, distillations, ad detection, chapter generation, episode chat, and deep research reports. No per-call API costs β all AI runs via the Claude CLI through your existing subscription.
I was paying for a premium podcast app specifically for its AI distillation feature β the ability to extract insights from what I was listening to on the fly. It worked well, but it felt wasteful: I was already running a VPS with the Claude CLI for my own OpenClaw agent, paying for a Claude subscription. Why route that through someone else's SaaS?
So I cut out the middleman.
DistillPod runs entirely on your own server. The AI features β distillations, ad detection, chapters, chat, research β all go through the Claude CLI using your existing subscription. No separate API key, no extra per-call charges on top of what you already pay, no data leaving your infrastructure to a third-party podcast app, no feature flags behind a paywall.
If you have a VPS and a Claude subscription, you already have everything you need to run this.
- π° Home feed β unified list of the latest episodes across all subscriptions, sorted by date. Shows distillation count per episode.
- π Search β find podcasts via the iTunes Search API (no key needed). When the search box is empty, a π€ Suggested for you section surfaces daily AI-generated recommendations based on your listening history.
- π Library β browse your subscribed podcasts and their episode lists with transcript status badges.
βΆοΈ Fullscreen Player β Spotify-style slide-up player with chapter navigation, ad-free toggle, and distillation controls.- βοΈ Distill β tap at any moment while listening. Captures the last 60 seconds of transcript, calls the Claude CLI, and returns a verbatim quote and a 1β2 sentence insight (~30s).
- βοΈ Ad-free audio β after transcription, Claude classifies ad segments and ffmpeg cuts them out. Stream the clean version from the player.
- π Chapters β Claude generates 4β10 named chapters with timestamps from the full transcript. Tap any chapter to jump directly.
- π¬ Episode chat β ask questions about any transcribed episode. Claude answers using the full transcript as context. History kept per episode (capped at 50 messages).
- π¬ Research β trigger a deep research report from any distillation. Claude generates queries, Tavily runs web searches, Claude synthesizes findings into an HTML report. Delivered via Telegram.
- π Distillations library β all your distillations grouped by episode. Copy, delete, or trigger research from any entry.
- β‘ Stale-while-revalidate caching β data is cached in localStorage with a 30-minute TTL and refreshed silently in the background.
All AI features call the Claude CLI as a subprocess:
result = subprocess.run(
["claude", "--print", prompt],
capture_output=True, text=True
)The CLI authenticates through your Claude subscription β no API key, no per-call billing.
Setup: install the Claude CLI and log in once:
npm install -g @anthropic-ai/claude-code
claude login| Feature | Trigger |
|---|---|
| Distillation (quote + insight) | User taps βοΈ while listening |
| Ad detection + audio cut | After transcription completes |
| Chapter generation + summary | Daily sync or manual script run |
| Episode chat | User opens chat or sends message |
| Deep research report | User triggers from a distillation |
| Podcast suggestions | Daily cron at 09:00 BRT |
When you tap Play:
POST /player/playtriggers a background download + transcription task- The episode MP3 is downloaded to
/media/(streaming, skips if cached) - faster-whisper transcribes with
word_timestamps=True(CPU,mediummodel by default) - Word-level timestamps saved to
transcriptstable - Ad detection runs non-fatally after transcription
- The βοΈ Distill button unlocks when
transcript_status = done
| Model | Speed (CPU) | Accuracy |
|---|---|---|
base |
Fastest | Good |
small |
Fast | Better |
medium |
Moderate | Very good (default) |
large-v3 |
Slow | Best |
Set via WHISPER_MODEL in .env.
| Layer | Tech |
|---|---|
| Frontend | React 18 + TypeScript + Vite + Tailwind CSS v3 |
| Client routing | React Router v6 |
| State | Zustand |
| Backend | FastAPI + uvicorn |
| Database | aiosqlite β async SQLite (WAL mode) |
| Transcription | faster-whisper β local speech-to-text (CTranslate2, int8) |
| AI | Claude CLI (claude --print) |
| RSS | feedparser |
| HTTP client | httpx |
| Auth | authlib β Google OAuth2 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User (browser) β
β https://your-domain.example.com β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β HTTPS (Apache reverse proxy)
ββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββ
β FastAPI app β port 8124 (localhost) β
β β
β ββββββββββββββββββ βββββββββββββββββββββββββββββββββββββ β
β β Static files β β API Routers β β
β β (React SPA) β β /auth /podcasts /player β β
β β /assets/** β β /gists /chat /research β β
β ββββββββββββββββββ ββββββββββββββββ¬βββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββ
β
βββββββββββββββββββββββββΌβββββββββββββββββββββββ
β β β
ββββββββββββΌβββββββ βββββββββββββΌβββββββ ββββββββββββΌβββββββ
β SQLite DB β β Media files β β Claude CLI β
β distillpod.db β β /media/*.mp3 β β claude --print β
βββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββ
| Component | Responsibility |
|---|---|
frontend/ |
React SPA β UI only, all state on backend |
backend/main.py |
FastAPI entry point, serves built frontend and reports |
backend/routers/auth.py |
Google OAuth2 flow, session cookie management |
backend/routers/podcasts.py |
Search, subscribe, episode listing, suggestions |
backend/routers/player.py |
Play trigger, audio streaming, transcript status, chapters |
backend/routers/gists.py |
Create, list, delete distillations |
backend/routers/chat.py |
Episode Q&A β init, message, history |
backend/routers/research.py |
Trigger + poll research reports |
backend/services/downloader.py |
Async MP3 download to /media/ |
backend/services/transcriber.py |
faster-whisper, word-level timestamps, async background task |
backend/services/snip_engine.py |
Timestamp window lookup + Claude distillation |
backend/services/ad_detector.py |
Claude ad classification + ffmpeg audio surgery |
backend/services/chapterizer.py |
Claude chapter + summary generation |
backend/services/researcher.py |
Multi-turn research pipeline: Claude + Tavily β HTML report |
backend/services/rss.py |
RSS feed parsing |
backend/services/podcast_index.py |
PodcastIndex API wrapper |
backend/database.py |
SQLite connection, schema init, WAL mode |
backend/config.py |
All settings via env vars (pydantic-settings) |
- Python 3.10+
- Node.js 18+
- ffmpeg (for ad-free audio generation)
- Claude CLI installed and authenticated
- A VPS with a few GB of RAM (faster-whisper
mediumuses ~1.5GB)
git clone https://github.com/your-username/distillpod.git
cd distillpod
cp .env.example .env # edit with your settings
# Backend
cd backend
pip install -r requirements.txt
uvicorn main:app --host 127.0.0.1 --port 8124
# Frontend (production build)
cd ../frontend
npm install
npm run build # outputs to frontend/dist/FastAPI serves frontend/dist/ at / automatically.
Copy .env.example to backend/.env and edit:
| Variable | Required | Description |
|---|---|---|
GOOGLE_CLIENT_ID |
Yes | Google OAuth2 client ID |
GOOGLE_CLIENT_SECRET |
Yes | Google OAuth2 client secret |
ALLOWED_EMAILS |
Yes | Comma-separated email allowlist |
SESSION_SECRET |
Yes | JWT signing secret (openssl rand -hex 32) |
PUBLIC_URL |
Yes | Public-facing domain (CORS, OAuth redirect, report URLs) |
PODCAST_INDEX_API_KEY |
No | Podcast Index API key for richer metadata |
PODCAST_INDEX_SECRET |
No | Podcast Index API secret |
TAVILY_API_KEY |
No | Enables deep research reports |
TELEGRAM_BOT_TOKEN |
No | Telegram notifications |
TELEGRAM_CHAT_ID |
No | Telegram chat ID |
WHISPER_MODEL |
No | Whisper model size: base, small, medium (default), large-v3 |
MEDIA_DIR |
No | Path for downloaded MP3s (default: media/) |
REPORTS_DIR |
No | Path for HTML research reports (default: reports/) |
Run backend and frontend with hot reload in separate terminals:
# Terminal 1 β backend
cd backend
uvicorn main:app --host 127.0.0.1 --port 8124 --reload
# Terminal 2 β frontend
cd frontend
npm install
npm run dev # http://localhost:5173[Unit]
Description=DistillPod β AI-powered podcast player
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/path/to/distillpod/backend
ExecStart=/usr/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port 8124
Restart=always
RestartSec=5
EnvironmentFile=/path/to/distillpod/backend/.env
[Install]
WantedBy=multi-user.targetProxyPreserveHost On
ProxyPass / http://127.0.0.1:8124/
ProxyPassReverse / http://127.0.0.1:8124/Two daily cron jobs run as background scripts:
| Job | Schedule | Script | What it does |
|---|---|---|---|
distillpod-daily-sync |
03:00 BRT | scripts/daily-sync.py |
RSS fetch β download β transcribe β ad detection β chapterization |
distillpod-suggest |
09:00 BRT | scripts/suggest-podcasts.py |
Claude generates queries β iTunes search β 4 suggestions stored |
The daily sync pipeline per subscription:
- Reset stuck
processingepisodes - Fetch latest 5 RSS episodes
- Download recent episodes (β€48h old)
- Transcribe with faster-whisper
- Detect + remove ads (non-fatal)
- Generate chapters + episode summary
- Telegram alert on errors
cd /path/to/distillpod
python3 -m pytest tests/ -vTests run against an in-memory SQLite DB. Auth bypassed via test session cookie.
cd frontend
npx playwright testMobile viewport (390x844), Chromium. Requires TEST_MODE=true in .env for auth bypass.
Test files: navigation, home feed, search, library, player (fullscreen, gists, chapters, ad-free, chat), gists library, caching, SPA routing.
MIT





