Files
serverAppConfigs/serverAppConfigs-docs/Dankserver/Audiobookshelf.md
T
Frank 48ef6d34cd docs(audiobookshelf): the first run is a script, and its re-run is the scan trigger
The page claimed the account can only be created from a LAN address; measured,
the door is POST /init and it answers plain text. Replaced with the bootstrap the
deployer now runs, and with the doors a consumer actually needs: /status reports
isInit with no /api prefix, the item count rides /api/libraries/{id}/items ->
total (the library DTO has no totals), and the list DTO carries media.numTracks
while /api/items/{id} returns media.audioFiles.

New law: the folder watcher cannot cross the rootless podman bind — the same gap
as Jellyfin's auto-scan note — so files added on the host stay invisible until a
scan runs, and the deployer's re-run of the bootstrapper is the cheap trigger.

Testing Policy lists the new seam; index.md regenerated from frontmatter.
2026-11-02 02:55:55 -08:00

8.2 KiB

type, description, tags, timestamp
type description tags timestamp
reference Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, the API bootstrap, laws.
dankserver
audiobooks
quadlet
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 Environment=PORT=8000 + PublishPort=8000:8000 — web UI and API on one port
Base path Environment=ROUTER_BASE_PATH=/ — the URL is the plain host:port, no /audiobookshelf prefix
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

Bootstrap

scripts/audiobookshelf-init.sh — run by the deployer after the health gate, so a deployed ABS is a usable ABS: /healthcheck answers 200 on a server with no user, no library and no scan, and nothing else in the deploy notices the difference. The script is idempotent; re-running it costs one incremental scan.

Step Door Printed
credential ~/.config/secrets/audiobookshelf (mode 600, generated once, host-side) credential: created …
account POST /init with {"newRoot":{"username","password"}} account: created (frank) / already registered
session POST /login → bearer token token: obtained (root)
library POST /api/libraries, folder /audiobooks, provider audiobook library: created Audiobooks (<id>) / exists
scan POST /api/libraries/<id>/scan scan: accepted
proof GET /api/libraries/<id>/items?limit=1 items: <total>

Sign in at http://192.168.0.235:8000 as frank; the password is awk -F= '/^ABS_ADMIN_PASSWORD=/{print $2}' ~/.config/secrets/audiobookshelf on Dankserver. Change it in Settings → Users → frank → Password; the secret file then holds a stale value and only matters again if the config bind is ever destroyed (a fresh DB makes the script create a new owner from it).

The scan of 1,103 shelves is slow and background; watch items: climb from 0, never /healthcheck. It also reads files that are genuinely broken and logs [AudioFileScanner] Invalid data found when processing input per file — that is the file, not the bind: the same line count grows while the total climbs.

Laws

  • Speak the image's env names, not the README's. The server reads PORT, CONFIG_PATH, METADATA_PATH and ROUTER_BASE_PATH; the ABS_* family printed by the server's own README is the npm package's naming and is inert in this image. Measured 2026-11-02 on the first deploy: with ABS_PORT=8000 the server listened on :80 under base path /audiobookshelf, so systemctl said active (running) while every probe of :8000 was refused and the deploy gate failed at 180 s. Two seconds with the pinned digest settles it: podman inspect --format '{{.Config.Env}}' <image>.
  • 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).
  • There is no /setup route; the creation door is POST /init. /status (no /api prefix) reports isInit = "a root user exists", POST /init with {"newRoot":{"username","password"}} creates it, POST /login returns the bearer every /api/* route wants. POST /setup answers Unauthorized — that is a missing route wearing an auth message. Read off dist-server/server/Server.js inside the pinned digest rather than from a forum.
  • A 200 may carry plain text. res.sendStatus(200) answers the body OK, and /init and the scan route use it; a client that parses every success as JSON crashes on the one call that matters (tests/audiobookshelf_init.bats pins it).
  • The item count is not a field on the library DTO. The door is GET /api/libraries/<id>/items?limit=1 → total. A poller that guesses a plausible name (numItems) prints 0 forever beside a scan that is working.
  • A fresh scan is the only thing that notices new audio. inotify does not cross a rootless podman bind — measured for Jellyfin, whose page owns that law — so the in-container watcher sees nothing a host process files onto the array, and the deployer's re-run of the bootstrapper is the cheap scan trigger.

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), tests/audiobookshelf_init.bats (the bootstrapper against a stub of the API above: create-once, idempotent re-run, no secret in a URL, refusal by URL when the server does not answer, and the deployer calling it only after the health gate) and tests/media_overlay.bats (only Plex and Jellyfin may reference the metadata view).