From a5459b0b1e20bbd8d75306eeb277c2eec2bf5a5a Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 30 Oct 2026 23:58:34 -0700 Subject: [PATCH] docs(media): the music library's own relayout, with the laws it earned --- .../Dankserver/Media Relayout.md | 80 ++++++++++++++++++- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/serverAppConfigs-docs/Dankserver/Media Relayout.md b/serverAppConfigs-docs/Dankserver/Media Relayout.md index d71bca6..00f2ecd 100644 --- a/serverAppConfigs-docs/Dankserver/Media Relayout.md +++ b/serverAppConfigs-docs/Dankserver/Media Relayout.md @@ -12,8 +12,8 @@ The movie library on [[Entropy Volume]] arrived as release folders — `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 release noise cost posters, extras and versions. These scripts turn that -library into Jellyfin's own shape — the movies, the TV packs, and the TV that -turns up in the movie share — and nothing else in the repo touches a media file. +library into Jellyfin's own shape — the movies, the TV packs, the TV that +turns up in the movie share, and the music library — and nothing else in the repo touches a media file. | Script | Touches media? | Output | |---|---|---| @@ -22,8 +22,10 @@ turns up in the movie share — and nothing else in the repo touches a media fil | `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-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 | -The seams are `tests/media_relayout.bats` and `tests/media_shows.bats`; the target contract is quoted from +The seams are `tests/media_relayout.bats`, `tests/media_shows.bats` and +`tests/media_music.bats`; the target contract is quoted from [jellyfin.org/docs/general/server/media/movies](https://jellyfin.org/docs/general/server/media/movies) in `media-plan.py`'s docstring. @@ -214,6 +216,63 @@ filename. Renaming a show is Jellyfin's work — a folder rename never touches i provider ids — so the mover keeps the name it is handed and strips only the pack index a release leads with (`4a. Seed DESTINY` → `Seed DESTINY`). +## The music library's own relayout + +`/mnt/entropy/Music` is rips and discography packs, and Jellyfin has matched and +tagged almost all of it. `scripts/media-music.py` re-files it into the shape +Jellyfin's music scanner reads — + + Music// ()/NN - .ext + +— and the authority is the tag row of the track, never the name the ripper gave +the file. + +```bash +# 1. The authority: Jellyfin's audio rows, one per track, with the album and +# artist ids its own database carries (the DB opens read-only). +python3 scripts/media-census.py music --root /mnt/entropy/Music \ + --out /tmp/music_rows.tsv +# 2. What is on disk (stat only). +python3 scripts/media-census.py enumerate --root /mnt/entropy/Music \ + --out /tmp/music_enum.tsv +# 3. The plan. +python3 scripts/media-music.py --root /mnt/entropy/Music --enum /tmp/music_enum.tsv \ + --rows /tmp/music_rows.tsv --out /tmp/music_plan.tsv \ + --exceptions /tmp/music_exceptions.txt +# 4. Read the exceptions file FIRST — it is the review, not a log. Then the same +# executor, journal and rollback as above, 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 +``` + +The group is what the tags assert, not where the files sit: **(album-artist tag, +album, year)**. One album arrives as three disc folders and as two copies of one +rip, and one MusicBrainz release group is one album. A compilation is one album +whose tracks name many performers, so it files once under `Various Artists`: +keying on the performer would file one album once per artist. Two different +album-artist tags on one album name are two albums, which is what Jellyfin's two +album items say. + +What it refuses: + ++- **A sidecar is placed by the audio around it, never by its own name.** A + `.lrc`, `.txt` or picture that shares a track's stem follows that track and + takes the name the track now has, or Jellyfin cannot find it; anything else + goes to the album its own folder was placed in, or to the folder whose discs + sit one level below it. A folder whose audio belongs to two albums parks its + sidecar rather than guessing. ++- **An image album keeps its name.** A folder holding one audio file and a + `.cue` is a whole album in one file; numbering it `01 - …` tells Jellyfin to + index it as one track. ++- **A video container parks**, because Jellyfin will not index it as music and a + 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. + +The 2026-10-30 dry run files 114k tracks and parks 6.8k — mostly sidecars +sitting in a folder that holds two albums, plus audio no provider row names. The +plan is NOT applied: the exceptions file is the review, and it is Frank's to +read. + ## Laws - **A moved file is a 404 in [[Plex]] until its section is rescanned.** The @@ -322,6 +381,21 @@ index a release leads with (`4a. Seed DESTINY` → `Seed DESTINY`). whiteout in the upper layer, which hides the array's copy without removing it; the file returns when the view is unmounted. Deletions in this pipeline happen on the raw path only, which is what the journal can undo. +- **The music album key is case-folded; the folder name is not.** `Master Of + Puppets` and `Master of Puppets` are one album to the matcher, so the spelling + that reaches the folder is the one most tracks carry. Taking it from whichever + row sorted first made the folder name depend on row order, and re-planning an + already-filed library renamed albums that were already in place. +- **An occupied path is never a destination**, not even when its occupant is + scheduled to leave: the executor refuses an occupied target, so a plan that + aims at one fails after the renames that were supposed to clear it. A file + standing where the contract puts it is a keep, and a file already carrying a + collision suffix keeps that suffix — otherwise the plan is a shuffle, not a + fixed point, and a re-run moves albums that are already right. +- **`media-census.py music` reads the `BaseItemProviders` table, not the item's + JSON.** This Jellyfin build carries an empty `ProviderIds` inside + `BaseItems.Data` for albums, so a census that parses the JSON finds no + MusicBrainz or Spotify id on any album and reads as "the library is untagged". ## Rollback