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.
This commit is contained in:
Frank committed 2026-11-02 02:55:55 -08:00
1 parent 317f36b456
commit 48ef6d34cd
3 files changed
+48 -16

No files matched your search

@@ -1,6 +1,6 @@
---
type: reference
description: "Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, first-run account, laws."
description: "Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, the API bootstrap, laws."
tags:
- dankserver
- audiobooks
@@ -37,19 +37,32 @@ manager) — **Internal-only:** LAN address, no vhost, no DNS name.
| Deploy | `./scripts/deploy.sh dankserver --service audiobookshelf` |
| Handler | `deploy_audiobookshelf` in `scripts/deploy-remote.sh` |
## First run
## Bootstrap
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.
`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.
Nothing in this repo holds an ABS credential: the account is created in the
browser and lives in the `config` bind on the SSD.
| 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
@@ -95,10 +108,29 @@ browser and lives in the `config` bind on the SSD.
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) and `tests/media_overlay.bats` (only [[Plex]] and
[[Jellyfin]] may reference the metadata view).
`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).