Headless Libation on nas: auto-download purchased Audible books #360

Closed
opened 2026-08-16 00:07:24 +02:00 by dominik.polakovics · 0 comments

Run Libation headless on the nas host to automatically download newly purchased Audible audiobooks to /var/lib/multimedia/audiobooks/Libation/ in an Audiobookshelf-friendly layout.

Decisions (grilled 2026-08-16)

  • Opportunistic sync only — no scheduled WOL from fw. Scan+liberate on boot and periodically while the NAS happens to be up. Accepted consequence: a purchase may sit undownloaded until something else wakes the NAS.
  • Auto-shutdown must not fire mid-sync. The existing open-fd check on /var/lib/multimedia in hosts/nas/modules/auto-shutdown.nix does NOT cover Libation: it downloads/decrypts in a separate InProgress working dir and only moves the finished file into Books at the end.
  • Bootstrap headlessly via libationcli login-external (exists in v13.3.6: prints an Audible URL, user signs in from any browser, pastes the resulting URL back). No GUI seeding, no sops: Libation rewrites AccountsSettings.json as tokens refresh, so it is seeded-once mutable state, not a deployable secret.
  • ABS-friendly naming templates instead of Libation's default <title short> [<id>].

Implementation notes (researched, v13.3.6 in nixpkgs release-26.05)

  • Package libation (13.3.6) is in the channel the nas host tracks (nixos-26.05); provides libationcli. No overlay needed.
  • Config dir: set env LIBATION_FILES_DIR=/var/lib/libation (Libation only honors the var if the directory already exists). Settings.json, AccountsSettings.json, LibationContext.db all live there.
  • New module hosts/nas/modules/libation.nix:
    • user/group libation, extraGroups [ "jellyfin" ] (audiobooks dir is jellyfin:jellyfin 0775; audiobookshelf reads via jellyfin group).
    • Books dir: d /var/lib/multimedia/audiobooks/Libation 0775 libation jellyfin (tmpfiles).
    • State dir /var/lib/libation (StateDirectory=libation), InProgress on local disk via CacheDirectory=libation → /var/cache/libation (wiped on boot is fine, a half download just re-downloads).
    • Seed/overwrite Settings.json on each service start from a Nix-generated template (declarative; Libation regenerates missing keys). Keys: Books, InProgress, FolderTemplate, FileTemplate, AutoScan false (our timer drives it), DownloadEpisodes.
    • Verified template tags: FolderTemplate <first author>/<if series-><first series>/<-if series><title short>, FileTemplate <title> [<id>]. Invalid templates silently fall back to defaults, so the tag spelling must be exact.
    • libation-sync.service: oneshot, User=libation, HOME=/var/lib/libation, runs libationcli scan then libationcli liberate. Generous TimeoutStartSec (first liberate downloads the whole backlog; a wedged download must not hold the box up forever — ~24h cap).
    • Pre-bootstrap guard: before login has been performed, libationcli scan exits non-zero ("No accounts"). The sync script must exit 0 cleanly when /var/lib/libation/AccountsSettings.json does not exist yet, so the unit does not sit in a failed/retry loop between first deploy and the one-time login.
    • Timer: OnStartupSec ~3min (after network-online), OnUnitActiveSec ~30min.
  • auto-shutdown.nix — add check #5 for libation-sync, with different semantics than checks 1–4:
    • While libation-sync is running: log and exit 0 WITHOUT resetting the idle clock (same pattern as the pyload-cgroup-unreadable fail-safe). Do NOT use stay_active: a 30-min sync timer that resets the 60-min idle streak on every run would keep the NAS from EVER powering off. With skip-without-reset, an in-flight download still blocks poweroff, a no-op scan does not extend uptime, and after a long download finishes the box powers off at the next check.
    • NB: a running oneshot without RemainAfterExit reports ActiveState="activating", not "active" — check systemctl show -p ActiveState --value libation-sync.service against activating|active, not is-active --quiet.
    • Update the module header comment (currently lists 4 conditions).
  • configuration.nix: import the module; add /var/lib/libation to the impermanence persistence list (tokens + LibationContext.db must survive reboot, else full re-download).
  • Document the one-time bootstrap in the PR: sudo -u libation env LIBATION_FILES_DIR=/var/lib/libation HOME=/var/lib/libation libationcli login-external -a <email> -l <locale> over SSH after first deploy (start libation-sync once first so the state dir + Settings.json exist). A small root-only wrapper script for this env/user dance is optional but nice for future ops (set-status, export).

Closes-when: purchases appear under /var/lib/multimedia/audiobooks/Libation//… while the box is up, the NAS does not power off during an active sync, and the NAS still powers off after 60 min of real idle despite the periodic sync timer.

Run Libation headless on the nas host to automatically download newly purchased Audible audiobooks to /var/lib/multimedia/audiobooks/Libation/ in an Audiobookshelf-friendly layout. ## Decisions (grilled 2026-08-16) - **Opportunistic sync only** — no scheduled WOL from fw. Scan+liberate on boot and periodically while the NAS happens to be up. Accepted consequence: a purchase may sit undownloaded until something else wakes the NAS. - **Auto-shutdown must not fire mid-sync.** The existing open-fd check on /var/lib/multimedia in hosts/nas/modules/auto-shutdown.nix does NOT cover Libation: it downloads/decrypts in a separate InProgress working dir and only moves the finished file into Books at the end. - **Bootstrap headlessly via `libationcli login-external`** (exists in v13.3.6: prints an Audible URL, user signs in from any browser, pastes the resulting URL back). No GUI seeding, no sops: Libation rewrites AccountsSettings.json as tokens refresh, so it is seeded-once mutable state, not a deployable secret. - **ABS-friendly naming templates** instead of Libation's default `<title short> [<id>]`. ## Implementation notes (researched, v13.3.6 in nixpkgs release-26.05) - Package `libation` (13.3.6) is in the channel the nas host tracks (nixos-26.05); provides `libationcli`. No overlay needed. - Config dir: set env `LIBATION_FILES_DIR=/var/lib/libation` (Libation only honors the var if the directory already exists). Settings.json, AccountsSettings.json, LibationContext.db all live there. - New module hosts/nas/modules/libation.nix: - user/group `libation`, extraGroups [ "jellyfin" ] (audiobooks dir is jellyfin:jellyfin 0775; audiobookshelf reads via jellyfin group). - Books dir: `d /var/lib/multimedia/audiobooks/Libation 0775 libation jellyfin` (tmpfiles). - State dir /var/lib/libation (StateDirectory=libation), InProgress on local disk via CacheDirectory=libation → /var/cache/libation (wiped on boot is fine, a half download just re-downloads). - Seed/overwrite Settings.json on each service start from a Nix-generated template (declarative; Libation regenerates missing keys). Keys: `Books`, `InProgress`, `FolderTemplate`, `FileTemplate`, `AutoScan` false (our timer drives it), `DownloadEpisodes`. - Verified template tags: FolderTemplate `<first author>/<if series-><first series>/<-if series><title short>`, FileTemplate `<title> [<id>]`. Invalid templates silently fall back to defaults, so the tag spelling must be exact. - `libation-sync.service`: oneshot, User=libation, HOME=/var/lib/libation, runs `libationcli scan` then `libationcli liberate`. Generous TimeoutStartSec (first liberate downloads the whole backlog; a wedged download must not hold the box up forever — ~24h cap). - **Pre-bootstrap guard:** before login has been performed, `libationcli scan` exits non-zero ("No accounts"). The sync script must exit 0 cleanly when /var/lib/libation/AccountsSettings.json does not exist yet, so the unit does not sit in a failed/retry loop between first deploy and the one-time login. - Timer: OnStartupSec ~3min (after network-online), OnUnitActiveSec ~30min. - auto-shutdown.nix — add check #5 for libation-sync, **with different semantics than checks 1–4**: - While libation-sync is running: log and `exit 0` WITHOUT resetting the idle clock (same pattern as the pyload-cgroup-unreadable fail-safe). Do NOT use `stay_active`: a 30-min sync timer that resets the 60-min idle streak on every run would keep the NAS from EVER powering off. With skip-without-reset, an in-flight download still blocks poweroff, a no-op scan does not extend uptime, and after a long download finishes the box powers off at the next check. - NB: a running oneshot without RemainAfterExit reports ActiveState="activating", not "active" — check `systemctl show -p ActiveState --value libation-sync.service` against activating|active, not `is-active --quiet`. - Update the module header comment (currently lists 4 conditions). - configuration.nix: import the module; add `/var/lib/libation` to the impermanence persistence list (tokens + LibationContext.db must survive reboot, else full re-download). - Document the one-time bootstrap in the PR: `sudo -u libation env LIBATION_FILES_DIR=/var/lib/libation HOME=/var/lib/libation libationcli login-external -a <email> -l <locale>` over SSH after first deploy (start libation-sync once first so the state dir + Settings.json exist). A small root-only wrapper script for this env/user dance is optional but nice for future ops (set-status, export). Closes-when: purchases appear under /var/lib/multimedia/audiobooks/Libation/<Author>/… while the box is up, the NAS does not power off during an active sync, and the NAS still powers off after 60 min of real idle despite the periodic sync timer.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos#360
No description provided.