docs(audiobookshelf): the base-path law, the SSO contract, the phone address
The page owned the URL as a plain host:port; every client now carries the /audiobookshelf prefix. Added the two laws a next operator needs (why the base path is not a choice, why the redirect subfolder must be sent) and the Single sign-on section naming which script owns which half.
This commit is contained in:
1 parent
393b77dfd2
commit
3138e782bd
2 files changed
+49
-13
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, the API bootstrap, laws."
|
||||
description: "Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, the API bootstrap, authentik SSO, laws."
|
||||
tags:
|
||||
- dankserver
|
||||
- audiobooks
|
||||
@@ -17,7 +17,7 @@ server — it shares nothing with [[Plex]] or [[Jellyfin]] and owns no files on
|
||||
array.
|
||||
|
||||
**Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user
|
||||
manager) — **Internal-only:** LAN address, no vhost, no DNS name.
|
||||
manager) — **Internal-only:** LAN address `http://192.168.0.235:8000/audiobookshelf`, no vhost, no DNS name.
|
||||
|
||||
## Shape
|
||||
|
||||
@@ -26,7 +26,7 @@ manager) — **Internal-only:** LAN address, no vhost, no DNS name.
|
||||
| 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 |
|
||||
| Base path | `Environment=ROUTER_BASE_PATH=/audiobookshelf` — every URL below carries this prefix, and the shipped SPA cannot boot without it (Laws) |
|
||||
| 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) |
|
||||
@@ -36,6 +36,7 @@ manager) — **Internal-only:** LAN address, no vhost, no DNS name.
|
||||
| Self-healing | `Restart=always`, `RestartSec=5`, `WantedBy=default.target` |
|
||||
| Deploy | `./scripts/deploy.sh dankserver --service audiobookshelf` |
|
||||
| Handler | `deploy_audiobookshelf` in `scripts/deploy-remote.sh` |
|
||||
| Single sign-on | authentik provider `audiobookshelf`; the local `frank` account stays enabled as break-glass — see *Single sign-on* |
|
||||
| Library layout | one book per folder — ABS groups by the first directory holding a media file (see Laws); every move is journalled at `/mnt/entropy/.media-campaign/media-relayout-audiobooks.jsonl` |
|
||||
|
||||
## Bootstrap
|
||||
@@ -54,7 +55,7 @@ difference. The script is idempotent; re-running it costs one incremental scan.
|
||||
| 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
|
||||
Sign in at `http://192.168.0.235:8000/audiobookshelf` 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
|
||||
@@ -68,8 +69,9 @@ the file, not the bind: the same line count grows while the total climbs.
|
||||
## Phone app
|
||||
|
||||
The official **Audiobookshelf** app connects with Server Address
|
||||
`http://192.168.0.235:8000`, username `frank`, and the same password as the web UI —
|
||||
no base-path prefix, plain HTTP. Verified from a second host 2026-11-03: `/healthcheck`
|
||||
`http://192.168.0.235:8000/audiobookshelf` — the base path belongs in the address — username
|
||||
`frank`, and the same password as the web UI, or the SSO button; plain HTTP. Verified from a
|
||||
second host 2026-11-02: `/healthcheck`
|
||||
and `/ping` answer 200 across the LAN and `iptables -S INPUT` carries no rule for 8000,
|
||||
so a phone on `192.168.0.0/24` connects with no further setup; `download: true` on the
|
||||
root account is what makes offline listening work. Away from the LAN it cannot connect —
|
||||
@@ -81,6 +83,25 @@ inside the user object. A client that reads `response.token` gets the empty stri
|
||||
`Authorization: Bearer ` with nothing after it, and every `/api/*` call then answers `401`
|
||||
as though the password were wrong.
|
||||
|
||||
## Single sign-on (authentik)
|
||||
|
||||
Both doors take the household identity: the web UI puts **Sign in with Authentik** beside
|
||||
the local form, and the phone app offers SSO on its server-address screen. The local
|
||||
`frank` account stays enabled on purpose — `authActiveAuthMethods` is `local` **and**
|
||||
`openid`, and `local` is how you get in when the IdP is down.
|
||||
|
||||
| Piece | Owner |
|
||||
|-------|-------|
|
||||
| Provider `audiobookshelf`, the two groups, the scope mapping | `scripts/authentik-audiobookshelf-sso.sh` — re-run it after an authentik rebuild, because this state lives in the IdP's database and a blueprint re-import reverts it silently |
|
||||
| Client secret | `ABS_OIDC_CLIENT_SECRET` in `~/.config/secrets/audiobookshelf` on Dankserver (mode 600; never in the repo, a URL or an argv) |
|
||||
| The ABS half: issuer, the four IdP URLs, claim, auto-register | `scripts/audiobookshelf-init.sh`, armed automatically when that secret exists in the credential file |
|
||||
| Role at first SSO sign-in | the `abs_groups` claim: `audiobookshelf_admins` → `admin`, `audiobookshelf_users` → `user`, in neither → denied (fail closed) |
|
||||
|
||||
The IdP saying yes is not the same as being allowed in: with `authOpenIDAutoRegister` on,
|
||||
a first sign-in creates the account, and the claim alone decides the type. A user in
|
||||
neither group is refused after a successful authentication — that is the mapping working,
|
||||
not a broken login.
|
||||
|
||||
## Laws
|
||||
|
||||
- **Speak the image's env names, not the README's.** The server reads `PORT`,
|
||||
@@ -91,6 +112,21 @@ as though the password were wrong.
|
||||
`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 URL carries `/audiobookshelf`, and that is not a style choice.** The SPA in
|
||||
`/app/client/dist/index.html` is built with `routerBasePath:"/audiobookshelf"`, so it asks
|
||||
for its assets under that prefix whatever the server is mounted at. Mounted at `/`, every
|
||||
script request falls through to the SPA fallback, comes back `text/html`, Chrome refuses to
|
||||
execute it, and the page never leaves "Loading…" — while `/healthcheck`, the API and the
|
||||
phone app stay perfectly healthy, which is how this hid behind an API-only verification
|
||||
(measured 2026-11-02). Prove a UI claim in a browser, never from `/health`.
|
||||
- **The OIDC redirect subfolder is a value you send, not a default you inherit.** The 2.17.4
|
||||
upgrade only fills `authOpenIDSubfolderForRedirectURLs` on a database that ALREADY had OIDC
|
||||
enabled, so a DB created before SSO leaves it unset and the server builds
|
||||
`http://host/undefined/auth/openid/callback`. authentik answers the authorize request with
|
||||
"Redirect URI Error" and nothing on the ABS side looks wrong (measured 2026-11-02). The
|
||||
provider's registered URI must equal `<origin><subfolder>/auth/openid/callback`, so each
|
||||
script derives the value from one variable of its own (`ABS_URL`, `ABS_BASE`) and
|
||||
`tests/audiobookshelf_init.bats` pins what reaches the wire.
|
||||
- **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
|
||||
@@ -207,12 +243,12 @@ as though the password were wrong.
|
||||
## 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).
|
||||
(boot persistence, the SSD state roots, the `:ro` array bind, the mount gate, the `wget`
|
||||
health check, the base path the shipped SPA needs), `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, the deployer calling it only after the
|
||||
health gate, and the SSO arm sending the mounted base path as the redirect subfolder) and
|
||||
`tests/media_overlay.bats` (only [[Plex]] and [[Jellyfin]] may reference the metadata view).
|
||||
|
||||
Five book folders hold media no scanner can read, so they stay out of the library:
|
||||
three 2024 Nebula nominees in the `_unsorted` box (two `.m4b` with no `moov` atom,
|
||||
|
||||
Reference in new issue
Block a user