Two scan rows that want the media mounted: an audio title, and which spelling a person keeps #85

Closed
opened 2026-08-11 22:02:20 +02:00 by Claude · 1 comment
Collaborator

Two rows a scan writes differently from upstream, both needing the media mounted to settle. Split out of #76, which closed everything else it named.

An audio file falls back to its stem where upstream reads the tag. /mnt/media/music/Feed Me/Mordez Moi/Feed_Me__Mordez_Moi.mp3 is Mordez Moi in upstream's scan and Feed_Me__Mordez_Moi in ours.

AudioFileProber.cs:189-193 reads title, album, year, track number and disc number as tag ?? probe. crates/jellyfin-ffmpeg/src/prober.rs's tag map reads genre, artist, albumartist, composer, two MusicBrainz ids, date and ReplayGain — so the title and the two numbers come from ffprobe alone, and a tag block ffprobe does not map leaves us with the file stem for the name and nothing for the disc. The 88 trailing-space KsTBeats names #75 closed were the same gap.

It is not a naming bug: jellyfin-naming derives correctly from whatever Name holds.

Why it is not just fixed. Upstream prefers the tag over the probe, so adding title, track and disc to the tag reader can move the name of any of the 4,889 audio rows in that library. Landing that blind is the wrong way round.

To settle it. Mount the media, scan with upstream's own prober — a distro ffprobe diverges the stream columns for reasons that are not the port's, and the first_video_frame line at startup says which you got — and diff:

jellyfin-harness diff-db var/big-upstream/config/data/jellyfin.db <a fresh scan of /mnt/media>

Expect the Name row above to close and nothing else to move; anything else that moves is the thing to look at.


A person with two spellings gets the other one. the GazettE upstream, The Gazette ours — one row on each side, not five, and Path/SortName/PresentationUniqueKey follow from it.

Peoples holds both spellings (the GazettE as artist, The Gazette as composer) and both servers write those rows identically. The by-name id folds case, so both agree there is exactly one item row; they disagree on which spelling it carries. Upstream creates it in SavePeopleMetadataAsync while saving the first crediting item's people, so the spelling is whichever the scan met first — confirmed by DateCreated: those rows were created during the media scan, at 11:26–11:35, not in name order, and PeopleValidator ran later. We create them afterwards from SELECT DISTINCT Name FROM Peoples ORDER BY Name and keep the byte-first spelling (T 0x54 before t 0x74).

Reproducing it means carrying the first-seen spelling per folded name from the item pass into the by-name step.

The library has exactly two folded-duplicate person names; the other, AES Dana/Aes Dana, agrees by luck. So a fix has one row of evidence, and it also needs a fresh scan to confirm.

Two rows a scan writes differently from upstream, both needing the media mounted to settle. Split out of #76, which closed everything else it named. **An audio file falls back to its stem where upstream reads the tag.** `/mnt/media/music/Feed Me/Mordez Moi/Feed_Me__Mordez_Moi.mp3` is `Mordez Moi` in upstream's scan and `Feed_Me__Mordez_Moi` in ours. `AudioFileProber.cs:189-193` reads title, album, year, **track number and disc number** as `tag ?? probe`. `crates/jellyfin-ffmpeg/src/prober.rs`'s tag map reads genre, artist, albumartist, composer, two MusicBrainz ids, date and ReplayGain — so the title and the two numbers come from ffprobe alone, and a tag block ffprobe does not map leaves us with the file stem for the name and nothing for the disc. The 88 trailing-space `KsTBeats` names #75 closed were the same gap. It is not a naming bug: `jellyfin-naming` derives correctly from whatever `Name` holds. **Why it is not just fixed.** Upstream *prefers* the tag over the probe, so adding title, track and disc to the tag reader can move the name of any of the 4,889 audio rows in that library. Landing that blind is the wrong way round. **To settle it.** Mount the media, scan with upstream's own prober — a distro ffprobe diverges the stream columns for reasons that are not the port's, and the `first_video_frame` line at startup says which you got — and diff: jellyfin-harness diff-db var/big-upstream/config/data/jellyfin.db <a fresh scan of /mnt/media> Expect the `Name` row above to close and nothing else to move; anything else that moves is the thing to look at. --- **A person with two spellings gets the other one.** `the GazettE` upstream, `The Gazette` ours — one row on each side, not five, and `Path`/`SortName`/`PresentationUniqueKey` follow from it. `Peoples` holds both spellings (`the GazettE` as artist, `The Gazette` as composer) and both servers write those rows identically. The by-name id folds case, so both agree there is exactly one item row; they disagree on which spelling it carries. Upstream creates it in `SavePeopleMetadataAsync` while saving the *first crediting item's* people, so the spelling is whichever the scan met first — confirmed by `DateCreated`: those rows were created during the media scan, at 11:26–11:35, not in name order, and `PeopleValidator` ran later. We create them afterwards from `SELECT DISTINCT Name FROM Peoples ORDER BY Name` and keep the byte-first spelling (`T` 0x54 before `t` 0x74). Reproducing it means carrying the first-seen spelling per folded name from the item pass into the by-name step. The library has exactly two folded-duplicate person names; the other, `AES Dana`/`Aes Dana`, agrees by luck. So a fix has one row of evidence, and it also needs a fresh scan to confirm.
Author
Collaborator

Both rows are closed, and the media was mounted, so this was settled the way the issue asked — a full rescan of the real library by both binaries with the pinned jellyfin-ffmpeg, not an argument from a fixture.

The audio title

The file carries two tag blocks. An ID3v2.3 block at the front holding only TPE1 (Feed Me), and an ID3v1 block in its last 128 bytes holding the title Mordez Moi, year 2008, genre 52, no album, no track. ffmpeg's mp3 demuxer reads ID3v1 only when nothing else produced metadata — so with TPE1 present, ffprobe never looks at the end of the file, and reports the artist alone. Upstream's reader takes both blocks, ID3v2 ahead of ID3v1, which is how it got the title where we fell back to the stem.

AudioFileProber.cs:189-193 takes title, album, year, track and disc as tag ?? probe, field by field. Two notes on the port: a 0 is no number is upstream's own rule verbatim (is null or 0), so a genuine track 0 or disc 0 falls back to the probe on both servers; and date was already asked for, so only four keys were added.

The disc half is a different file — Spor - Haunt Me + Brickbeats - 01 - Haunt Me.mp3, whose disc lives in a block ffprobe missed.

The person's spelling

SavePeopleMetadataAsync walks a crediting item's people in order and creates each row it misses under that item's spelling, so the spelling is whichever the scan met first. Nothing in this port deletes from Peoples, so its rowid is the order the credits were first written: SELECT Name FROM Peoples GROUP BY Name ORDER BY MIN(rowid).

That invariant has a dependency, and it is now written down where the code depends on it. Upstream does sweep people nothing credits any more — on item delete, and again in the people validation task, which also merges case-duplicates. A library that has lost a credit is a library the two can disagree about, until this port sweeps too.

The risk this issue was blocked on

The concern was that preferring the tag could move the name of any of the 4,889 audio rows, and that landing it blind was the wrong way round. That is now a number rather than a worry.

Both binaries scanned /mnt/media/{movies,tv,music} with reference/published/ffmpeg (first_video_frame=true at startup, so upstream's own prober rather than a distro build). Comparing our own before and after, over all 72 BaseItems columns and every content table:

Eight rows move. Five are the two servers' own data-directory roots. Three are the targets, and each lands on upstream's value exactly:

  • Feed_Me__Mordez_Moi → Mordez Moi (with SortName, CleanName)
  • Haunt Me disc NULL → 1 (with SortName)
  • The Gazette → the GazettE (with Path, SortName, PresentationUniqueKey)

IndexNumber and Album move on zero rows. Peoples, PeopleBaseItemMap, ItemValues, ItemValuesMap, MediaStreamInfos, AncestorIds, Chapters and BaseItemProviders are byte-identical before and after. Against upstream, Name goes 7 → 5, SortName 8 → 5, ParentIndexNumber 1 → 0; the five residual names are Witcher episodes where upstream keeps the full release filename, which is a video difference this change cannot touch.

One measurement caveat, stated plainly

jellyfin-harness diff-db reports 14 tables differ, 32100 rows both before and after. That number is insensitive to three rows: it compares whole rows, and every BaseItems row on both sides already differs on DateLastRefreshed/DateLastSaved/Data, which no scan of a different install can match — BaseItems alone contributes 30,332 of the 32,100. diff-db does support per-column exemptions, so it could answer this, but only with a permanent db-ignore-list.json entry over those three columns — and blanket-exempting Data, the serialized item blob, would hide real regressions in everything it carries. So the per-column comparison above is the instrument instead. The id sets are identical before and after (symmetric difference 0, 15,133 shared rows both times), which is the assumption that comparison rests on.

Both rows are closed, and the media was mounted, so this was settled the way the issue asked — a full rescan of the real library by both binaries with the pinned `jellyfin-ffmpeg`, not an argument from a fixture. ## The audio title The file carries **two** tag blocks. An ID3v2.3 block at the front holding only `TPE1` (`Feed Me`), and an ID3v1 block in its last 128 bytes holding the title `Mordez Moi`, year 2008, genre 52, no album, no track. ffmpeg's mp3 demuxer reads ID3v1 only when nothing else produced metadata — so with `TPE1` present, ffprobe never looks at the end of the file, and reports the artist alone. Upstream's reader takes both blocks, ID3v2 ahead of ID3v1, which is how it got the title where we fell back to the stem. `AudioFileProber.cs:189-193` takes title, album, year, track and disc as `tag ?? probe`, field by field. Two notes on the port: **a 0 is no number** is upstream's own rule verbatim (`is null or 0`), so a genuine track 0 or disc 0 falls back to the probe on both servers; and `date` was already asked for, so only four keys were added. The disc half is a different file — `Spor - Haunt Me + Brickbeats - 01 - Haunt Me.mp3`, whose disc lives in a block ffprobe missed. ## The person's spelling `SavePeopleMetadataAsync` walks a crediting item's people in order and creates each row it misses under that item's spelling, so the spelling is whichever the scan met first. Nothing in this port deletes from `Peoples`, so its `rowid` **is** the order the credits were first written: `SELECT Name FROM Peoples GROUP BY Name ORDER BY MIN(rowid)`. That invariant has a dependency, and it is now written down where the code depends on it. Upstream *does* sweep people nothing credits any more — on item delete, and again in the people validation task, which also merges case-duplicates. A library that has lost a credit is a library the two can disagree about, until this port sweeps too. ## The risk this issue was blocked on The concern was that preferring the tag could move the name of any of the 4,889 audio rows, and that landing it blind was the wrong way round. That is now a number rather than a worry. Both binaries scanned `/mnt/media/{movies,tv,music}` with `reference/published/ffmpeg` (`first_video_frame=true` at startup, so upstream's own prober rather than a distro build). Comparing our own before and after, over all 72 `BaseItems` columns and every content table: **Eight rows move. Five are the two servers' own data-directory roots. Three are the targets**, and each lands on upstream's value exactly: - `Feed_Me__Mordez_Moi` → `Mordez Moi` (with `SortName`, `CleanName`) - `Haunt Me` disc `NULL` → `1` (with `SortName`) - `The Gazette` → `the GazettE` (with `Path`, `SortName`, `PresentationUniqueKey`) `IndexNumber` and `Album` move on **zero** rows. `Peoples`, `PeopleBaseItemMap`, `ItemValues`, `ItemValuesMap`, `MediaStreamInfos`, `AncestorIds`, `Chapters` and `BaseItemProviders` are byte-identical before and after. Against upstream, `Name` goes 7 → 5, `SortName` 8 → 5, `ParentIndexNumber` 1 → 0; the five residual names are Witcher episodes where upstream keeps the full release filename, which is a video difference this change cannot touch. ## One measurement caveat, stated plainly `jellyfin-harness diff-db` reports `14 tables differ, 32100 rows` both before **and** after. That number is insensitive to three rows: it compares whole rows, and every `BaseItems` row on both sides already differs on `DateLastRefreshed`/`DateLastSaved`/`Data`, which no scan of a different install can match — `BaseItems` alone contributes 30,332 of the 32,100. `diff-db` does support per-column exemptions, so it *could* answer this, but only with a permanent `db-ignore-list.json` entry over those three columns — and blanket-exempting `Data`, the serialized item blob, would hide real regressions in everything it carries. So the per-column comparison above is the instrument instead. The id sets are identical before and after (symmetric difference 0, 15,133 shared rows both times), which is the assumption that comparison rests on.
Sign in to join this conversation.
No description provided.