docs(audiobookshelf): one page for the endpoint, and the rescan law at the relayout
The page owns the shape, the first-run account step (ABS accepts the initial account only from a local-network address) and the laws: why the array bind is read-only and what the one-line rollback is if on-disk sidecars are ever wanted; why the metadata view is not that rollback; why the mount gate, the wget health check and the amd64 digest pin are each mandatory; and that LAN-only is a posture, not an accident. Media Audiobook Relayout gains the third reader: ABS stores absolute paths, so the relayout procedure has to reach its library too or it keeps rows for folders that are gone. The vault index is regenerated, not hand-carved.
This commit is contained in:
1 parent
ed1a20a960
commit
b7f4b7e148
3 files changed
+101
-1
No files matched your search
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
type: reference
|
||||||
|
description: "Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, first-run account, laws."
|
||||||
|
tags:
|
||||||
|
- dankserver
|
||||||
|
- audiobooks
|
||||||
|
- quadlet
|
||||||
|
timestamp: 2026-11-02
|
||||||
|
---
|
||||||
|
|
||||||
|
# Audiobookshelf
|
||||||
|
|
||||||
|
Audiobookshelf (ABS) serves the [[Entropy Volume]] audiobook library to browsers
|
||||||
|
and phones: it scans `/mnt/entropy/Audiobooks`, keeps its own SQLite library with
|
||||||
|
per-listener playback positions, and streams the audio. It is **not** a media
|
||||||
|
server — it shares nothing with [[Plex]] or [[Jellyfin]] and owns no files on the
|
||||||
|
array.
|
||||||
|
|
||||||
|
**Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user
|
||||||
|
manager) — **Internal-only:** LAN address, no vhost, no DNS name.
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|------|-------|
|
||||||
|
| Unit file | `dankserver/audiobookshelf/quadlet/audiobookshelf.container` (the authority for every number here) |
|
||||||
|
| Image | `ghcr.io/advplyr/audiobookshelf` pinned by **amd64 manifest digest** — see Laws |
|
||||||
|
| Port | `PublishPort=8000:8000` — web UI and API on the same port |
|
||||||
|
| Network | `pasta` (standalone; publishes nothing to other containers) |
|
||||||
|
| Audio bind | `/mnt/entropy/Audiobooks:/audiobooks:ro` — read-only, by design |
|
||||||
|
| State | `/home/dank/Containers/audiobookshelf/config` → `/config`, `…/metadata` → `/metadata` (both NVMe) |
|
||||||
|
| Mount gate | `ExecStartPre=…/wait-for-mount.sh /mnt/entropy` |
|
||||||
|
| Health | `wget` against `/healthcheck` inside the container, 30 s / 3 retries |
|
||||||
|
| I/O share | `IOWeight=10` — a scan loses the array to every other tenant |
|
||||||
|
| Self-healing | `Restart=always`, `RestartSec=5`, `WantedBy=default.target` |
|
||||||
|
| Deploy | `./scripts/deploy.sh dankserver --service audiobookshelf` |
|
||||||
|
| Handler | `deploy_audiobookshelf` in `scripts/deploy-remote.sh` |
|
||||||
|
|
||||||
|
## First run
|
||||||
|
|
||||||
|
1. Open `http://192.168.0.235:8000` **from the LAN**. ABS accepts the initial
|
||||||
|
account creation only from a local-network address; a remote address gets a
|
||||||
|
refusal, not a form.
|
||||||
|
2. Create the owner account, then add one **Audi Book** library with folder
|
||||||
|
`/audiobooks`. The path is the in-container name of the read-only bind.
|
||||||
|
3. The first scan reads the whole tree once. It runs in the background — the
|
||||||
|
deploy gate is already green by then, and `IOWeight=10` keeps it off the
|
||||||
|
critical path.
|
||||||
|
|
||||||
|
Nothing in this repo holds an ABS credential: the account is created in the
|
||||||
|
browser and lives in the `config` bind on the SSD.
|
||||||
|
|
||||||
|
## Laws
|
||||||
|
|
||||||
|
- **The audio bind stays `:ro`.** ABS writes nothing that belongs beside the
|
||||||
|
audio: its library, covers, progress and sidecar metadata all land in
|
||||||
|
`/metadata` on the SSD. A writable bind would put stray writes on the spindle
|
||||||
|
and hand the array to a service with no reason to hold it. If per-book
|
||||||
|
`.opf`/`.nfo` sidecars on disk are ever wanted, the rollback is one line —
|
||||||
|
point the audio bind at the [[Media Metadata View]] the media servers already
|
||||||
|
use — but that view's upper layer exists for those two servers' sidecars, so a
|
||||||
|
third writer needs a reason first ([[Media Metadata View]]).
|
||||||
|
- **Do not bind the raw array without the mount gate.** In a rootless unit
|
||||||
|
`ConditionPathIsMountPoint=` is fail-*dead* and `RequiresMountsFor=` is inert,
|
||||||
|
so a missing array does not stop the start: podman invents an empty directory,
|
||||||
|
ABS indexes it, and an empty library becomes recorded truth. `wait-for-mount.sh`
|
||||||
|
is the only gate that works here — same law the media servers obey
|
||||||
|
([[Quadlet Lifecycle]]).
|
||||||
|
- **The `HealthCmd` must use `wget`.** The image is Alpine and ships `wget`, not
|
||||||
|
`curl`; a `curl` health command reads a perfectly healthy server as unhealthy
|
||||||
|
and `Restart=always` turns that into a loop.
|
||||||
|
- **The image pin is the amd64 manifest digest, not the index digest.**
|
||||||
|
`podman inspect` reports the platform manifest (`RepoDigests[0]`) for what was
|
||||||
|
pulled, which differs from the manifest-list digest `latest` points at. Pin the
|
||||||
|
one this host runs. `latest` is newer than the newest version tag: upstream
|
||||||
|
stopped publishing version tags after `2.19.0` while `latest` kept moving, so
|
||||||
|
"highest version tag" is stale here — read the index, don't guess from tags.
|
||||||
|
- **`IOWeight=10` is inert without the host drop-ins.** `dankserver/host/systemd/`
|
||||||
|
carries `user.slice.d/io-accounting.conf` and the `user@1000` delegate, and the
|
||||||
|
deployer installs both; without them systemd silently drops the weight
|
||||||
|
([[Quadlet Lifecycle]]).
|
||||||
|
- **No `systemctl --user enable`.** Generated Quadlet units cannot be enabled on
|
||||||
|
this host; `WantedBy=default.target` in the unit file is what carries the
|
||||||
|
service across a reboot ([[Quadlet Lifecycle]]).
|
||||||
|
- **LAN-only until Frank says otherwise.** `0.0.0.0:8000` with no reverse proxy
|
||||||
|
in front. Any public vhost requires the Anubis + Authentik chain first, and the
|
||||||
|
outpost proxy provider has to exist before the vhost flips (external exposure
|
||||||
|
policy in `AGENTS.md`).
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
`./tests/run.sh` covers this unit through `tests/dankserver_quadlet.bats`
|
||||||
|
(boot persistence, the SSD state roots, the `:ro` array bind, the mount gate, the
|
||||||
|
`wget` health check) and `tests/media_overlay.bats` (only [[Plex]] and
|
||||||
|
[[Jellyfin]] may reference the metadata view).
|
||||||
@@ -11,7 +11,8 @@ timestamp: 2026-10-31T00:00:00Z
|
|||||||
`/mnt/entropy/Audiobooks` is the one library here that [[Jellyfin]] does not read
|
`/mnt/entropy/Audiobooks` is the one library here that [[Jellyfin]] does not read
|
||||||
at all — its music scanner has no audiobook concept — while [[Plex]] indexes all
|
at all — its music scanner has no audiobook concept — while [[Plex]] indexes all
|
||||||
of it as Artist → Album → Track: the album is the BOOK, that book's single artist
|
of it as Artist → Album → Track: the album is the BOOK, that book's single artist
|
||||||
is the NARRATOR, both matched out of the files' own tags. Today one book is split
|
is the NARRATOR, both matched out of the files' own tags. [[Audiobookshelf]]
|
||||||
|
streams the same tree, read-only, and owns nothing in it. Today one book is split
|
||||||
across `… [Part 1]`, `… [Part 2]` folders three or four deep inside a collection
|
across `… [Part 1]`, `… [Part 2]` folders three or four deep inside a collection
|
||||||
pack, so a scan serves three quarters of a novel; `scripts/media-audiobooks.py`
|
pack, so a scan serves three quarters of a novel; `scripts/media-audiobooks.py`
|
||||||
re-files the tree so the property both servers' book scanners assume is true —
|
re-files the tree so the property both servers' book scanners assume is true —
|
||||||
@@ -36,6 +37,9 @@ python3 scripts/media-audiobooks.py --root /mnt/entropy/Audiobooks \
|
|||||||
# as [[Media Relayout]] shows, on its own journal, and refresh the Plex section
|
# as [[Media Relayout]] shows, on its own journal, and refresh the Plex section
|
||||||
# as in step 5 of the movie Procedure — from inside the container, never over
|
# as in step 5 of the movie Procedure — from inside the container, never over
|
||||||
# the host port.
|
# the host port.
|
||||||
|
# Audiobookshelf stores absolute paths, so the same refresh has to reach its
|
||||||
|
# library too ([[Audiobookshelf]]) — otherwise it keeps rows for folders that
|
||||||
|
# no longer exist.
|
||||||
scripts/media-apply.py apply --plan /tmp/ab_plan.tsv --root /mnt/entropy \
|
scripts/media-apply.py apply --plan /tmp/ab_plan.tsv --root /mnt/entropy \
|
||||||
--journal /mnt/entropy/media-relayout-audiobooks.jsonl --apply
|
--journal /mnt/entropy/media-relayout-audiobooks.jsonl --apply
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ is a locator: one row, one clause, no numbers.
|
|||||||
|
|
||||||
| Page | Owns |
|
| Page | Owns |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| [[Audiobookshelf]] | Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, first-run account, laws. |
|
||||||
| [[Calibre]] | Calibre is the e-book library manager that backs Calibre-Web. |
|
| [[Calibre]] | Calibre is the e-book library manager that backs Calibre-Web. |
|
||||||
| [[Calibre-Web]] | Calibre-Web is a web application that browses, reads, and downloads eBooks from an existing Calibre library database. |
|
| [[Calibre-Web]] | Calibre-Web is a web application that browses, reads, and downloads eBooks from an existing Calibre library database. |
|
||||||
| [[Copyparty]] | Copyparty is a lightweight file server and media streamer that runs as a single Python binary. |
|
| [[Copyparty]] | Copyparty is a lightweight file server and media streamer that runs as a single Python binary. |
|
||||||
|
|||||||
Reference in new issue
Block a user