A person is asked about on every RefreshPeople run: DateLastRefreshed is never stamped #154

Closed
opened 2026-08-16 21:43:46 +02:00 by Claude · 1 comment
Collaborator

RefreshPeople asks a database about the same person on every run, forever.

crates/jellyfin-db/src/scan/people.rs gates the sweep on upstream's own
predicate — DateLastRefreshed IS NULL OR DateLastRefreshed < ?2 — and its doc
says both halves matter: "one that was asked about yesterday is not asked again
today however little came back." That half is inert, because nothing ever
stamps the column for a person.

crates/jellyfin-db/src/scan/row.rs:232 binds it as
(item.saved() && item.kind() != Kind::Person).then(...), which was true when
nothing asked about a person: the comment beside it says "a person is created
and saved and never refreshed". #107 made that false. refresh_people
(crates/jellyfin-server/src/libraries.rs) rebuilds the row and files it
through the same writer, so the stamp is still NULL and the person qualifies
again on the next run.

Two consequences, and the second is worse than the first:

  • a credited person whose answer carries no biography and no portrait — very
    common for minor crew — is searched for again on every run
  • a person the database cannot identify at all writes no row, so nothing is
    ever stamped for them under any fix that only stamps a successful refresh

Upstream stamps it. PeopleValidationTask's second phase refreshes every
person to validate them, which is what sets DateLastRefreshed and is why its
third phase then asks about nobody — see reference/README.md.

Where to start

The bind is shared by every write, and the two cases want different answers: a
person the scan stands up should keep the null upstream leaves, and one
refresh_people files should carry the stamp. So the mark belongs on the
Write rather than on the kind — jellyfin_core::item::Write::only_created
and its saved flag are the precedent for how a writer says which kind of
write this is.

How to know it is done

A person refreshed once is not in people_wanting_a_refresh's answer on the
next run, and a person nothing could identify is not either. The second is the
half that needs deciding rather than writing: a row that was never written
cannot carry a stamp.

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.

`RefreshPeople` asks a database about the same person on every run, forever. `crates/jellyfin-db/src/scan/people.rs` gates the sweep on upstream's own predicate — `DateLastRefreshed IS NULL OR DateLastRefreshed < ?2` — and its doc says both halves matter: "one that was asked about yesterday is not asked again today however little came back." That half is inert, because nothing ever stamps the column for a person. `crates/jellyfin-db/src/scan/row.rs:232` binds it as `(item.saved() && item.kind() != Kind::Person).then(...)`, which was true when nothing asked about a person: the comment beside it says "a person is created and saved and never refreshed". #107 made that false. `refresh_people` (`crates/jellyfin-server/src/libraries.rs`) rebuilds the row and files it through the same writer, so the stamp is still NULL and the person qualifies again on the next run. Two consequences, and the second is worse than the first: - a credited person whose answer carries no biography and no portrait — very common for minor crew — is searched for again on every run - a person the database cannot identify at all writes no row, so nothing is ever stamped for them under any fix that only stamps a successful refresh Upstream stamps it. `PeopleValidationTask`'s second phase refreshes every person to validate them, which is what sets `DateLastRefreshed` and is why its third phase then asks about nobody — see `reference/README.md`. ## Where to start The bind is shared by every write, and the two cases want different answers: a person the *scan* stands up should keep the null upstream leaves, and one `refresh_people` files should carry the stamp. So the mark belongs on the `Write` rather than on the kind — `jellyfin_core::item::Write::only_created` and its `saved` flag are the precedent for how a writer says which kind of write this is. ## How to know it is done A person refreshed once is not in `people_wanting_a_refresh`'s answer on the next run, and a person nothing could identify is not either. The second is the half that needs deciding rather than writing: a row that was never written cannot carry a stamp. ### 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.
Author
Collaborator

Landed in 6a774be, with corrections in b5de663.

The stamp is bound from the write rather than from the row's kind:
Write::refreshed says this write went over the row, for_a_name leaves a
person unrefreshed the way upstream leaves the column null, and a scan standing
one up still writes no stamp.

The half that wanted deciding. Upstream settles it rather than us:
MetadataService.cs:213 stamps on hasRefreshedMetadata && hasRefreshedImages,
and those go false on a provider that threw — not on one that answered with
nothing. So a name no database knows is a refresh that ran, and an ask that
failed is not. refreshed_person returns the two separately instead of
collapsing both to None; the people nothing knew are stamped where they stand
by mark_people_refreshed, and the ones the ask failed for are left to be asked
again.

Worth knowing: upstream's PeopleValidator passes MetadataRefreshMode.ValidationOnly,
so it runs no remote provider for a person at all and always stamps on the first
validation. Failed has no upstream analogue — retrying is this port's choice,
made because this port does ask.

The review found the fix was half of one. A person's stamp was being
overwritten with null by the next item crediting them: every read of a crediting
item rebuilds their row from the name, which carries no stamp, and the upsert
wrote it. So this was fixed only for a library where nothing is ever re-read —
one new film would send all twenty of its cast back to the sweep. For a person
alone the row now keeps the stamp it has where the write carries none; every
other kind is still written as the row describing it says, which is what lets
#155's folder gate clear one.

Five tests, each checked against the mutation it exists to catch: reverting the
bind, stamping every kind, dropping the person filter, making the stamp a no-op,
and re-reading a crediting item.

What is left is filed as #159 — a cancelled run discards the stamps it earned,
which is the only one of its three that loses work.

Landed in `6a774be`, with corrections in `b5de663`. The stamp is bound from the write rather than from the row's kind: `Write::refreshed` says this write went over the row, `for_a_name` leaves a person unrefreshed the way upstream leaves the column null, and a scan standing one up still writes no stamp. **The half that wanted deciding.** Upstream settles it rather than us: `MetadataService.cs:213` stamps on `hasRefreshedMetadata && hasRefreshedImages`, and those go false on a provider that *threw* — not on one that answered with nothing. So a name no database knows is a refresh that ran, and an ask that failed is not. `refreshed_person` returns the two separately instead of collapsing both to `None`; the people nothing knew are stamped where they stand by `mark_people_refreshed`, and the ones the ask failed for are left to be asked again. Worth knowing: upstream's `PeopleValidator` passes `MetadataRefreshMode.ValidationOnly`, so it runs no remote provider for a person at all and always stamps on the first validation. `Failed` has no upstream analogue — retrying is this port's choice, made because this port does ask. **The review found the fix was half of one.** A person's stamp was being overwritten with null by the next item crediting them: every read of a crediting item rebuilds their row from the name, which carries no stamp, and the upsert wrote it. So this was fixed only for a library where nothing is ever re-read — one new film would send all twenty of its cast back to the sweep. For a person alone the row now keeps the stamp it has where the write carries none; every other kind is still written as the row describing it says, which is what lets #155's folder gate clear one. Five tests, each checked against the mutation it exists to catch: reverting the bind, stamping every kind, dropping the person filter, making the stamp a no-op, and re-reading a crediting item. What is left is filed as #159 — a cancelled run discards the stamps it earned, which is the only one of its three that loses work.
Sign in to join this conversation.
No description provided.