Turn your audiobook files into per-chapter podcast feeds any podcast app can play.
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.