feat(media): the music library's own relayout, filed by its tags #100

Merged
dank merged 3 commits from feat/jellyfin-provider-keys into main 2026-10-31 00:04:41 -07:00
Owner

The music library gets the same treatment as the movies and the two TV shares:
scripts/media-music.py files /mnt/entropy/Music into
Music/<album artist>/<Album> (<year>)/NN - <Title>.ext from the tag row
Jellyfin matched, never from the name the ripper gave the file.

  • Authority: media-census.py music exports Jellyfin's audio rows with the
    album and artist ids read from BaseItemProviders — BaseItems.Data carries an
    empty ProviderIds for albums in this build, so a census that parses the item
    JSON reads the whole library as untagged.
  • Identity: (album-artist tag, album, year), case-folded. A compilation files
    once under Various Artists; two album-artist tags on one album name are two
    albums. The folder spelling is the dominant one, because a name taken from
    whichever row sorted first depends on row order and re-names albums that are
    already filed.
  • Refusals: a sidecar is placed by the audio around it, never by its own name;
    an image album (one .flac + .cue) keeps its name; video containers and audio
    no provider row names park under _unsorted for a human to read.
  • An occupied path is never a destination — the executor refuses occupied
    targets, so a plan that aims at one fails after the renames meant to clear it.

Evidence:

  • Real library, dry run: 114,009 files filed, 6,760 parked (120,769 total) into
    8,391 album folders under 923 artists. Plan and review live on the share:
    /mnt/entropy/media-relayout-music-{plan.tsv,exceptions.txt}.
  • Fixed point: re-planning the post-move tree leaves 114,002 of 114,009 files
    where they are. The 6 residuals are album-image files that merge with another
    rip of the same album and converge on the next run; the law is on the page.
  • Executor smoke against a 398-file hardlink scratch of real paths (emoji, " in
    filenames, accents): apply moved all of them and created 260 folders,
    rollback put every path back and removed the folders it made, and verify
    refuses a wrong --root.
  • ./tests/run.sh green; the new seam is tests/media_music.bats (24 tests,
    including the re-plan-that-moves-nothing law).

Nothing is applied to the music library: the exceptions file is the review, and
the apply waits for Frank.


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

The music library gets the same treatment as the movies and the two TV shares: `scripts/media-music.py` files `/mnt/entropy/Music` into `Music/<album artist>/<Album> (<year>)/NN - <Title>.ext` from the tag row Jellyfin matched, never from the name the ripper gave the file. - **Authority**: `media-census.py music` exports Jellyfin's audio rows with the album and artist ids read from `BaseItemProviders` — `BaseItems.Data` carries an empty `ProviderIds` for albums in this build, so a census that parses the item JSON reads the whole library as untagged. - **Identity**: (album-artist tag, album, year), case-folded. A compilation files once under `Various Artists`; two album-artist tags on one album name are two albums. The folder spelling is the dominant one, because a name taken from whichever row sorted first depends on row order and re-names albums that are already filed. - **Refusals**: a sidecar is placed by the audio around it, never by its own name; an image album (one `.flac` + `.cue`) keeps its name; video containers and audio no provider row names park under `_unsorted` for a human to read. - **An occupied path is never a destination** — the executor refuses occupied targets, so a plan that aims at one fails after the renames meant to clear it. Evidence: - Real library, dry run: 114,009 files filed, 6,760 parked (120,769 total) into 8,391 album folders under 923 artists. Plan and review live on the share: `/mnt/entropy/media-relayout-music-{plan.tsv,exceptions.txt}`. - Fixed point: re-planning the post-move tree leaves 114,002 of 114,009 files where they are. The 6 residuals are album-image files that merge with another rip of the same album and converge on the next run; the law is on the page. - Executor smoke against a 398-file hardlink scratch of real paths (emoji, `"` in filenames, accents): `apply` moved all of them and created 260 folders, `rollback` put every path back and removed the folders it made, and `verify` refuses a wrong `--root`. - `./tests/run.sh` green; the new seam is `tests/media_music.bats` (24 tests, including the re-plan-that-moves-nothing law). Nothing is applied to the music library: the exceptions file is the review, and the apply waits for Frank. --- cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
dank added 3 commits 2026-10-31 00:02:50 -07:00
The music library is rips and discography packs whose filenames say nothing the
matcher needs. The authority is Jellyfin's own audio rows: one per track, with
the album and album-artist ids it carries in BaseItemProviders (the item JSON
holds an empty ProviderIds for albums in this build).

Grouping is what the tags assert, not where files sit: (album-artist tag, album,
year), case-folded. A compilation is one album whose tracks name many performers,
so it files once under Various Artists; two album-artist tags on one album name
are two albums. The folder spelling is the dominant one, because a name taken
from whichever row sorted first depends on row order and re-names albums that are
already filed.

An occupied path is never a destination: the executor refuses occupied targets,
so a plan that aims at one fails after the renames meant to clear it. A file
standing on its own target, or already carrying a collision suffix, is a keep -
otherwise the plan shuffles instead of being a fixed point.
dank merged commit 382e3eb0b8 into main 2026-10-31 00:04:41 -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#100