Files
serverAppConfigs/serverAppConfigs-docs/Dankserver/Audiobookshelf.md
T
Frank 56dfc6d0b9 docs(audiobookshelf): the scanner refusal names the file, not the bind
The first full scan logged "Invalid data found when processing input" per file and
carried on. That line is about the file: the scanner skips it, so the book never
appears in the library at all. Census of the 43 refused files: 38 were 0 bytes, 3
held WAV bytes behind a .mp3 name, 2 were unreadable. The check to run first is a
byte count, not a server investigation.

Full suite: 818 tests, exit 0.
2026-11-02 02:57:32 -08:00

8.6 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.
  • Invalid data found when processing input names the file, not the bind. The scanner logs it per file and skips the file — the book never appears — so a missing title beside those lines is library debris, not a server fault. Census of the refusals on the first full scan: 38 of 43 files were 0 bytes, 3 were WAV bytes wearing a .mp3 name, 2 were unreadable. stat -c %s before any investigation of the server.

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).