Compare commits

...
Author SHA1 Message Date
Frank 03e0d84380 docs(audiobookshelf): the phone app's connection facts and where its token lives
The page told an agent how to sign in from a browser and nothing about the phone.
Measured 2026-11-03 on the deployed service:

- Server Address `http://192.168.0.235:8000`, user `frank`, plain HTTP, no base-path
  prefix; `/healthcheck` and `/ping` answer 200 from a second host on the LAN and
  `iptables -S INPUT` has no rule for 8000, so no phone-side setup is needed.
  `download: true` on the root account is what enables offline listening.
- `POST /login` puts the bearer at `user.token`, not top level. Reading
  `response.token` yields "", the client sends `Authorization: Bearer ` with nothing
  after it, and every `/api/*` answers 401 as though the password were wrong — which
  it was not. That is the failure I hit inside five minutes of scripting this.
- The LAN-only law gains its consequence for clients: the Anubis + Authentik chain
  serves browsers, so guarding a vhost does not make the phone app work remotely.
  Remote listening wants a VPN into the LAN; the LAN address is then still correct.

bats: 822 seams green.
2026-11-02 19:09:23 -08:00
dank 988c4899d4 Merge pull request 'docs(media): point plan/journal/park paths at .media-campaign and NeedsProcessing' (#125) from docs/media-artifact-paths into main 2026-11-02 18:42:34 -08:00
Frank 77e9973ff5 docs(media): point plan/journal/park paths at .media-campaign and NeedsProcessing
The campaign-artifact home moved (42bc482) but the copy-paste surfaces still
named the old ones, and a tool's usage block is exactly what an agent runs:

- scripts/media-shows.py, media-tv.py, media-tvdupes.py: usage examples wrote
  plans and journals at the array root. media-apply.py defaults its journal to
  <root>/media-relayout.jsonl, so any run copied from those lines re-creates
  the root-level clutter the vault now forbids. Each block now names
  /mnt/entropy/.media-campaign/ and says why --journal is explicit.
- media-shows.py: --park named the retired media-park; the pile is
  /mnt/entropy/NeedsProcessing/TVShows.
- Jellyfin.md: library-coverage law named /data/media-park; names the pile by
  its current path and records when it moved.
- Media TV Relayout.md: the runnable --park path named media-park.

bats: 822 seams green.
2026-11-02 18:02:48 -08:00
Frank 625263dd48 Merge remote-tracking branch 'origin/main' into feat/jellyfin-provider-keys 2026-11-02 17:44:49 -08:00
Frank 42bc482ed8 docs(media): campaign artifacts live in /mnt/entropy/.media-campaign
The array root carried 48 planning artifacts (journals, plans, run logs)
accumulated by the relayout campaigns since 2026-11-02. They sit beside the
libraries now, which is how a media server ends up indexing a journal.

Moved all 48 (219.8 MiB) into the dot-directory `/mnt/entropy/.media-campaign`
with a journaled `media-apply` run (plan, apply, verify — 48 verified at their
targets, 0 root files left). Jellyfin and Plex ignore dot-directories, and the
overlay view exposes the array root to both servers, so the same pollution
existed on the view side.

Every vault page that named a root-level journal path is rewritten to the new
home (26 occurrences across 6 pages), and the law is stated once, in
[[Media Relayout]], which owns the tool chain: `media-apply.py` defaults its
journal to `media-relayout.jsonl` beside `--root`, so a run that omits
`--journal` recreates exactly the root-level log this now forbids.
2026-11-02 17:44:49 -08:00
dank aea24353ad Merge pull request 'docs(jellyfin): the switches live under LibraryOptions; a pinned art pass is not a provider census' (#124) from feat/jellyfin-provider-keys into main 2026-11-02 17:39:21 -08:00
10 changed files with 93 additions and 48 deletions

No files matched your search

+10 -4
View File
@@ -78,10 +78,16 @@ one journal is one undo:
python3 scripts/media-shows.py --root /mnt/entropy/TVShows \
--enum /tmp/tv-enum.tsv --titles /tmp/jellyfin-tv.tsv \
--tvdb --tmdb --out /mnt/entropy/media-tv-plan.tsv \
--park /mnt/entropy/media-park/TVShows
python3 scripts/media-apply.py apply --plan /mnt/entropy/media-tv-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/media-tv-relayout.jsonl
--tvdb --tmdb --out /mnt/entropy/.media-campaign/media-tv-plan.tsv \
--park /mnt/entropy/NeedsProcessing/TVShows
python3 scripts/media-apply.py apply \
--plan /mnt/entropy/.media-campaign/media-tv-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/.media-campaign/media-tv-relayout.jsonl
Every plan, journal and log lives in /mnt/entropy/.media-campaign/ — the array
root holds libraries only. media-apply.py defaults its journal to
<root>/media-relayout.jsonl, so pass --journal explicitly or the run puts the
log back on the root.
"""
import argparse
import collections
+13 -6
View File
@@ -17,13 +17,20 @@ the executor's own path guard still has teeth, and give it its own journal —
one journal is one undo, and a TV move must not be welded to a movie relayout:
python3 scripts/media-tv.py --root /mnt/entropy/Movies \
--library /mnt/entropy/TVShows --out /mnt/entropy/media-tv-plan.tsv
python3 scripts/media-apply.py apply --plan /mnt/entropy/media-tv-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/media-relayout-tv.jsonl
python3 scripts/media-apply.py verify --plan /mnt/entropy/media-tv-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/media-relayout-tv.jsonl
--library /mnt/entropy/TVShows \
--out /mnt/entropy/.media-campaign/media-tv-plan.tsv
python3 scripts/media-apply.py apply \
--plan /mnt/entropy/.media-campaign/media-tv-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/.media-campaign/media-relayout-tv.jsonl
python3 scripts/media-apply.py verify \
--plan /mnt/entropy/.media-campaign/media-tv-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/.media-campaign/media-relayout-tv.jsonl
python3 scripts/media-apply.py sweep --root /mnt/entropy \
--journal /mnt/entropy/media-relayout-tv.jsonl --apply
--journal /mnt/entropy/.media-campaign/media-relayout-tv.jsonl --apply
Plans and journals live in `/mnt/entropy/.media-campaign/`, never loose on the
array root: `media-apply.py` defaults its journal to `<root>/media-relayout.jsonl`,
so `--journal` is always explicit here.
Guards, and why each one is here:
+8 -6
View File
@@ -16,14 +16,16 @@ the journal and frees no space until the hold is purged.
python3 scripts/media-tvdupes.py --library /mnt/entropy/TVShows \
--hold /mnt/entropy/.AGENTTRASH/20261102-tv-duplicates \
--out /mnt/entropy/tv-dupes-plan.tsv
python3 scripts/media-apply.py apply --plan /mnt/entropy/tv-dupes-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/tv-dupes.jsonl \
--out /mnt/entropy/.media-campaign/tv-dupes-plan.tsv
python3 scripts/media-apply.py apply \
--plan /mnt/entropy/.media-campaign/tv-dupes-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/.media-campaign/tv-dupes.jsonl \
--metadata-view /var/lib/media-meta/view --apply
python3 scripts/media-apply.py verify --plan /mnt/entropy/tv-dupes-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/tv-dupes.jsonl
python3 scripts/media-apply.py verify \
--plan /mnt/entropy/.media-campaign/tv-dupes-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/.media-campaign/tv-dupes.jsonl
python3 scripts/media-apply.py sweep --root /mnt/entropy \
--journal /mnt/entropy/tv-dupes.jsonl --apply # empty folders only
--journal /mnt/entropy/.media-campaign/tv-dupes.jsonl --apply # empty folders only
scripts/media-view.sh reap # ghost sidecars above
Then both servers have to be told: the plan moves files on the raw array, and no
@@ -36,7 +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` |
| 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-relayout-audiobooks.jsonl` |
| 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
@@ -65,6 +65,22 @@ 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.
## 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`
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 —
see the LAN-only law, and note that the guard chain is not the remedy there.
**The login token is at `user.token`, not top level.** `POST /login` answers
`{ user, serverSettings, ereaderDevices, Source, userDefaultLibraryId }` and the bearer sits
inside the user object. A client that reads `response.token` gets the empty string, sends
`Authorization: Bearer ` with nothing after it, and every `/api/*` call then answers `401`
as though the password were wrong.
## Laws
- **Speak the image's env names, not the README's.** The server reads `PORT`,
@@ -108,7 +124,10 @@ the file, not the bind: the same line count grows while the total climbs.
- **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`).
policy in `AGENTS.md`). That chain serves browsers: the phone app is a non-browser
client and cannot solve a challenge, so guarding a vhost does not make remote
listening work — remote listening wants a VPN into the LAN, after which the LAN
address in *Phone app* is correct unchanged.
- **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
+3 -3
View File
@@ -58,9 +58,9 @@ reads — movie art has never existed beside the media on either layer.
**Library coverage is the first thing to check when a title is missing.** The
four libraries are `movies → /data/Movies`, `tvshows → /data/TVShows`,
`music → /data/Music`, `boxsets → /config/data/collections`
(`GET /Library/VirtualFolders`). `/data/media-park` is in **none** of them, so
everything filed under it — the `_unsorted` release bundles and the `extras/`
folders the relayout wrote there — is invisible to Jellyfin by configuration,
(`GET /Library/VirtualFolders`). The review pile — `/data/NeedsProcessing`, the
former `media-park`, drained into it 2026-11-02 — is in **none** of them, so
everything filed there is invisible to Jellyfin by configuration,
not by fault: a recursive refresh answers 204, burns CPU for minutes and
indexes nothing, because it never looks there. Check coverage before re-scanning.
@@ -41,7 +41,7 @@ python3 scripts/media-audiobooks.py --root /mnt/entropy/Audiobooks \
# library too ([[Audiobookshelf]]) — otherwise it keeps rows for folders that
# no longer exist.
scripts/media-apply.py apply --plan /tmp/ab_plan.tsv --root /mnt/entropy \
--journal /mnt/entropy/media-relayout-audiobooks.jsonl --apply
--journal /mnt/entropy/.media-campaign/media-relayout-audiobooks.jsonl --apply
```
The group is the identity Plex asserts in its own database — **(narrator, book,
@@ -68,7 +68,7 @@ no chapter row names, and a loose file in a folder holding two books all park un
that unrelated books share, or a chapter number mistagged as the artist (`0.4`).
`Various Artists` is not a placeholder — that is a multi-cast read.
The reviewed plan lives at `/mnt/entropy/media-relayout-audiobooks-plan.tsv`; it
The reviewed plan lives at `/mnt/entropy/.media-campaign/media-relayout-audiobooks-plan.tsv`; it
applies on its own journal, behind the music writer, and then refreshes the Plex
section. Verify it with `--db-kind plex` — Jellyfin holds no rows here, so a
Jellyfin pass proves nothing either way.
@@ -65,8 +65,8 @@ executor's own format; it never moves anything itself.
python3 scripts/media-tvdupes.py --library /mnt/entropy/TVShows \
--hold /mnt/entropy/.AGENTTRASH/<date>-tv-duplicates \
--meta /var/lib/media-meta/view/TVShows --witness witness.tsv \
--ids /mnt/entropy/tv-tvdb-ids.tsv \
--out /mnt/entropy/tv-dupes-plan.tsv --json /tmp/tv-groups.json
--ids /mnt/entropy/.media-campaign/tv-tvdb-ids.tsv \
--out /mnt/entropy/.media-campaign/tv-dupes-plan.tsv --json /tmp/tv-groups.json
# then media-apply.py apply / verify / sweep / rollback, exactly as for a film prune
```
@@ -76,7 +76,7 @@ python3 scripts/media-tvdupes.py --library /mnt/entropy/TVShows \
match, not a lookup: `BaseItems` (`Type like '%TV.Series'`) joined to
`BaseItemProviders` where `ProviderId = 'Tvdb'`, the `/data/TVShows/` prefix
stripped to the bare top-level folder name — 616 folders, none carrying two
ids, written to `/mnt/entropy/tv-tvdb-ids.tsv`. Two folders with disjoint ids
ids, written to `/mnt/entropy/.media-campaign/tv-tvdb-ids.tsv`. Two folders with disjoint ids
are two shows however alike they read — *Avatar: The Last Airbender* (1999)
beside the 2024 remake, *Millennium* (1996) beside a German thriller Jellyfin
had also titled *Millennium* — and a pair whose identity cannot be proven is
@@ -158,12 +158,12 @@ Nothing is deleted. Every doomed copy moves into a dated hold with the same
executor the relayout uses, so any file comes back:
```bash
scripts/media-apply.py apply --plan /mnt/entropy/dup-prune-similar-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/dup-prune-similar.jsonl # dry
scripts/media-apply.py apply --plan /mnt/entropy/.media-campaign/dup-prune-similar-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/.media-campaign/dup-prune-similar.jsonl # dry
scripts/media-apply.py apply --plan … --root /mnt/entropy --journal … --apply
scripts/media-apply.py verify --plan … --journal /mnt/entropy/dup-prune-similar.jsonl
scripts/media-apply.py sweep --journal /mnt/entropy/dup-prune-similar.jsonl --root /mnt/entropy --apply
scripts/media-apply.py rollback --journal /mnt/entropy/dup-prune-similar.jsonl --root /mnt/entropy --apply
scripts/media-apply.py verify --plan … --journal /mnt/entropy/.media-campaign/dup-prune-similar.jsonl
scripts/media-apply.py sweep --journal /mnt/entropy/.media-campaign/dup-prune-similar.jsonl --root /mnt/entropy --apply
scripts/media-apply.py rollback --journal /mnt/entropy/.media-campaign/dup-prune-similar.jsonl --root /mnt/entropy --apply
```
The plan rows are the tool's own format (`action src dst kind`, `mkdir` rows for
@@ -33,7 +33,7 @@ python3 scripts/media-music.py --root /mnt/entropy/Music --enum /tmp/music_enum.
# 4. Read the exceptions file FIRST — it is the review, not a log. Then the same
# executor, journal and rollback as [[Media Relayout]] shows, on its own journal.
scripts/media-apply.py apply --plan /tmp/music_plan.tsv --root /mnt/entropy \
--journal /mnt/entropy/media-relayout-music.jsonl --apply
--journal /mnt/entropy/.media-campaign/media-relayout-music.jsonl --apply
```
The group is what the tags assert, not where the files sit: **(album-artist tag,
@@ -71,12 +71,23 @@ that would be filed wrong, and the plan is not run until it is understood.
```bash
scripts/media-apply.py apply --plan /tmp/jf-plan.tsv --root /mnt/entropy/Movies # dry, writes nothing
scripts/media-apply.py apply --plan /tmp/jf-plan.tsv --root /mnt/entropy/Movies --apply
scripts/media-apply.py verify --plan /tmp/jf-plan.tsv --journal /mnt/entropy/media-relayout.jsonl
scripts/media-apply.py sweep --journal /mnt/entropy/media-relayout.jsonl --root /mnt/entropy/Movies # dry
scripts/media-apply.py sweep --journal /mnt/entropy/media-relayout.jsonl --root /mnt/entropy/Movies --apply
scripts/media-apply.py rollback --journal /mnt/entropy/media-relayout.jsonl --root /mnt/entropy/Movies --apply
scripts/media-apply.py verify --plan /tmp/jf-plan.tsv --journal /mnt/entropy/.media-campaign/media-relayout.jsonl
scripts/media-apply.py sweep --journal /mnt/entropy/.media-campaign/media-relayout.jsonl --root /mnt/entropy/Movies # dry
scripts/media-apply.py sweep --journal /mnt/entropy/.media-campaign/media-relayout.jsonl --root /mnt/entropy/Movies --apply
scripts/media-apply.py rollback --journal /mnt/entropy/.media-campaign/media-relayout.jsonl --root /mnt/entropy/Movies --apply
```
**Every campaign artifact — plan, journal, exceptions file, run log — lives in
`/mnt/entropy/.media-campaign/`.** The array root carries libraries and nothing
else: a root strewn with `*.jsonl` and `*-plan.tsv` gets browsed as content, and
the dot-directory is ignored by both media servers. Relocated 2026-11-02, 48 files,
undo with `media-apply.py rollback --journal /mnt/entropy/.media-campaign/media-campaign-relocate.jsonl --root /mnt/entropy --apply`.
The holds and trash (`.AGENTTRASH`, `.HOLD-<date>-*`) stay at the root on purpose:
the purge machinery is given those paths as `--hold`, and a hold that moves out
from under the tool that made it is a hold nobody can find. Name `--journal`
explicitly on every run — the tool's default is `media-relayout.jsonl` beside
`--root`, so a bare run recreates exactly the root-level log this page forbids.
An apply also leaves the release folders it emptied standing where they were —
hundreds of them at the library root — which is what `sweep` removes, dry-run
first, from the same journal.
@@ -110,7 +121,7 @@ a read beside a running scan can fail as "database disk image is malformed".
`immutable` stays only the fallback for a database whose `-shm` cannot be read.
```bash
scripts/media-verify.py --journal /mnt/entropy/media-relayout-music.jsonl \
scripts/media-verify.py --journal /mnt/entropy/.media-campaign/media-relayout-music.jsonl \
--db-kind jellyfin --ignore-ext jpg,jpeg,png,log,cue,toc,nfo,txt,m3u,accurip
```
@@ -150,11 +161,11 @@ by series, and writes a plan in the executor's own format:
```bash
scripts/media-tv.py --root /mnt/entropy/Movies --library /mnt/entropy/TVShows \
--out /mnt/entropy/media-tv-plan.tsv
scripts/media-apply.py apply --plan /mnt/entropy/media-tv-plan.tsv --root /mnt/entropy
scripts/media-apply.py apply --plan /mnt/entropy/media-tv-plan.tsv --root /mnt/entropy \
--journal /mnt/entropy/media-relayout-tv.jsonl --apply
scripts/media-apply.py sweep --journal /mnt/entropy/media-relayout-tv.jsonl \
--out /mnt/entropy/.media-campaign/media-tv-plan.tsv
scripts/media-apply.py apply --plan /mnt/entropy/.media-campaign/media-tv-plan.tsv --root /mnt/entropy
scripts/media-apply.py apply --plan /mnt/entropy/.media-campaign/media-tv-plan.tsv --root /mnt/entropy \
--journal /mnt/entropy/.media-campaign/media-relayout-tv.jsonl --apply
scripts/media-apply.py sweep --journal /mnt/entropy/.media-campaign/media-relayout-tv.jsonl \
--root /mnt/entropy --apply
```
@@ -51,16 +51,16 @@ python3 scripts/media-census.py probe --root /mnt/entropy/TVShows \
# against scp'd TSVs — which is where the two provider secrets live.
python3 scripts/media-shows.py --root /mnt/entropy/TVShows --enum /tmp/tv_enum.tsv \
--titles /tmp/tv_titles.tsv --probed /tmp/tv_probed.tsv \
--tmdb-cache /mnt/entropy/media-tv-names.json --tmdb --tvdb \
--park /mnt/entropy/media-park/TVShows \
--out /mnt/entropy/media-relayout-shows2-plan.tsv \
--exceptions /mnt/entropy/media-relayout-shows2-exceptions.txt
--tmdb-cache /mnt/entropy/.media-campaign/media-tv-names.json --tmdb --tvdb \
--park /mnt/entropy/NeedsProcessing/TVShows \
--out /mnt/entropy/.media-campaign/media-relayout-shows2-plan.tsv \
--exceptions /mnt/entropy/.media-campaign/media-relayout-shows2-exceptions.txt
# 4. Read the exceptions file FIRST — it is the review, not a log. Same executor
# as above, but on this arm's OWN journal, NAMED PER PASS: the journal of an
# applied pass is its unwind record, so a second pass that reuses the name
# destroys the only map back.
scripts/media-apply.py apply --plan /mnt/entropy/media-relayout-shows2-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/media-relayout-shows2.jsonl --apply
scripts/media-apply.py apply --plan /mnt/entropy/.media-campaign/media-relayout-shows2-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/.media-campaign/media-relayout-shows2.jsonl --apply
```
Evidence, strongest first — the order the planner believes things: