An episode's playback state is filed under the wrong key #66

Closed
opened 2026-08-04 13:58:16 +02:00 by Claude · 1 comment
Collaborator

An episode's playback state is filed under its own id rather than its series'.

/Items/Latest over a production library:

upstream ours
UserData.Key on an episode 369844 607cef76-ab4e-fcd8-2248-60d94ce6cfa1

An episode's key is built from its series' external ids — this port already
knows that (user_data::keys::episode takes series_providers), and a movie's
key agrees with upstream exactly. What is missing is the loading: on the routes
that do not ask for provider ids, the series' rows are not read, so the
derivation falls back to the guid.

Upstream never has this problem — a BaseItem carries its ProviderIds in
memory from its Data blob, so GetUserDataKeys() always has them whatever the
request asked for.

Measured 3 occurrences in the browsing scenario, all episodes, all on
/Items/Latest. A movie on the same response matches.

This matters beyond a field: the key is what playback state is filed under, so
an episode's watched state is recorded against a different key from the one
upstream uses.

An episode's playback state is filed under its own id rather than its series'. `/Items/Latest` over a production library: | | upstream | ours | |---|---|---| | `UserData.Key` on an episode | `369844` | `607cef76-ab4e-fcd8-2248-60d94ce6cfa1` | An episode's key is built from its **series'** external ids — this port already knows that (`user_data::keys::episode` takes `series_providers`), and a movie's key agrees with upstream exactly. What is missing is the loading: on the routes that do not ask for provider ids, the series' rows are not read, so the derivation falls back to the guid. Upstream never has this problem — a `BaseItem` carries its `ProviderIds` in memory from its `Data` blob, so `GetUserDataKeys()` always has them whatever the request asked for. Measured 3 occurrences in the `browsing` scenario, all episodes, all on `/Items/Latest`. A movie on the same response matches. This matters beyond a field: the key is what playback state is filed under, so an episode's watched state is recorded against a different key from the one upstream uses.
Author
Collaborator

Fixed — and the issue text was wrong about which items were affected.

Episodes already agreed. The four failing keys were the series containers
that /Items/Latest reports a batch of episodes under.

The rule: BaseItemRepository.RetrieveItem always includes the provider rows,
where a query includes them only when DtoOptions asks
(BaseItemRepository.QueryBuilding.cs:227). /Items/Latest with no parent
groups by LatestItemsIndexContainer, which resolves the series through
GetItemById — a by-id read, so it carries its external ids whatever the
request asked for.

The Data-blob explanation in the issue does not hold: the series' blob in this
database carries no provider ids at all.

Verified without trusting a possibly-cached key — upstream's UserItemData
cache is per user and item, so the check ran against three user ids that had
never been browsed. Cold, upstream still reports 369844 for the container, and
the same series reached through a query reports its guid. Both now match.

Providers are loaded when a row was read by id or the request asked for them, so
assemble::many is untouched: /Items?Recursive=true&Limit=5000 is 1.89 s
before and 1.72 s after, within noise.

Upstream's typed-library latest paths fetch their entities without provider
rows, so those containers key on the guid — which is what this port already did,
and it is left alone.

Fixed — and the issue text was wrong about which items were affected. Episodes already agreed. The four failing keys were the **series containers** that `/Items/Latest` reports a batch of episodes under. The rule: `BaseItemRepository.RetrieveItem` always includes the provider rows, where a *query* includes them only when `DtoOptions` asks (`BaseItemRepository.QueryBuilding.cs:227`). `/Items/Latest` with no parent groups by `LatestItemsIndexContainer`, which resolves the series through `GetItemById` — a by-id read, so it carries its external ids whatever the request asked for. The `Data`-blob explanation in the issue does not hold: the series' blob in this database carries no provider ids at all. Verified without trusting a possibly-cached key — upstream's `UserItemData` cache is per user and item, so the check ran against three user ids that had never been browsed. Cold, upstream still reports `369844` for the container, and the same series reached through a query reports its guid. Both now match. Providers are loaded when a row was read by id or the request asked for them, so `assemble::many` is untouched: `/Items?Recursive=true&Limit=5000` is 1.89 s before and 1.72 s after, within noise. Upstream's typed-library latest paths fetch their entities without provider rows, so those containers key on the guid — which is what this port already did, and it is left alone.
Sign in to join this conversation.
No description provided.