Chapter images are never extracted, so a chapter list renders without pictures #98

Closed
opened 2026-08-13 22:59:39 +02:00 by Claude · 1 comment
Collaborator

A scan already writes the Chapters rows — crates/jellyfin-db/src/scan.rs:1668 deletes and re-inserts them from what ffprobe reported — and ImagePath on every one of them is null. Nothing extracts a frame.

Upstream's ChapterImagesTask (Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs, key RefreshChapterImages, daily) walks every video, seeks to each chapter's start, writes a jpg under the metadata directory, and stores the path on the row. A client draws the chapter menu from those paths; with none, the menu is a list of times.

Two things to settle from upstream rather than guess at: which items are eligible (it has a configuration switch per library and skips items with too many chapters), and where the file goes — the path convention is the same kind of rule M6 settled for local artwork, and getting it wrong writes files upstream cannot read.

Depends on the scheduler issue for the when; the extraction itself is standalone and can be driven by hand first.


Where to start

The rows already exist: crates/jellyfin-db/src/scan.rs:1668 deletes and
re-inserts Chapters from what the prober reported, and ImagePath is written
null. crates/jellyfin-db/src/items.rs:340 is the read.

Upstream is Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
(key RefreshChapterImages, daily). Two things to take from it rather than
decide: which items are eligible (there is a per-library switch and a ceiling on
chapter count), and where the file goes — the on-disk convention is the same
kind of rule M6 settled for local artwork, and a file in the wrong place is one
upstream cannot read.

The extraction itself is a seek and a still, through crates/jellyfin-ffmpeg.

How to know it is done

diff-db over a library both servers scanned reports no difference on
Chapters, ImagePath included once the paths are substituted — that column is
the whole point and it is diffed today, so this is a check that already exists
and currently passes vacuously because both sides are null.

Compare a written jpg against upstream's on bytes, not on dimensions. M10
learned that twice: a check on a projection is not a check.

House rules

CLAUDE.md is the short version and these are the parts that bite: tests run
under cargo nextest; upstream's source is the checkout in upstream/ at the
pin
(v12.0-rc5, 6d501ba4) and not the clone beside the repository;
generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and Dn references stay out of code and comments.
ARCHITECTURE.md decides which crate a thing goes in and which dependency edges
are illegal, DECISIONS.md says why a rule exists.

The reference pair is cargo xtask reference up (and reference provision,
which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,
with diff-db for the two databases. crates/jellyfin-harness/README.md says
what a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.

A scan already writes the `Chapters` rows — `crates/jellyfin-db/src/scan.rs:1668` deletes and re-inserts them from what ffprobe reported — and `ImagePath` on every one of them is null. Nothing extracts a frame. Upstream's `ChapterImagesTask` (`Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs`, key `RefreshChapterImages`, daily) walks every video, seeks to each chapter's start, writes a jpg under the metadata directory, and stores the path on the row. A client draws the chapter menu from those paths; with none, the menu is a list of times. Two things to settle from upstream rather than guess at: which items are eligible (it has a configuration switch per library and skips items with too many chapters), and where the file goes — the path convention is the same kind of rule M6 settled for local artwork, and getting it wrong writes files upstream cannot read. Depends on the scheduler issue for the *when*; the extraction itself is standalone and can be driven by hand first. --- ## Where to start The rows already exist: `crates/jellyfin-db/src/scan.rs:1668` deletes and re-inserts `Chapters` from what the prober reported, and `ImagePath` is written null. `crates/jellyfin-db/src/items.rs:340` is the read. Upstream is `Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs` (key `RefreshChapterImages`, daily). Two things to take from it rather than decide: which items are eligible (there is a per-library switch and a ceiling on chapter count), and where the file goes — the on-disk convention is the same kind of rule M6 settled for local artwork, and a file in the wrong place is one upstream cannot read. The extraction itself is a seek and a still, through `crates/jellyfin-ffmpeg`. ## How to know it is done `diff-db` over a library both servers scanned reports no difference on `Chapters`, `ImagePath` included once the paths are substituted — that column is the whole point and it is diffed today, so this is a check that already exists and currently passes vacuously because both sides are null. Compare a written jpg against upstream's on **bytes**, not on dimensions. M10 learned that twice: a check on a projection is not a check. ### House rules `CLAUDE.md` is the short version and these are the parts that bite: tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin** (`v12.0-rc5`, `6d501ba4`) and not the clone beside the repository; generated files are regenerated rather than edited; a comment says what the code does, not how it got there; and `Dn` references stay out of code and comments. `ARCHITECTURE.md` decides which crate a thing goes in and which dependency edges are illegal, `DECISIONS.md` says why a rule exists. The reference pair is `cargo xtask reference up` (and `reference provision`, which is idempotent); a run is `cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR`, with `diff-db` for the two databases. `crates/jellyfin-harness/README.md` says what a diff means and what an ignore entry has to carry — every entry needs a reason and an expiry milestone, and an adds-only history is the smell the risk table names.
Author
Collaborator

Done, on main — dda1d09.

The Chapters rows already existed with ImagePath null. Three chapter rows now agree column for column and the three jpegs are identical byte for byte — 9973, 9578 and 9510 bytes. The path convention, the 15-second offset for the opening chapter, the average-gap floor and the failure list under the cache directory are all upstream's.

This uncovered a bug outside the batch, in a path nothing could have caught. jellyfin-encoding::input::video_sync_option handed the number to the option that takes a name: upstream maps -1 → auto for ffmpeg 5.1 and later, and ours emitted -fps_mode -1, which ffmpeg refuses outright — so everything reaching it failed. It is also in the attachment burn-in path (attach.rs:123 sets input_video_sync = "-1"). Fixed here; the corpus gap that hid it is filed separately, because neither generated corpus file contains -fps_mode at all.

One db-ignore-list.json entry added: Chapters.ImageDateModified, value-scoped, on the same wall-clock argument as BaseItems.DateLastRefreshed — which chapters carry a picture is still diffed, and so is ImagePath beside it. It cannot fire on the current fixture, because nothing there has chapters, which is the honest cost of adding it before the fixture grows one.

The fixture wants a chaptered file. This was measured on one built by hand — Night Signal remuxed with three chapters — and registered as a library on both servers. Until the committed fixture carries one, nothing in a replay holds this.

Done, on `main` — `dda1d09`. The `Chapters` rows already existed with `ImagePath` null. Three chapter rows now agree column for column and the three jpegs are **identical byte for byte** — 9973, 9578 and 9510 bytes. The path convention, the 15-second offset for the opening chapter, the average-gap floor and the failure list under the cache directory are all upstream's. **This uncovered a bug outside the batch, in a path nothing could have caught.** `jellyfin-encoding::input::video_sync_option` handed the *number* to the option that takes a *name*: upstream maps `-1 → auto` for ffmpeg 5.1 and later, and ours emitted `-fps_mode -1`, which ffmpeg refuses outright — so everything reaching it failed. It is also in the attachment burn-in path (`attach.rs:123` sets `input_video_sync = "-1"`). Fixed here; the corpus gap that hid it is filed separately, because neither generated corpus file contains `-fps_mode` at all. One `db-ignore-list.json` entry added: `Chapters.ImageDateModified`, value-scoped, on the same wall-clock argument as `BaseItems.DateLastRefreshed` — which chapters carry a picture is still diffed, and so is `ImagePath` beside it. It **cannot fire on the current fixture**, because nothing there has chapters, which is the honest cost of adding it before the fixture grows one. **The fixture wants a chaptered file.** This was measured on one built by hand — `Night Signal` remuxed with three chapters — and registered as a library on both servers. Until the committed fixture carries one, nothing in a replay holds this.
Sign in to join this conversation.
No description provided.