Podspine

Turn your audiobook files into per-chapter podcast feeds any podcast app can play.

CI Latest release Signed release build (cosign/Sigstore + SLSA 3 provenance) Documentation License: AGPL-3.0 Rust 2024, MSRV 1.88 OpenSSF Best Practices OpenSSF Scorecard Reviewed by Greptile Coverage

Podspine is a small self-hosted server. Point it at a folder of audiobooks and it gives each book its own podcast RSS feed — one episode per chapter, in the right order — that you subscribe to in Apple Podcasts, Pocket Casts, Overcast, AntennaPod, or anything else that reads RSS. No accounts, no separate app, no built-in player. Just a feed URL. - **Chapters as episodes, in order.** The #1 bug in naive attempts is episodes playing out of order; Podspine emits sequential `pubDate`s (oldest = chapter 1) and `itunes:episode` numbers, and refuses to serve a feed that fails its own self-check. - **Zero-config.** `docker run` with your library mounted just works. - **No re-encode, no quality loss.** Podspine never transcodes: chapters are extracted by stream copy, and whole files are served straight from your library. - **Your files stay yours.** DRM-free input only — Podspine ships no DRM circumvention. > Status: released and feature-complete — library scan with live auto-refresh, web > UI, cover art, MP3 folders, Tier-2 formats, chapter sidecars, private > capability-URL feeds (with one-click regenerate), a one-tap **subscribe page** > with per-app deep links + QR, and security hardening. See [CHANGELOG](CHANGELOG.md). ## Quick start ### Docker (recommended) ```bash docker run \ -v /path/to/audiobooks:/library:ro \ -v podspine-data:/data \ -p 8080:8080 \ -e PODSPINE_BASE_URL=http://:8080 \ ghcr.io/schubydoo/podspine:latest ``` Then open to browse your books and copy feed URLs. > **The first scan can take a while.** On first start Podspine processes your whole > library — **chaptered** books are split into per-chapter episodes (whole-file books, > like MP3 folders, stream in place), so a large chaptered library can take minutes. > While it runs, the browse page shows a self-refreshing **"Scanning…"** message with > the count of books finished so far (not an error — it becomes your book grid when the > scan finishes) and feed/audio requests answer `503` with `Retry-After` so podcatchers > retry. A restart with an already-populated `/data` volume is fast — it only processes > what's new or changed. > **Set `PODSPINE_BASE_URL`** to the address podcast apps will actually reach > (your LAN IP or public hostname). It defaults to `http://localhost:8080`, which > only works from the same machine — feed and audio URLs are built from it. `ffmpeg`/`ffprobe` are bundled in the image. The image runs as a non-root user; `/data` holds the SQLite index and any extracted chapter files (whole-file episodes stream from your library), so keep it on a persistent volume. ### Install the binary Linux / macOS (downloads the signed binary for your OS/arch, verifies its checksum, installs to `~/.local/bin`): ```bash curl -fsSL https://raw.githubusercontent.com/schubydoo/podspine/main/install.sh | bash ``` Windows (PowerShell): ```powershell irm https://raw.githubusercontent.com/schubydoo/podspine/main/install.ps1 | iex ``` Or a package manager — `brew install schubydoo/podspine/podspine`, `scoop bucket add podspine https://github.com/schubydoo/scoop-podspine && scoop install podspine`, `nix profile install github:schubydoo/podspine`, or `cargo binstall --git https://github.com/schubydoo/podspine podspine`. `ffmpeg`/`ffprobe` must be on your `PATH`. Full matrix, version pinning, uninstall, and signature verification: **[Installing](https://schubydoo.github.io/podspine/latest/installation/)**. ```bash podspine --library /path/to/audiobooks --base-url http://:8080 # → http://localhost:8080 ``` ### From source ```bash cargo run -- --library ./sample-books ``` ## Configuration The library path is the only required input; everything else has a default and can be set via CLI flag, environment variable, or a TOML file (`--config`), in that precedence. See the **[full option reference](docs/DEPLOYMENT.md#configuration)**. **Heads-up on disk use.** Whole-file episodes — folder-of-MP3 tracks and chapterless single files — are **streamed in place from your library, no copy**. Only a **chaptered** book (one container Podspine splits into per-chapter episodes) materializes audio under its data dir: `full` mode (the default) keeps a per-chapter split of every such book, so a chaptered library roughly **doubles** in total size; `saver` mode (`PODSPINE_STORAGE_MODE=saver`) keeps only a bounded cache instead — it still splits each chapter once at ingest (to record its real byte length), then deletes and regenerates on first play, cutting *steady-state* disk (not ingest time) for a small first-play delay. So budget extra space for chaptered libraries; whole-file libraries cost only their index and covers. Full details: **[storage mode](docs/DEPLOYMENT.md#storage-mode-full-vs-saver)**. **FLAC, Ogg, Opus or ALAC books?** Most podcast apps refuse to play them. Podspine is copy-first, so by default it serves them as-is; set `PODSPINE_TRANSCODE=aac` to re-encode just those books to AAC 128k at ingest (MP3/AAC sources are never touched) — see **[transcoding](docs/DEPLOYMENT.md#transcoding-oddball-formats-podspine_transcode)**. A whole-file `.m4a`/`.m4b` that isn't "faststart" plays fine but seeks slowly; Podspine flags it at ingest, and `PODSPINE_REMUX_NON_FASTSTART=true` will remux it to faststart on demand (cache-managed, no re-encode) — see **[faststart](docs/DEPLOYMENT.md#faststart-for-whole-file-mp4)**. **One book acting up?** Drop a `.podspine.toml` next to it (or in its folder) to override settings — `storage_mode`, `title`/`author`, `disabled`, and more — for just that book. See **[per-book overrides](docs/DEPLOYMENT.md#per-book-overrides-podspinetoml)**. Each book's feed lives at an unguessable **capability URL** — `/feed/{feed_id}.xml`, with `/audio/{feed_id}/{n}` (episode audio, HTTP Range), `/cover/{feed_id}`, and `/subscribe/{feed_id}` (the add-to-app helper page the QR opens). The browse UI (`/`, `/book/{slug}`) enumerates your library, so keep it on the LAN or behind proxy-auth while the capability routes are safe to expose — see **[exposing Podspine safely](docs/DEPLOYMENT.md#exposing-podspine-safely)**. Want to graph it? `--metrics-bind 127.0.0.1:9090` exposes Prometheus metrics — books indexed, feeds served, split duration, errors — on their own listener, separate from the feed server. See **[metrics](docs/DEPLOYMENT.md#metrics-prometheus)**. ## Supported formats Point Podspine at a folder; each audiobook becomes its own feed. A book can be a single file or a per-book subfolder, **at any depth** — an `Author/Title/book.m4b` library (what Audiobookshelf, Plex and Jellyfin produce) is scanned as-is, series folders and all. Nothing needs rearranging. | Tier | Formats | Chapter source | |---|---|---| | **1** | M4B / M4A (AAC/ALAC), single-file MP3, **folder of per-chapter MP3s** | embedded chapters / file (track) order | | **2** | OGG Vorbis, Opus, FLAC, **folder of per-chapter Tier-2 tracks** | embedded chapters, or a `.cue` sidecar (FLAC needs one) / file (track) order | `PODSPINE_TRANSCODE` reaches folder tracks too: with it on, a folder of FLAC tracks is re-encoded once at ingest, so any podcast app can play it. A folder of several `.m4b`/`.m4a` files is several books, one per file — that is what an author folder usually is. Put `folder_is_one_book = true` in a `.podspine.toml` inside the folder when it is really one book split by disc. **Chapter sidecars.** A companion file beside the audio is preferred over embedded chapters, in priority order: **`.cue`** (`INDEX 01`, 75 frames/sec) → **`.ffmeta`** → embedded. `.opf` / `.nfo` / `.odm` are never treated as chapter sources. Use `--force-embedded-chapters` to ignore sidecars. **DRM.** DRM-protected files — Audible `.aax`/`.aaxc`/`.aa`, OverDrive `.odm` — are **skipped** with a logged notice. Podspine ships no DRM circumvention. If you own such files, convert them to a DRM-free format (M4B/MP3/FLAC/…) with your own tools first, then drop the result in your library. ## Adding a feed to your podcast app Open the Podspine UI and click a book. Copy its feed URL, or scan the QR code with your phone to open its **subscribe page** — a set of one-tap "Open in…" deep links for Apple Podcasts, Overcast, Pocket Casts, Castro, AntennaPod, and Podcast Addict. For per-app steps and troubleshooting, see **[docs/importing.md](docs/importing.md)**. ## Documentation 📖 **Full docs: ** (searchable, versioned). The sources also render on GitHub: - **[Deploying](docs/DEPLOYMENT.md)** — Docker/compose, reverse proxy, systemd, backups, and the full config reference. - **[Adding to your podcast app](docs/importing.md)** — per-app import steps and troubleshooting. - **[Architecture](docs/ARCHITECTURE.md)** — how the pipeline and feeds work, and the invariants behind them. - **[Development](docs/DEVELOPMENT.md)** — local setup, crate layout, testing, and release builds. - **[Contributing](CONTRIBUTING.md)** · **[Security](SECURITY.md)** · **[Changelog](CHANGELOG.md)** ## Development Rust workspace, one crate per pipeline stage; requires `ffmpeg`/`ffprobe` on `PATH`. See **[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)** for setup, the crate layout, and testing, and **[CONTRIBUTING.md](CONTRIBUTING.md)** for the workflow. ## License [AGPL-3.0-only](LICENSE). Podspine shells out to `ffmpeg`/`ffprobe` as separate processes (no linking) and ships no DRM circumvention.