/Studios search orders two studios differently from upstream #122

Closed
opened 2026-08-14 00:15:43 +02:00 by Claude · 1 comment
Collaborator

Measured on the live pair during #91's review:

/Studios?searchTerm=Br
  upstream: Brandywine Productions, Warner Bros. Pictures, Shaw Brothers
  ours:     Brandywine Productions, Shaw Brothers, Warner Bros. Pictures

Both servers find the same three; they disagree about which of the two non-prefix matches comes first. This is distinct from the studio-picture difference crates/jellyfin-harness/README.md already names.

The likely root is the search ranking rather than the listing: a term the name starts with outranks one it merely contains, and among the ones that merely contain it upstream has a tiebreak this port does not reproduce. Warner Bros. contains Br at a word start; Shaw Brothers does too. So the tiebreak is something else — read it rather than guess.

Related and already settled once: closed issue #88 was "the search candidate cut runs over the whole narrowing, and there is no fallback when the ranking matches nothing", and #49 was "search hints come back in a different order". This is the same family and the fix probably belongs beside them.

Where to start

The by-name listing and its search ranking, crates/jellyfin-app/src/scan/by_name.rs and the discover routes in crates/jellyfin-http/src/routes/discover.rs. Upstream's ranking is in its search engine rather than in the controller.

How to know it is done

The measurement agrees, and a scenario carries a searchTerm over a by-name listing with more than one non-prefix match — the shape that finds this.

House rules

Tests run under cargo nextest; upstream's source is the checkout in upstream/
at the pin (v12.0-rc5, 6d501ba4); generated files are regenerated rather
than edited; comments say what the code does, not how it got there. The
reference pair is cargo xtask reference up, a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,
and crates/jellyfin-harness/README.md says what a diff means. A server started
off current main binds UDP 7359 for discovery — set [discovery] enabled = false
in a scratch config if another one is already running.

Measured on the live pair during #91's review: ``` /Studios?searchTerm=Br upstream: Brandywine Productions, Warner Bros. Pictures, Shaw Brothers ours: Brandywine Productions, Shaw Brothers, Warner Bros. Pictures ``` Both servers find the same three; they disagree about which of the two non-prefix matches comes first. This is distinct from the studio-picture difference `crates/jellyfin-harness/README.md` already names. The likely root is the search ranking rather than the listing: a term the name **starts with** outranks one it merely contains, and among the ones that merely contain it upstream has a tiebreak this port does not reproduce. `Warner Bros.` contains `Br` at a word start; `Shaw Brothers` does too. So the tiebreak is something else — read it rather than guess. Related and already settled once: closed issue #88 was "the search candidate cut runs over the whole narrowing, and there is no fallback when the ranking matches nothing", and #49 was "search hints come back in a different order". This is the same family and the fix probably belongs beside them. ## Where to start The by-name listing and its search ranking, `crates/jellyfin-app/src/scan/by_name.rs` and the discover routes in `crates/jellyfin-http/src/routes/discover.rs`. Upstream's ranking is in its search engine rather than in the controller. ## How to know it is done The measurement agrees, and a scenario carries a `searchTerm` over a by-name listing with more than one non-prefix match — the shape that finds this. ### House rules Tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin** (`v12.0-rc5`, `6d501ba4`); generated files are regenerated rather than edited; comments say what the code does, not how it got there. The reference pair is `cargo xtask reference up`, a run is `cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR`, and `crates/jellyfin-harness/README.md` says what a diff means. A server started off current `main` binds UDP 7359 for discovery — set `[discovery] enabled = false` in a scratch config if another one is already running.
Author
Collaborator

Done, on main.

/Studios?searchTerm=Br  upstream  Brandywine, Warner Bros., Shaw Brothers
                        before    Brandywine, Shaw Brothers, Warner Bros.
/Studios?searchTerm=o   upstream  Warner Bros., Shaw Brothers, Brandywine
                        before    Brandywine, Shaw Brothers, Warner Bros.

The ranking tiers were right; the tiebreak was not. BaseItemRepository.QueryBuilding.cs:366 adds the trailing SortName key only if (!hasSearch), so a search suppresses it and ties fall out in the order WHERE Id IN (...) yields — primary-key order. We were sorting ties by name.

Confirmed on a listing with four tied rows: /Genres?searchTerm=r gives Drama, Thriller, Adventure, Mystery on both — id order, conspicuously not alphabetical — while searchTerm=d gives Drama, Adventure, prefix outranking contains.

One thing left deliberately: by_relevance ranks on the folded name where upstream's MapSearchRelevanceOrder also matches OriginalTitle. A by-name row stands for an interned value and carries none, so both halves of every tier consulting it are unreachable — and the code now says so rather than leaving it silent.


The batch this closed in

Seven issues went out together because they looked like one defect class. They turned out to be three roots:

  • #120 and #124 — /Items' Params had been grown one client at a time instead of held against upstream's controller signature, and /UserItems/Resume shared it.
  • #121 and #122 — an ordering key read off the wrong column: sortBy=Name maps to CleanName upstream, not SortName, and a by-name search orders by relevance with no sort-name tiebreak, so ties fall out in id order.
  • #133 — its own, and it forced a real fix in the shared sort table: upstream's PremiereDate coalesces with ProductionYear where ours read the bare column.

A reviewer re-derived the parameter audit mechanically — extracting every [FromQuery] name from GetItems and doing set arithmetic against what we bind — and got nothing bound that upstream does not take and nothing in the register that is not an upstream parameter. This class is closed rather than narrowed, which matters because it had been found three times.

Verified on a restored pair: browsing 34 → 0, items 33 requests at 0, clients 56 → 3, portable 115 → 6. JELLYFIN_RS_REQUIRE_FIXTURES=1 cargo nextest run --workspace: 2228 passed, 0 skipped. Clippy clean. No ignore-list entry was added or touched.

Two things the review caught that the batch had not

Five of the ten new scenario entries could not fail. The default snapshot has no genres, studios or tags, so five items.json entries compared two empty pages — reverting the whole fix would still have replayed clean. They moved into editing.json, after the save that gives one film a genre, a tag, a certificate and a studio and then takes them away, so each selects one row out of nineteen. That is the fifth time this session the same failure has turned up in a different layer, after M5's vacuous scenarios, M8's bundle.json, #111's all-refusals visibility.json and #137's silently-skipping tests.

Reading the reference can write to it. Studios=, Artists= and Albums= resolve through upstream's GetStudio/GetArtist, which call CreateItemByName — so a read files a BaseItems row. That is how a stray Brandywine Productions got into the shared fixture mid-session and made portable.json disagree by 149. Genres, Tags and OfficialRatings narrow through ItemValues and are safe. Both halves are now written into crates/jellyfin-harness/README.md.

Done, on `main`. ``` /Studios?searchTerm=Br upstream Brandywine, Warner Bros., Shaw Brothers before Brandywine, Shaw Brothers, Warner Bros. /Studios?searchTerm=o upstream Warner Bros., Shaw Brothers, Brandywine before Brandywine, Shaw Brothers, Warner Bros. ``` The ranking tiers were right; the tiebreak was not. `BaseItemRepository.QueryBuilding.cs:366` adds the trailing `SortName` key **only** `if (!hasSearch)`, so a search suppresses it and ties fall out in the order `WHERE Id IN (...)` yields — primary-key order. We were sorting ties by name. Confirmed on a listing with four tied rows: `/Genres?searchTerm=r` gives `Drama, Thriller, Adventure, Mystery` on both — id order, conspicuously not alphabetical — while `searchTerm=d` gives `Drama, Adventure`, prefix outranking contains. One thing left deliberately: `by_relevance` ranks on the folded name where upstream's `MapSearchRelevanceOrder` also matches `OriginalTitle`. A by-name row stands for an interned value and carries none, so both halves of every tier consulting it are unreachable — and the code now says so rather than leaving it silent. --- ## The batch this closed in Seven issues went out together because they looked like one defect class. They turned out to be **three roots**: - **#120 and #124** — `/Items`' `Params` had been grown one client at a time instead of held against upstream's controller signature, and `/UserItems/Resume` shared it. - **#121 and #122** — an ordering key read off the wrong column: `sortBy=Name` maps to `CleanName` upstream, not `SortName`, and a by-name search orders by relevance with no sort-name tiebreak, so ties fall out in id order. - **#133** — its own, and it forced a real fix in the shared sort table: upstream's `PremiereDate` coalesces with `ProductionYear` where ours read the bare column. A reviewer re-derived the parameter audit mechanically — extracting every `[FromQuery]` name from `GetItems` and doing set arithmetic against what we bind — and got nothing bound that upstream does not take and nothing in the register that is not an upstream parameter. **This class is closed rather than narrowed**, which matters because it had been found three times. Verified on a restored pair: `browsing` **34 → 0**, `items` 33 requests at 0, `clients` **56 → 3**, `portable` 115 → 6. `JELLYFIN_RS_REQUIRE_FIXTURES=1 cargo nextest run --workspace`: **2228 passed, 0 skipped**. Clippy clean. No ignore-list entry was added or touched. ### Two things the review caught that the batch had not **Five of the ten new scenario entries could not fail.** The `default` snapshot has no genres, studios or tags, so five `items.json` entries compared two empty pages — reverting the whole fix would still have replayed clean. They moved into `editing.json`, after the save that gives one film a genre, a tag, a certificate and a studio and then takes them away, so each selects one row out of nineteen. That is the fifth time this session the same failure has turned up in a different layer, after M5's vacuous scenarios, M8's `bundle.json`, #111's all-refusals `visibility.json` and #137's silently-skipping tests. **Reading the reference can write to it.** `Studios=`, `Artists=` and `Albums=` resolve through upstream's `GetStudio`/`GetArtist`, which call `CreateItemByName` — so a *read* files a `BaseItems` row. That is how a stray `Brandywine Productions` got into the shared fixture mid-session and made `portable.json` disagree by 149. `Genres`, `Tags` and `OfficialRatings` narrow through `ItemValues` and are safe. Both halves are now written into `crates/jellyfin-harness/README.md`.
Sign in to join this conversation.
No description provided.