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
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.
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.
## 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
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 main2026-10-31 00:38:02 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
scripts/media-census.pyaudiobooksexport: the chapters Plex matched under a root (host paths straight through, nothing outside the root, nothing Plex deleted)scripts/media-audiobooks.pytests/media_audiobooks.batsserverAppConfigs-docs/Dankserver/Media Relayout.mdMerging is the win: a book that shipped as
… [Part 1]/… [Part 2]inside a packlands 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
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.
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 inno 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
_unsortedwith the path they have today. Three cases themusic arm does not have, all found by running the planner against the real library
rather than the fixture:
[Unknown Album],Unknown album (03/03/2015 …),no title) names no book. 1,106 of those chapterscame 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.
[Unknown Artist],<Unknown>) or achapter 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 Artistsis 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../tests/run.sh:EXIT=0, 736 tests, 0 failures.2.40 TiB.
6,671 folders, 1,096 narrator shelves, 0 placeholder- or number-named shelves.
media-apply.py applyin dry run against the real filesystem: 62,878 moves, 6,671folders, 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 finishesso the array has one writer at a time;
media-apply.py rollback --applyon thatjournal 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