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) |
Architecture
Section titled “Architecture”┌──────────────────────┐ ┌──────────────────────────────────┐│ 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 (fromservices/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(aThreadPoolExecutor) that records every job in thejobstable. Each job gets aJobControlfor 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_SECONDSand re-syncs podcasts whose interval has passed. It’s started bypodvault 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).
Project layout
Section titled “Project layout”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 fontsService modules
Section titled “Service modules”| 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 |
Data flow
Section titled “Data flow”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:
- Build the episode folder from the naming template.
- Download the audio with its real extension (an existing file under any audio extension is reused).
- Download the episode image, write the tags and cover art, and save the podcast image once.
- Save the original RSS entry as
episode_metadata.xml. - Stamp the SHA-256 checksum and mark the episode
downloaded. Database updates are committed in batches.
Failed downloads are retried with exponential backoff.
Database schema
Section titled “Database schema”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 downloadedormissing), episode and season numbers, image URL, custom title, saved path, size, file name,Checksum,checksum_verified_at,checksum_status(ok,corruptor unset) andin_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.
Running from source
Section titled “Running from source”git clone https://github.com/chmedinap/PodVault.gitcd PodVaultpip install -e .podvault serveThe web interface is at http://127.0.0.1:8000. See Installation for details.
Running the tests
Section titled “Running the tests”pip install -e ".[test]"pytestThe 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.
Building the Docker image
Section titled “Building the Docker image”docker compose up -d --buildThe published image is multi-arch, built with:
docker buildx build --platform linux/amd64,linux/arm64 ... --pushThe Dockerfile itself is architecture-neutral.
Contributing
Section titled “Contributing”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.