A scan's rows differ from upstream's though the counts nearly agree #75

Closed
opened 2026-08-04 16:10:36 +02:00 by Claude · 4 comments
Collaborator

The rows a scan writes differ from upstream's, though the counts nearly agree.

First diff-db between upstream's own scan of /mnt/media
(var/big-upstream) and this port's scan of the same three directories.

The shape agrees. Item counts by type are identical — 7905 Episode, 4889
Audio, 708 Season, 329 MusicAlbum, 289 Series, 284 Movie — and three tables
match row for row:

table upstream ours
Chapters 36602 36602
PeopleBaseItemMap 9151 9151
AttachmentStreamInfos 225 225
BaseItems 15147 15141
MediaStreamInfos 116987 116917
AncestorIds 69656 69659
BaseItemProviders 19511 19401
ItemValuesMap 20295 20126
Peoples 375 309
BaseItemImageInfos 109 10

The content does not. 47,632 rows differ on at least one column, dominated
by MediaStreamInfos at 16,772 — roughly one row in seven of that table. That
is the largest single thing left, and it is invisible to the response harness,
which compares what a route serves rather than what a scan wrote.

Not defects, and to exclude before counting:

  • Users, Permissions, Preferences, Devices are empty here only because
    the scan config provisioned no account.
  • ActivityLogs and the display-preference tables are already exempt with
    recorded reasons.

Seen in the BaseItems sample and worth pulling apart first: upstream stores a
copyright string in Studios (2026 BBC (programme data only)) where this port
stores '', and carries a PremiereDate this port leaves null. Both come from
the probe rather than from a provider, so both should be reachable.

Rerun with:

jellyfin-harness diff-db var/big-upstream/config/data/jellyfin.db \
    <a fresh scan of /mnt/media>
The rows a scan writes differ from upstream's, though the counts nearly agree. First `diff-db` between upstream's own scan of `/mnt/media` (`var/big-upstream`) and this port's scan of the same three directories. **The shape agrees.** Item counts by type are identical — 7905 Episode, 4889 Audio, 708 Season, 329 MusicAlbum, 289 Series, 284 Movie — and three tables match row for row: | table | upstream | ours | |---|---|---| | Chapters | 36602 | 36602 | | PeopleBaseItemMap | 9151 | 9151 | | AttachmentStreamInfos | 225 | 225 | | BaseItems | 15147 | 15141 | | MediaStreamInfos | 116987 | 116917 | | AncestorIds | 69656 | 69659 | | BaseItemProviders | 19511 | 19401 | | ItemValuesMap | 20295 | 20126 | | Peoples | 375 | 309 | | BaseItemImageInfos | 109 | 10 | **The content does not.** 47,632 rows differ on at least one column, dominated by `MediaStreamInfos` at 16,772 — roughly one row in seven of that table. That is the largest single thing left, and it is invisible to the response harness, which compares what a route serves rather than what a scan wrote. Not defects, and to exclude before counting: - `Users`, `Permissions`, `Preferences`, `Devices` are empty here only because the scan config provisioned no account. - `ActivityLogs` and the display-preference tables are already exempt with recorded reasons. Seen in the `BaseItems` sample and worth pulling apart first: upstream stores a copyright string in `Studios` (`2026 BBC (programme data only)`) where this port stores `''`, and carries a `PremiereDate` this port leaves null. Both come from the probe rather than from a provider, so both should be reachable. Rerun with: jellyfin-harness diff-db var/big-upstream/config/data/jellyfin.db \ <a fresh scan of /mnt/media>
Author
Collaborator

Scoped. The stream table is not 16,772 rows of trouble — it is two columns.

All 116,917 rows this port wrote match upstream's on every column but three:

column rows differing upstream ours
RefFrames 8,350 1 absent
Hdr10PlusPresentFlag 248 1 absent
BitRate 2 134982 130913

Everything else — codec, profile, dimensions, channel layout, language, the
dynamic-range columns, the disposition flags — agrees row for row. The harness
counts a row as differing when any column does, which is what made two missing
fields look like one row in seven.

RefFrames is the reference-frame count ffprobe reports as refs, and this
port never records it. Hdr10PlusPresentFlag is already written here but is
reached only where the binary takes -only_first_vframe; the 248 are rows
upstream measured and this port did not.

The remaining 70 rows are streams this port did not write at all, on 6 items it
also did not write — worth pulling apart separately from the columns.

So the order of work is: RefFrames, then the 70 absent rows, then the two
bitrates. The other tables' differences are still unexamined.

Scoped. The stream table is not 16,772 rows of trouble — it is **two columns**. All 116,917 rows this port wrote match upstream's on every column but three: | column | rows differing | upstream | ours | |---|---|---|---| | `RefFrames` | 8,350 | `1` | absent | | `Hdr10PlusPresentFlag` | 248 | `1` | absent | | `BitRate` | 2 | 134982 | 130913 | Everything else — codec, profile, dimensions, channel layout, language, the dynamic-range columns, the disposition flags — agrees row for row. The harness counts a row as differing when any column does, which is what made two missing fields look like one row in seven. `RefFrames` is the reference-frame count ffprobe reports as `refs`, and this port never records it. `Hdr10PlusPresentFlag` is already written here but is reached only where the binary takes `-only_first_vframe`; the 248 are rows upstream measured and this port did not. The remaining 70 rows are streams this port did not write at all, on 6 items it also did not write — worth pulling apart separately from the columns. So the order of work is: `RefFrames`, then the 70 absent rows, then the two bitrates. The other tables' differences are still unexamined.
Author
Collaborator

The BaseItems half, scoped the same way.

Comparing the 15,139 items both scans wrote (ignoring Data and the
save/refresh timestamps):

column items differing upstream ours
ProductionLocations 15,139 — every one '' NULL
Tags 15,139 — every one '' NULL
PremiereDate 9,631 2018-12-19 00:01:06 NULL
ProductionYear 1,017 2018 2001
DateLastMediaAdded 289 a date NULL
DateModified 219 a date NULL
RunTimeTicks 118 3600000000 NULL
Genres 110 `Ambient Drum And Bass
Name / SortName 101 / 100 differ in spelling
Overview 37 present NULL

The first two are one bug and the cheapest win here: upstream's columns are
non-nullable, so nothing reads as '' where this port writes NULL. That is the
same shape as the DateCreated fix in #51, and it accounts for 30,278 of the
differing values on its own.

ProductionYear is the one to look at hardest — those are different values,
not a missing one (2018 against 2001), so something is being read from a
different place rather than not read at all.

NormalizationGain (83) is already fixed by #72; this database was scanned
before that merged. Check the others for the same before assuming they are open.

Eight items upstream has and this port does not, and most are known scope
rather than defects: the MØL artist (#71's unresolvable name), three
MusicGenre rows from upstream's artist metadata refresh, the playlists folder,
and the Live TV view.

The `BaseItems` half, scoped the same way. Comparing the 15,139 items both scans wrote (ignoring `Data` and the save/refresh timestamps): | column | items differing | upstream | ours | |---|---|---|---| | `ProductionLocations` | **15,139 — every one** | `''` | NULL | | `Tags` | **15,139 — every one** | `''` | NULL | | `PremiereDate` | 9,631 | `2018-12-19 00:01:06` | NULL | | `ProductionYear` | 1,017 | 2018 | **2001** | | `DateLastMediaAdded` | 289 | a date | NULL | | `DateModified` | 219 | a date | NULL | | `RunTimeTicks` | 118 | 3600000000 | NULL | | `Genres` | 110 | `Ambient|Drum And Bass|…` | `''` | | `Name` / `SortName` | 101 / 100 | differ in spelling | | | `Overview` | 37 | present | NULL | **The first two are one bug and the cheapest win here**: upstream's columns are non-nullable, so nothing reads as `''` where this port writes NULL. That is the same shape as the `DateCreated` fix in #51, and it accounts for 30,278 of the differing values on its own. `ProductionYear` is the one to look at hardest — those are *different values*, not a missing one (2018 against 2001), so something is being read from a different place rather than not read at all. `NormalizationGain` (83) is already fixed by #72; this database was scanned before that merged. Check the others for the same before assuming they are open. Eight items upstream has and this port does not, and most are known scope rather than defects: the `MØL` artist (#71's unresolvable name), three `MusicGenre` rows from upstream's artist metadata refresh, the playlists folder, and the Live TV view.
Author
Collaborator

The stream half is not a defect. It was my measurement.

The scan those numbers came from ran with ffprobe = "ffprobe" — the distro
build. -only_first_vframe is a jellyfin-ffmpeg patch, and a distro binary
refuses it, so the probe drops -show_frames -only_first_vframe. That is
upstream's own rule (MediaEncoder.cs:517, gated by the capability check at
:238) and this port implements it identically. Without -show_frames,
ffprobe never opens a decoder, and all three columns follow from that one fact
— measured against the pinned image's own binary on the same file:

probe arguments refs HDR10+ side data aac bitrate
-show_streams -show_format absent not emitted 130913
+ -show_frames -only_first_vframe 1 present 134982

The two BitRate rows follow arithmetically: video is estimated as container
minus audio, so the audio's 4,069 moves to video with the opposite sign. Both
were on one file and cancelled.

Rescanning the same subsets with the right prober closes every column:

subset prober RefFrames Hdr10Plus BitRate
movies (289 items) distro 303 3 0
movies jellyfin-ffmpeg 0 0 0
one show distro 7 0 2
one show jellyfin-ffmpeg 0 0 0

Neither side was broken: refs is parsed, mapped, written, read back and
served, and the read path was proven live against the upstream-scanned database
— both servers return byte-identical RefFrames, Hdr10PlusPresentFlag and
BitRate for the same item.

No source change was made, and that is deliberate. Rather than manufacture
one to satisfy a test requirement, the existing behaviour is now pinned: a
stream round trip over every probe-filled field (that file claimed to cover them
and covered only the dynamic-range columns, so RefFrames had no write-side
assertion at all), and three unit tests for the HDR10+ rule that decides all 248
rows and had none. Both shown load-bearing by reverting the source.

The 70 absent rows are also not this. My pairing of them with the 6 missing
items was wrong: all 70 are Lyric streams on 70 audio items this port did
write. Upstream extracts a lyrics-eng tag to an .lrc file under its metadata
directory and then resolves that file back as a stream. No .lrc exists under
/mnt/media; it is a metadata-writing feature this port does not have.

README now records that a scan being diffed wants the prober upstream ships, and
that the first_video_frame line at startup says which one you got.

**The stream half is not a defect. It was my measurement.** The scan those numbers came from ran with `ffprobe = "ffprobe"` — the distro build. `-only_first_vframe` is a jellyfin-ffmpeg patch, and a distro binary refuses it, so the probe drops `-show_frames -only_first_vframe`. That is upstream's own rule (`MediaEncoder.cs:517`, gated by the capability check at `:238`) and this port implements it identically. Without `-show_frames`, ffprobe never opens a decoder, and all three columns follow from that one fact — measured against the pinned image's own binary on the same file: | probe arguments | `refs` | HDR10+ side data | aac bitrate | |---|---|---|---| | `-show_streams -show_format` | absent | not emitted | 130913 | | `+ -show_frames -only_first_vframe` | `1` | present | **134982** | The two `BitRate` rows follow arithmetically: video is estimated as container minus audio, so the audio's 4,069 moves to video with the opposite sign. Both were on one file and cancelled. Rescanning the same subsets with the right prober closes every column: | subset | prober | RefFrames | Hdr10Plus | BitRate | |---|---|---|---|---| | movies (289 items) | distro | 303 | 3 | 0 | | movies | jellyfin-ffmpeg | **0** | **0** | **0** | | one show | distro | 7 | 0 | 2 | | one show | jellyfin-ffmpeg | **0** | **0** | **0** | Neither side was broken: `refs` is parsed, mapped, written, read back and served, and the read path was proven live against the upstream-scanned database — both servers return byte-identical `RefFrames`, `Hdr10PlusPresentFlag` and `BitRate` for the same item. **No source change was made, and that is deliberate.** Rather than manufacture one to satisfy a test requirement, the existing behaviour is now pinned: a stream round trip over every probe-filled field (that file claimed to cover them and covered only the dynamic-range columns, so `RefFrames` had no write-side assertion at all), and three unit tests for the HDR10+ rule that decides all 248 rows and had none. Both shown load-bearing by reverting the source. **The 70 absent rows are also not this.** My pairing of them with the 6 missing items was wrong: all 70 are Lyric streams on 70 audio items this port *did* write. Upstream extracts a `lyrics-eng` tag to an `.lrc` file under its metadata directory and then resolves that file back as a stream. No `.lrc` exists under `/mnt/media`; it is a metadata-writing feature this port does not have. README now records that a scan being diffed wants the prober upstream ships, and that the `first_video_frame` line at startup says which one you got.
Author
Collaborator

Closing this on what it settled, with the remainder split out as #76.

The stream columns were not a defect. The scan behind the original numbers
ran with the distro ffprobe, which refuses -only_first_vframe — a
jellyfin-ffmpeg patch — so the probe correctly drops -show_frames, and without
a decoder refs, the HDR10+ side data and the aac bitrate all disappear
together. Rescanning the same subsets with the prober upstream ships closes all
three to zero. No source change was warranted; instead the existing behaviour is
now pinned, and that turned up real coverage gaps — crates/jellyfin-db/tests/ streams.rs claimed to cover every probe-filled field and covered only the
dynamic-range columns, so RefFrames had no write-side assertion at all, and
the HDR10+ rule deciding 248 rows had none either.

The item columns were, and are fixed. Upstream reads an audio date from its
tag library rather than from the prober, and the two disagree: ffmpeg merges
ID3v2.3's TYER and TDAT into one date where ATL does not. Every ID3v2
block in the library is v2.3, which is why 1,437 mp3s and 61 flacs carried a
full date where upstream has a year. Measured against upstream's own scan of the
same files:

library column before after
movies ProductionLocations, Tags, PremiereDate 289, 289, 242 0, 0, 0
music the same three, ProductionYear, Album 5944, 5944, 5203, 304, 17 0, 0, 0, 0, 0
tv DateLastMediaAdded — 0 (289/289 series)

ProductionLocations and Tags were the cheapest of it: upstream's columns are
non-nullable so every row holds '', and this port wrote NULL — 30,278 values
from one omission.

A rescan of an unchanged library still writes nothing, verified on both real
libraries with every column byte-identical across the second pass.

Also corrected here: the 70 absent stream rows are not paired with the 6 missing
items. They are Lyric streams on 70 items this port did write, extracted by
upstream from a lyrics-eng tag into an .lrc file it then resolves back. No
.lrc exists under the measured library.

What is left is in #76 — it is a different kind of work from this, not more of
the same.

Closing this on what it settled, with the remainder split out as #76. **The stream columns were not a defect.** The scan behind the original numbers ran with the distro ffprobe, which refuses `-only_first_vframe` — a jellyfin-ffmpeg patch — so the probe correctly drops `-show_frames`, and without a decoder `refs`, the HDR10+ side data and the aac bitrate all disappear together. Rescanning the same subsets with the prober upstream ships closes all three to zero. No source change was warranted; instead the existing behaviour is now pinned, and that turned up real coverage gaps — `crates/jellyfin-db/tests/ streams.rs` claimed to cover every probe-filled field and covered only the dynamic-range columns, so `RefFrames` had no write-side assertion at all, and the HDR10+ rule deciding 248 rows had none either. **The item columns were, and are fixed.** Upstream reads an audio date from its tag library rather than from the prober, and the two disagree: ffmpeg merges `ID3v2.3`'s `TYER` and `TDAT` into one date where ATL does not. Every ID3v2 block in the library is v2.3, which is why 1,437 mp3s and 61 flacs carried a full date where upstream has a year. Measured against upstream's own scan of the same files: | library | column | before | after | |---|---|---|---| | movies | `ProductionLocations`, `Tags`, `PremiereDate` | 289, 289, 242 | **0, 0, 0** | | music | the same three, `ProductionYear`, `Album` | 5944, 5944, 5203, 304, 17 | **0, 0, 0, 0, 0** | | tv | `DateLastMediaAdded` | — | **0** (289/289 series) | `ProductionLocations` and `Tags` were the cheapest of it: upstream's columns are non-nullable so every row holds `''`, and this port wrote NULL — 30,278 values from one omission. A rescan of an unchanged library still writes nothing, verified on both real libraries with every column byte-identical across the second pass. Also corrected here: the 70 absent stream rows are not paired with the 6 missing items. They are Lyric streams on 70 items this port did write, extracted by upstream from a `lyrics-eng` tag into an `.lrc` file it then resolves back. No `.lrc` exists under the measured library. What is left is in #76 — it is a different kind of work from this, not more of the same.
Sign in to join this conversation.
No description provided.