Skip to content

Development

PodVault is a Python application: a service layer with all the business logic, and two thin interfaces on top of it, a CLI and a server-rendered web interface. The source is at github.com/chmedinap/PodVault under the MIT License.

Part Technology
Language Python
Web FastAPI, Jinja2 server-rendered HTML, vanilla JavaScript (no SPA framework, no build step)
Storage SQLite
Feeds feedparser, requests
Tags mutagen (ID3 for MP3, MP4 atoms for M4A/M4B/MP4)
Data handling pandas
Tests pytest
Container Docker, python:3.11-slim, multi-arch (linux/amd64, linux/arm64)
┌──────────────────────┐ ┌──────────────────────────────────┐
│ CLI (cli.py) │ │ Web UI (web/: FastAPI + Jinja2) │
└──────────┬───────────┘ └───────┬──────────────────┬───────┘
│ │ submits │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ JobRunner + JobControl │ │
│ │ (jobs.py) │ │
│ │ Scheduler thread │ │
│ │ (scheduler.py) │ │
│ └────────────┬─────────────┘ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Service layer (services/) │
│ feeds · downloads · episodes · podcasts · queue · scheduling │
│ storage · integrity · verification · migration · export │
│ playback · settings · opml · local_feed │
└──────────┬───────────────────────────────┬──────────────────────┘
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────────────┐
│ Data collection (data/) │ │ Download operations (download/) │
│ RSS parsing, Apple → RSS │ │ downloader, tags, metadata, │
│ feed cache, statistics │ │ naming templates, hashing.py │
└──────────┬───────────────┘ └───────────────┬──────────────────┘
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Data access: database/ (SQLite: per-podcast tables, summary, │
│ app_settings, jobs) · file system (downloads/) │
└─────────────────────────────────────────────────────────────────┘
  • The CLI and web routes are thin. They validate input, format output and delegate to services/. Service functions return plain dataclasses (from services/results.py) and never print or prompt, so the same call can back a terminal command, an HTML page or a background job.
  • Slow work runs as background jobs. The web layer submits downloads, syncs and scans to a JobRunner (a ThreadPoolExecutor) that records every job in the jobs table. Each job gets a JobControl for pause, resume, skip and cancel, checked between episodes, never in the middle of a file.
  • The scheduler is one daemon thread that wakes every PODVAULT_SCHEDULER_POLL_INTERVAL_SECONDS and re-syncs podcasts whose interval has passed. It’s started by podvault serve, not on import, so tests never start a background thread.
  • No new infrastructure. No Redis, no Celery, no Postgres, no separate worker container. One process, one SQLite file, one data volume.
  • No session store. The optional login uses an HMAC-signed cookie and plain ASGI middleware, so streamed audio passes through untouched.

podvault.services is also the stable entry point for scripting: every function returns a dataclass and never prompts (pass require_confirmation=False to download_all).

PodVault/
├── README.md, ARCHITECTURE.md, ROADMAP.md, LICENSE, DOCKERHUB.md
├── Dockerfile, docker-compose.yml
├── setup.py # Package setup
├── tests/ # pytest suite
└── podvault/ # Main package
├── cli.py # Command-line interface
├── config.py # Configuration (env-var overridable)
├── jobs.py # Background job runner + job control (cancel/pause/skip)
├── scheduler.py # Auto-refresh background thread
├── hashing.py # SHA-256 checksums for integrity checks
├── dates.py # Parsing/sorting feed publication dates
├── validation.py # Input validation
├── logging_config.py # Logging setup
├── data/ # RSS feed parsing and statistics
├── database/ # Connections, schema, queries, jobs, settings, scheduling, backups
├── download/ # Downloader, audio tags, metadata, path/naming utilities
├── services/ # Business logic shared by the CLI and web UI
└── web/ # FastAPI app, optional login, routes, Jinja templates, static fonts
Module Responsibility
feeds.py Add and sync feeds; keeps download state and episodes removed from the feed across re-syncs
downloads.py Download all, latest, or a single episode; preview what’s pending
episodes.py List and get episodes (with duration and tag completeness on demand); rename
podcasts.py Podcast list, info and status, with per-podcast aggregates in one SQL query
queue.py Estimated speed and ETA for the Queue page
scheduling.py Per-podcast schedules and the due-check the scheduler runs
storage.py Disk usage, tag-check jobs, local-only episodes
integrity.py Checksum re-verification job, list of corrupt episodes
verification.py Mark episodes missing when their file is gone
migration.py Move existing files to the current naming template (dry run by default)
export.py JSON manifests per podcast or for the whole library
playback.py Resolve audio and cover paths for the web player
settings.py Naming templates: get, validate and update, preview
opml.py OPML export and defensive OPML import
local_feed.py PodVault’s own RSS feed per podcast, built from downloaded episodes

Adding a feed: the URL is normalized (Apple Podcasts links are resolved to RSS through the iTunes Search API), the feed is parsed with feedparser, episode metadata is stored in the podcast’s table in SQLite, and the summary table is updated. Download state and episodes no longer in the feed are carried over on every re-sync.

Downloading an episode:

  1. Build the episode folder from the naming template.
  2. Download the audio with its real extension (an existing file under any audio extension is reused).
  3. Download the episode image, write the tags and cover art, and save the podcast image once.
  4. Save the original RSS entry as episode_metadata.xml.
  5. Stamp the SHA-256 checksum and mark the episode downloaded. Database updates are committed in batches.

Failed downloads are retried with exponential backoff.

SQLite, in one file:

  • One table per podcast, named after the normalized podcast name. Each row is an episode: title, links, publication date, description, GUID, status (downloaded, not downloaded or missing), episode and season numbers, image URL, custom title, saved path, size, file name, Checksum, checksum_verified_at, checksum_status (ok, corrupt or unset) and in_feed (0 when the episode has left the live feed but is kept).
  • summary: one row per podcast with episode counts, cover URL, RSS URL and the auto-refresh schedule (check_interval_hours, auto_download, last_checked_at).
  • app_settings: a single row with the folder and filename templates.
  • jobs: one row per background job (type, podcast, status, progress, message, error). It backs the Activity and Queue pages.
Terminal window
git clone https://github.com/chmedinap/PodVault.git
cd PodVault
pip install -e .
podvault serve

The web interface is at http://127.0.0.1:8000. See Installation for details.

Terminal window
pip install -e ".[test]"
pytest

The suite has 271 tests and runs offline in a few seconds. Every test uses an isolated temporary database and downloads folder, and network calls are mocked, so it’s safe to run any time without touching your real podvault_data.db or downloads/. The fixture also pins the login and backup settings, so your own PODVAULT_* environment can’t change test results.

It covers the service layer and the web routes: feed ingestion and re-sync safety, downloads and job control, verification, migration, renaming, naming templates and settings, scheduling, playback, queue estimates, storage, hashing and integrity, export, the login (sessions, lockout, redirects, cookie flags), audio formats, backups, OPML and the podcast feeds, and date ordering.

Not covered: real network and RSS parsing, real tag writing against real audio files, and concurrent job execution.

Terminal window
docker compose up -d --build

The published image is multi-arch, built with:

Terminal window
docker buildx build --platform linux/amd64,linux/arm64 ... --push

The Dockerfile itself is architecture-neutral.

Bug reports and ideas are welcome on GitHub Issues. If you send a change, keep the existing style: business logic in services/, thin CLI and web layers, no new infrastructure, and tests that run offline.

PodVault is MIT-licensed: use it, copy it, modify it and build on it, as long as you keep the copyright notice and credit: © Carlos Medina.

PodVault v1.3.1 · GitHub · Docker Hub ·Report an issue

Built for home networks · No trackers · MIT License · © Carlos Medina