feat(media): the audiobook relayout, filed by the book Plex matched #101
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user