docs(media): the audiobook arm, and the music plan's applied state

One page owns the relayout facts: the new section carries the Plex-only reader,
the shape, the four commands, and the two laws a future operator would otherwise
re-litigate (never renumber a chapter; the shelf is the narrator). The music
section said its plan was NOT applied — it was applied on 2026-10-31, and a stale
line in a runbook gets believed, so it now names the journal and the rollback.
This commit is contained in:
dank committed 2026-10-31 00:24:50 -07:00
1 parent a7ceda95fd
commit ceb5617a42
1 file changed
+72 -10
@@ -1,7 +1,7 @@
--- ---
type: reference type: reference
title: Media Relayout title: Media Relayout
description: "Census, plan and apply the movie and TV libraries into Jellyfin format: gates, evidence chain, refusals, rollback." description: "Census, plan and apply the movie, TV, music and audiobook libraries into their media servers' format: gates, evidence chain, refusals, rollback."
tags: [dankserver, media-server, jellyfin, plex, procedure] tags: [dankserver, media-server, jellyfin, plex, procedure]
timestamp: 2026-10-30T00:00:00Z timestamp: 2026-10-30T00:00:00Z
--- ---
@@ -12,20 +12,23 @@ The movie library on [[Entropy Volume]] arrived as release folders —
`Some.Movie.2019.1080p.WiKiTaLx.x265-FOO/` with the video inside named after `Some.Movie.2019.1080p.WiKiTaLx.x265-FOO/` with the video inside named after
the release. [[Jellyfin]] matches a movie by folder and file name, so the the release. [[Jellyfin]] matches a movie by folder and file name, so the
release noise cost posters, extras and versions. These scripts turn that release noise cost posters, extras and versions. These scripts turn that
library into Jellyfin's own shape — the movies, the TV packs, the TV that library into the shape its media server reads — the movies, the TV packs, the TV
turns up in the movie share, and the music library — and nothing else in the repo touches a media file. that turns up in the movie share, the music library, and the audiobooks, whose
only reader is Plex — and nothing else in the repo touches a media file.
| Script | Touches media? | Output | | Script | Touches media? | Output |
|---|---|---| |---|---|---|
| `scripts/media-census.py` | reads headers only | `enum`, `probed`, `plex` TSVs | | `scripts/media-census.py` | reads headers and the servers' own DBs, never writes | `enum`, `probed`, and one authority TSV per library |
| `scripts/media-plan.py` | no — TSVs in, TSVs out | the plan + the exception report | | `scripts/media-plan.py` | no — TSVs in, TSVs out | the plan + the exception report |
| `scripts/media-apply.py` | **renames, never copies**; sweeps only what a run emptied | the moves, plus a journal | | `scripts/media-apply.py` | **renames, never copies**; sweeps only what a run emptied | the moves, plus a journal |
| `scripts/media-tv.py` | no — TSV out | a plan that moves TV into the TV library | | `scripts/media-tv.py` | no — TSV out | a plan that moves TV into the TV library |
| `scripts/media-shows.py` | no — TSV out | a plan that re-files the TV library itself | | `scripts/media-shows.py` | no — TSV out | a plan that re-files the TV library itself |
| `scripts/media-music.py` | no — TSV out | a plan that re-files the music library by its tags | | `scripts/media-music.py` | no — TSV out | a plan that re-files the music library by its tags |
| `scripts/media-audiobooks.py` | no — TSV out | a plan that re-files the audiobooks by the book Plex matched |
The seams are `tests/media_relayout.bats`, `tests/media_shows.bats` and The seams are `tests/media_relayout.bats`, `tests/media_shows.bats`,
`tests/media_music.bats`; the target contract is quoted from `tests/media_music.bats` and `tests/media_audiobooks.bats`; the movie and TV
target contracts are quoted from
[jellyfin.org/docs/general/server/media/movies](https://jellyfin.org/docs/general/server/media/movies) [jellyfin.org/docs/general/server/media/movies](https://jellyfin.org/docs/general/server/media/movies)
in `media-plan.py`'s docstring. in `media-plan.py`'s docstring.
@@ -292,10 +295,69 @@ What it refuses:
rename to `.m4a` would claim it is; **audio no provider row names parks** too, rename to `.m4a` would claim it is; **audio no provider row names parks** too,
because unmatched audio is a tagging job, not a renaming job. because unmatched audio is a tagging job, not a renaming job.
The 2026-10-30 dry run files 114k tracks and parks 6.8k — mostly sidecars The plan was applied on 2026-10-31; its journal is
sitting in a folder that holds two albums, plus audio no provider row names. The `/mnt/entropy/media-relayout-music.jsonl` and its rollback is that journal run
plan is NOT applied: the exceptions file is the review, and it is Frank's to through `media-apply.py rollback --apply`. Whatever the tags could not name sits
read. in `Music/_unsorted`, keeping the relative path it had, for a human to review:
the pile is mostly sidecars in a folder holding two albums, plus audio no
provider row names, which is a tagging job and not a renaming job.
## The audiobook library's own relayout
`/mnt/entropy/Audiobooks` is the one library here that [[Jellyfin]] does not read
at all — its music scanner has no audiobook concept — while [[Plex]] indexes all
of it as Artist → Album → Track, where the album is the BOOK, that book's single
artist is the NARRATOR, and both were matched out of the files' own tags.
`scripts/media-audiobooks.py` re-files the tree so the property both servers'
book scanners assume is true —
Audiobooks/<Narrator>/<Book> (<year>)/<the file keeps its own name>
Today it is not: the top level is a mix of author-named shelves and collection
packs, and a pack's book is split across `… [Part 1]`, `… [Part 2]` folders three
or four deep, so a scan shows three quarters of a novel.
```bash
# 1. The authority: Plex's chapter rows for this root (the DB opens read-only).
python3 scripts/media-census.py audiobooks --root /mnt/entropy/Audiobooks \
--out /tmp/ab_rows.tsv
# 2. What is on disk (stat only).
python3 scripts/media-census.py enumerate --root /mnt/entropy/Audiobooks \
--out /tmp/ab_enum.tsv
# 3. The plan.
python3 scripts/media-audiobooks.py --root /mnt/entropy/Audiobooks \
--enum /tmp/ab_enum.tsv --rows /tmp/ab_rows.tsv --out /tmp/ab_plan.tsv \
--exceptions /tmp/ab_exceptions.txt
# 4. Read the exceptions file FIRST. Then the same executor, journal and rollback
# as above, on its own journal, and refresh the Plex section as in step 5 of
# the movie Procedure — from inside the container, never over the host port.
scripts/media-apply.py apply --plan /tmp/ab_plan.tsv --root /mnt/entropy \
--journal /mnt/entropy/media-relayout-audiobooks.jsonl --apply
```
The group is the identity Plex asserts in its own database — **(narrator, book,
year)** — so one book arriving in four part folders becomes one folder, and a
1965 and a 2020 recording that share a title stay two books.
Two laws here are the opposite of the music arm's, and both are forced by what
the rows actually hold:
+- **A chapter is never renumbered.** Most books contain chapters whose index Plex
does not know, so a number written into a name would be invented — and it would
buy nothing, because both servers order playback by the embedded track tag, not
by the file name. A wrong number in a name is the number a human later trusts.
Fix order in the tags: a Plex scan re-reads tags and never renames a file.
+- **The shelf is the narrator, not the author.** Every book has exactly one artist
row and it is the narrator; the author survives only inside some album titles
(`Kryptos by Graham McNeill`) and in no column Plex stores, so deriving it would
mean parsing titles where a ripper happened to write one and inventing it
everywhere else. One rule that always resolves beats a shelf that is half author
and half narrator, and the author stays visible in the folder name Plex shows.
What it refuses, same list as the music arm with one addition: a video container,
audio no chapter row names, and a loose file in a folder holding two books all
park under `_unsorted` with the path they have today — and so does a chapter whose
row has **no book or no narrator**, because inventing a shelf is filing it wrong.
## Laws ## Laws