feat(media): the audiobook relayout, filed by the book Plex matched #101

Merged
dank merged 5 commits from feat/media-audiobook-relayout into main 2026-10-31 00:38:02 -07:00
Owner

What

The audiobook tree is the one library on Dankserver that Jellyfin does not read at
all and Plex reads completely — 58,975 chapters already matched into its
Artist/Album/Track shape, where the album row is the BOOK and that book's single
artist row is the NARRATOR. This lands the tool that re-shelves the tree by those
rows instead of by the collection folder a release pack made:

Audiobooks/<Narrator>/<Book> (<year>)/<the file keeps its own name>
File Role
scripts/media-census.py new audiobooks export: the chapters Plex matched under a root (host paths straight through, nothing outside the root, nothing Plex deleted)
scripts/media-audiobooks.py the planner: TSVs in, rename intents out, nothing touched
tests/media_audiobooks.bats 28 tests — the export's join against a fixture Plex DB, and the plan's output
serverAppConfigs-docs/Dankserver/Media Relayout.md the arm's section: shape, commands, refusals, the laws

Merging is the win: a book that shipped as … [Part 1]/… [Part 2] inside a pack
lands in the one folder Plex's own album row names, which is the property both
servers' book scanners assume. 182 book folders legitimately gather chapters from
more than one collection pack — the same novel sitting in the Hugo pack, the Nebula
pack and the Hugo-winners pack is one album to Plex already, so the disk now says
the same thing the database does.

The two laws that are the opposite of the music arm's

  • A chapter is never renumbered. Most books carry at least one chapter whose
    index Plex does not know, and playback order comes from the embedded track tag
    either way — so a number written into a name would be invented and would buy
    nothing. A wrong number in a name is the number a human later trusts.
  • The shelf is the narrator, not the author. Every book has exactly one artist
    row (verified: 0 books with none, 0 with more than one) and it is the narrator.
    The author survives only in the by <author> suffix of some album titles and in
    no column Plex stores — so deriving the author would mean parsing titles for one
    book in seven and inventing it for the rest.

Refusals

A video container, audio no chapter row names, and a loose file in a folder holding
two books park under _unsorted with the path they have today. Three cases the
music arm does not have, all found by running the planner against the real library
rather than the fixture:

  • A row whose album title is a tag-scan placeholder ([Unknown Album],
    Unknown album (03/03/2015 …), no title) names no book. 1,106 of those chapters
    came from 14 unrelated top-level folders: filing on the shared string would have
    merged books that have nothing to do with each other onto one shelf.
  • A row whose artist title is a placeholder ([Unknown Artist], <Unknown>) or a
    chapter number a ripper mistagged as the artist (0.4, 31 — 272 chapters)
    names no narrator. The test is "contains a letter", applied to the narrator only:
    a book title may honestly be 1984.
  • Various Artists is not a placeholder — it is what a multi-cast read carries,
    and the 1,189-chapter Bible under it is genuinely one book.

Evidence

  • tests/media_audiobooks.bats: 28/28.
  • Full ./tests/run.sh: EXIT=0, 736 tests, 0 failures.
  • Census of the real library: 58,975 chapter rows, 0 outside the root; 62,879 files,
    2.40 TiB.
  • Plan: 55,114 moves + 7,764 parked + 1 already placed = 62,879 files accounted for,
    6,671 folders, 1,096 narrator shelves, 0 placeholder- or number-named shelves.
  • media-apply.py apply in dry run against the real filesystem: 62,878 moves, 6,671
    folders, 0 refused targets, journal untouched.

Not in this PR

No rename has been applied. The apply runs after this merges, on its own journal
(/mnt/entropy/media-relayout-audiobooks.jsonl), after the music relayout finishes
so the array has one writer at a time; media-apply.py rollback --apply on that
journal is the rollback, and the Plex section gets rescanned from inside its
container afterwards.


cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb

## What The audiobook tree is the one library on Dankserver that Jellyfin does not read at all and Plex reads completely — 58,975 chapters already matched into its Artist/Album/Track shape, where the album row is the BOOK and that book's single artist row is the NARRATOR. This lands the tool that re-shelves the tree by those rows instead of by the collection folder a release pack made: ``` Audiobooks/<Narrator>/<Book> (<year>)/<the file keeps its own name> ``` | File | Role | |---|---| | `scripts/media-census.py` | new `audiobooks` export: the chapters Plex matched under a root (host paths straight through, nothing outside the root, nothing Plex deleted) | | `scripts/media-audiobooks.py` | the planner: TSVs in, rename intents out, nothing touched | | `tests/media_audiobooks.bats` | 28 tests — the export's join against a fixture Plex DB, and the plan's output | | `serverAppConfigs-docs/Dankserver/Media Relayout.md` | the arm's section: shape, commands, refusals, the laws | Merging is the win: a book that shipped as `… [Part 1]`/`… [Part 2]` inside a pack lands in the one folder Plex's own album row names, which is the property both servers' book scanners assume. 182 book folders legitimately gather chapters from more than one collection pack — the same novel sitting in the Hugo pack, the Nebula pack and the Hugo-winners pack is one album to Plex already, so the disk now says the same thing the database does. ## The two laws that are the opposite of the music arm's - **A chapter is never renumbered.** Most books carry at least one chapter whose index Plex does not know, and playback order comes from the embedded track tag either way — so a number written into a name would be invented and would buy nothing. A wrong number in a name is the number a human later trusts. - **The shelf is the narrator, not the author.** Every book has exactly one artist row (verified: 0 books with none, 0 with more than one) and it is the narrator. The author survives only in the ` by <author>` suffix of some album titles and in no column Plex stores — so deriving the author would mean parsing titles for one book in seven and inventing it for the rest. ## Refusals A video container, audio no chapter row names, and a loose file in a folder holding two books park under `_unsorted` with the path they have today. Three cases the music arm does not have, all found by running the planner against the real library rather than the fixture: - A row whose album title is a **tag-scan placeholder** (`[Unknown Album]`, `Unknown album (03/03/2015 …)`, `no title`) names no book. 1,106 of those chapters came from 14 unrelated top-level folders: filing on the shared string would have merged books that have nothing to do with each other onto one shelf. - A row whose artist title is a placeholder (`[Unknown Artist]`, `<Unknown>`) or a **chapter number a ripper mistagged as the artist** (`0.4`, `31` — 272 chapters) names no narrator. The test is "contains a letter", applied to the narrator only: a book title may honestly be `1984`. - `Various Artists` is **not** a placeholder — it is what a multi-cast read carries, and the 1,189-chapter Bible under it is genuinely one book. ## Evidence - `tests/media_audiobooks.bats`: 28/28. - Full `./tests/run.sh`: `EXIT=0`, 736 tests, 0 failures. - Census of the real library: 58,975 chapter rows, 0 outside the root; 62,879 files, 2.40 TiB. - Plan: 55,114 moves + 7,764 parked + 1 already placed = 62,879 files accounted for, 6,671 folders, 1,096 narrator shelves, 0 placeholder- or number-named shelves. - `media-apply.py apply` in dry run against the real filesystem: 62,878 moves, 6,671 folders, **0 refused** targets, journal untouched. ## Not in this PR No rename has been applied. The apply runs after this merges, on its own journal (`/mnt/entropy/media-relayout-audiobooks.jsonl`), after the music relayout finishes so the array has one writer at a time; `media-apply.py rollback --apply` on that journal is the rollback, and the Plex section gets rescanned from inside its container afterwards. --- cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
dank added 5 commits 2026-10-31 00:37:47 -07:00
The audiobook tree is the one library Jellyfin does not read at all and Plex
reads completely: 58,975 chapters matched into Artist/Album/Track, where the
album is the BOOK and the book's single artist row is the NARRATOR. That is the
authority the new planner files by, never the collection folder a pack made.

  Audiobooks/<Narrator>/<Book> (<year>)/<the file's own name>

Two laws are the opposite of the music tool's, both forced by the rows: files
are never renumbered (72% of the books carry chapters with no usable index, and
playback order comes from the embedded track tag either way, so an invented
number can only lie), and the shelf is the narrator, not the author (every book
has exactly one artist row; the author survives in 10% of album titles and in no
column Plex stores, so one rule beats a shelf half authors and half narrators).

media-census.py gains the audiobooks export: the chapters Plex matched under a
root, host paths straight through, nothing outside the root and nothing Plex
deleted. 22 tests in tests/media_audiobooks.bats pin the export's join and the
plan's output.
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.
The linter caps a page at 500 lines and a description at 120 chars: the section
lost four lines of prose (no law went with them) and the description lost its
restatement of the format names. gen-vault-index.py regenerated the locator row.
The first real plan filed 1,106 chapters from 14 unrelated top-level folders
onto one shelf, because all 1,106 carry the album tag [Unknown Album] and the
same artist tag. Plex's album title is the authority for the shelf only when it
names a book; a placeholder names nothing, and unrelated books share it. Those
chapters now park with the path they have today, timestamp-glued variants
included; Various Artists stays a legitimate multi-cast narrator.
Plex's artist row for 272 chapters is a chapter number the ripper put in the
artist tag (0.4, 31, 05), which would have filed them under a shelf called 0.4.
A narrator is a person, so it must contain a letter; the same test is deliberately
not applied to a book title, which may honestly be 1984.
dank merged commit 3613f69593 into main 2026-10-31 00:38:02 -07:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/serverAppConfigs#101