0.3.27 - Omni search fixes, Services in genre browser..

This commit is contained in:
ponzischeme89
2026-08-26 08:42:56 +12:00
parent 919a12f8d5
commit 9d11bb7282
26 changed files with 2202 additions and 590 deletions
+70 -5
View File
@@ -703,11 +703,76 @@ of a copy, wrong the moment Tracearr corrects a session. It is read two ways:
6×6 on-screen keyboard on the left, with `SearchGenres` above `SearchResults` on the right.
The results list updates as you type and uses the same shared `MediaResultCard` presentation
as My Requests; request status and actions remain feature-owned slots around that shared row.
Nothing is ever "submitted". `SearchViewModel` runs one pipeline — `debounce(250)`
`trim``distinctUntilChanged``collectLatest { repository.search(it) }` — and
`collectLatest` is the load-bearing part: it cancels the in-flight request, so a slow
response for a prefix can never overwrite the results for what was typed after it.
Searching starts at two characters (`shouldSearch`); one letter matches half a library.
**It is two pipelines, and the whole design is that they never wait for each other.**
`SearchViewModel` runs a *library* search on `debounce(250)``trim`
`distinctUntilChanged``collectLatest { repository.search(it) }`, where `collectLatest`
is the load-bearing part: it cancels the in-flight request, so a slow response for a prefix
can never overwrite the results for what was typed after it. Searching starts at two
characters (`shouldSearch`); one letter matches half a library. Beside it runs *external
discovery* — `GET /v1/search/discover`, Sonarr and Radarr — which used to ride the same
per-keystroke stream and therefore sent nine lookups to two of somebody else's services on
the way to typing one title. Things to preserve:
- **Discovery is deliberately hard to trigger, in four independent ways.** A longer
debounce (`DISCOVERY_DEBOUNCE_MS`, 700ms, several times the library's — at 250ms most of
the gaps between presses on a remote look like the end of a word), a three-character
floor (`MIN_DISCOVERY_QUERY_LENGTH`, where the library's is two), a `distinctUntilChanged`
over the **normalised** query, and a session cache. Any one of them removed puts the
*arrs back on the keystroke path.
- **`normaliseDiscoveryQuery` exists twice** — `data/SearchDiscovery.kt` and the gateway's
`normaliseDiscoveryQuery` — and the two must agree, because it is what the server's
cooldown is keyed on: "Disclosure", "disclosure" and "disclosure " have to reach the
cache rather than Radarr.
- **Pressing Search, and finishing a voice query, skip the debounce.** They are somebody
saying they have finished typing, which is the one thing a timer is guessing at — so the
keyboard's Search key and `onVoiceResult` both go through `submitQuery`, which runs the
library search immediately and emits into `submissions`, the flow merged past the
debounce. A completed transcription is a submitted query, never a keystroke.
- **The cooldown is the server's, keyed on `cache.MetadataKey`.** What Radarr knows about a
title is a fact about the world and the household's catalogue with nothing derived from
whoever typed it, so one lookup answers every television for `discoveryCacheTTL` (10
minutes; `discoveryEmptyTTL`, 2, for a lookup that found nothing, because nothing is the
answer for a typo and typos get corrected). `cachedRead` puts a singleflight in front of
it, so two sets missing the same key in the same second make one lookup. Whether *this*
viewer may press Request is stamped on the way out (`applyRequestable`) rather than baked
into what the household shares — the `MetadataKey` rule, exactly.
- **`state.discovery` is kept apart from `state.results`.** That separation is what makes
every promise below it true: the library list cannot be reordered by a late answer,
nothing on screen moves when the second section arrives, and it is simply appended under
whatever the viewer is already looking at. Merging the two lists is how a focused card
comes to jump.
- **A title is never offered twice.** The gateway drops anything its own catalogue answers
for (`discoverSonarr` / `discoverRadarr` skip `inLibrary`); `discoverySection` on the
television is the other half, for a title Emby holds that the import has not caught up
with. Both match on a stable id first (`searchIdentity`: Tmdb, Tvdb, Imdb) and fall back
to title-and-type, which is deliberately the weakest rule rather than the first tried.
`/v1/search` asks Emby for `ProviderIds` for exactly this — hence the `search:v2:` cache
key.
- **Sonarr or Radarr being down is never an error the viewer sees.** A failed leg leaves
the library section reading exactly as it does when everything works, and the section
says "Additional results unavailable" or says nothing. `partial` on the response is what
distinguishes "nothing else exists" from "we could not ask"; only the second is worth a
line.
- **A requestable result opens a page, and does not send a request.** `ui/
RequestDetailsOverlay.kt` is that page — the same backdrop, poster, fact line and button
language every other detail page has, deliberately *not* the ordinary movie or series
layout, which is built around a file that exists and would carry Play, a resume bar and
watched state as four lies arranged as furniture. It is the judgement
`RadarrMovieDetailsOverlay` already makes for the schedule row, and the two are built
from the same pieces. It is stateless — poster and message are parameters — so
`SearchSectionsScreenshotTest` renders it with no gateway.
- **It replaced the request *mode*.** Search used to carry a Request chip in its heading
that swapped the results pane for a grid of `lookupMediaRequests` candidates. Discovery
is that feature, inline and progressive, and one thing reachable two ways is one thing
whose two entry points drift apart.
- **`ui/search/SearchTrace.kt` is where the time went** (`adb logcat -s MembySearch`),
debug-only like `StartupTrace`. The most useful line is `event=discovery_skipped`: an
external lookup that never happened is the feature *working*, and without a record of why
it is indistinguishable from one that silently failed. It logs the query's length rather
than the query.
- **The gateway's own line is DEBUG**, for the reason the search line is, and carries the
per-source counts, durations and cache verdicts plus how many duplicates the merge
removed.
`rankSearchResults` is a pure, stable sort that only lifts exact/prefix/word-boundary
title matches above the backend's own relevance order — it never re-sorts alphabetically,
and it keeps weak matches rather than showing an empty pane. A small access-ordered map