Files
jellytau/docs/specs/scoped-search.md
T
dtourolle 8b028b6b60 docs: specs, requirements, ux-flows and traceability for new features
Add specs for the account menu, downloads-as-offline-library, offline
downloaded-only filter, and scoped search (+ boundary revision). Add the
new UR/DR entries to requirements.md, update ux-flows, and regenerate the
traceability matrix.

TRACES: UR-049, UR-050, UR-052, UR-053, UR-054, UR-055, UR-056
2026-07-23 20:04:35 +02:00

8.9 KiB

Spec: Context-scoped search with filter chips and configurable group order

⚠️ Superseded in part by scoped-search-boundary.md. The "frontend only, no Rust changes" decision below (§Background 2, §Design "Scope model" and "Threading scope through the store") left Jellyfin's item-type taxonomy in the presentation layer, which violates the backend/frontend boundary. The taxonomy is being moved into Rust. The user-facing behaviour and UX in this spec are unchanged; only where the scope→item-type mapping and result bucketing live changes. Read the boundary spec before touching search code.

Status: Implemented (boundary revision pending — see banner above) Scope: Frontend only. No Rust changes required. (Revised — see banner.) Requirements: UR-049 → DR-063, DR-064, DR-065; UR-050 → DR-066, DR-067 (see requirements.md). UX spec: ux-flows.md §6 — §6.1 scope, §6.2 layout, §6.3 group order, §6.4 current deviations.

Summary

Two related changes to search:

  1. Scope — a search started inside a library searches that library. Started from Home, /library, or the search tab, it searches everything. The active scope shows as a chip row under the search bar, preselected from context and freely changeable without retyping.
  2. Group order — the order result groups appear in (Songs, Albums, Artists, Movies, TV Shows) becomes a drag-and-drop setting instead of being hardcoded.

Motivation

Searching "office" while browsing TV currently returns music albums, because both search entry points call the same unscoped query. The user has already told us what they're looking at; ignoring that makes search feel indiscriminate and pushes the relevant result below unrelated media.

Background: what already exists

Verified in code — most of the plumbing is already there. This is substantially a wiring task, not new infrastructure.

  1. SearchOptions already carries the filter. bindings.tsSearchOptions = { limit?, includeItemTypes?, searchTerm? }.

  2. Rust already honours include_item_types on both paths — online (online.rs, in the get_items options mapping) and offline (offline.rs, which builds a SQL type filter from it). Do not add Rust code for filtering.

  3. Per-page list search already does this correctly. GenericMediaListPage.svelte passes includeItemTypes: [config.itemType] to repo.search(...). Use it as the reference for the call shape, including the requestId handling.

  4. The gap is exactly one function. library.tssearch(query) takes only a query and calls repo.search(query, { limit: 10000 }, requestId), dropping any scope. Both callers (search/+page.svelte and library/+layout.svelte) go through it.

  5. Group order is hardcoded in markup. SearchResults.svelte categorizes into music{tracks,albums,artists} / movies / tvShows and renders three fixed sections in source order.

  6. Frontend preferences persist via localStorage, per the existing viewMode precedent in library.ts (jellytau-view-mode). Follow that pattern — do not add a Rust settings command for this.

Design

Scope model

One SearchScope type, defined once and shared:

Scope includeItemTypes Chip label
all unset All
music MusicAlbum, MusicArtist, Audio, Playlist Music
movies Movie Movies
tv Series, Episode TV

all must send no includeItemTypes key rather than a list of every type — the two are not equivalent for item types not enumerated here (Person, folders).

Route → scope resolution (DR-063)

A pure function, unit-testable without a DOM:

resolveSearchScope(pathname: string): SearchScope
  • /library/music*music
  • /library/movies*movies
  • /library/tv*tv
  • /, /library, /search, anything else → all

Note /library/shows/genres exists as a route; treat shows as tv. Check the current route list before finalising — do not assume this table is exhaustive.

Scope is a starting point, not a lock (DR-064)

The resolved scope sets the initial chip only. Once the user taps a chip, their choice governs until they leave the search surface. Concretely: derive the initial value from the route, hold it in component state, and do not re-derive it on every navigation — otherwise a user who widens to All snaps back to TV.

Changing a chip re-runs the current query at the new scope. Changing the query keeps the current scope.

Threading scope through the store (DR-065)

Extend the store's search signature to accept an optional scope and pass includeItemTypes down to repo.search. Preserve the existing behaviour exactly: the requestId bump, the stale-response guard, the search-event listener merge, the 10s timeout, and the empty-query clear path. This is an additive parameter — no caller should break.

Group order (DR-066, DR-067)

Persist an ordered array of group ids:

["songs", "albums", "artists", "movies", "tvShows"]   // shipped default

Rendering composes scope and order as two independent axes, in this order:

  1. drop groups outside the active scope,
  2. sort the remainder by the user's saved order,
  3. omit groups that came back empty.

Scope never rewrites the saved order — narrowing to Music and back to All must restore the user's full arrangement. See ux-flows.md §6.3 for the worked example.

Settings gets a reorderable list. Dragging alone is not sufficient: provide keyboard-operable move up/down controls with proper labels, or the setting is unusable with a screen reader and on any pointerless input.

Unknown or missing ids in the stored array must not crash rendering — treat the stored order as a hint, append any group it doesn't mention, and ignore ids that no longer exist. A user upgrading from a build with fewer groups must not lose the new ones.

Out of scope

  • Ranking within a group. Order is presentation-only.
  • Server-side search ranking or the Jellyfin query itself.
  • Scope chips on the per-page list search in GenericMediaListPage — that page is already implicitly scoped by its own itemType.
  • Any Rust change.

Acceptance criteria

  • Searching from inside Music returns no movies or TV; from inside TV, no music.
  • Searching from Home, /library, or the search tab returns all types.
  • The chip row renders under the search bar on both the search page and the in-library header search, with the context-derived chip preselected.
  • Tapping a chip re-runs the search with the query preserved; editing the query preserves the selected chip.
  • Tapping "All" from a context-scoped search widens results without retyping.
  • Result groups render in the user's configured order, with out-of-scope and empty groups omitted and relative order preserved.
  • Group order is reorderable by drag and by keyboard, persists across restarts, and ships with the documented default.
  • Offline search respects scope (the offline path already filters — verify, don't reimplement).
  • bun run check and bun run test pass.

Testing

Follow the existing frontend test conventions (vitest, src/lib/**/*.test.ts).

  • resolveSearchScope — pure unit tests over the route table, including the /library/shows/genres case and unknown routes falling back to all.
  • Scope → includeItemTypes mapping, asserting all omits the key entirely.
  • The compose step: scope filter + user order + empty-group omission, including the "narrow then widen restores order" case and a stored order containing an unknown id.
  • Store-level: scoped search forwards includeItemTypes to the repository, and the existing stale-requestId guard still discards superseded responses.

New requirement-implementing code needs TRACES: comments — see CLAUDE.md. Suggested tags: the scope resolver and chip row UR-049 | DR-063, DR-064, the store change UR-049 | DR-065, the settings list and ordered rendering UR-050 | DR-066, DR-067.

Notes for the implementer

  • Read ux-flows.md §6 first — it is the behavioural spec; this document is the implementation plan.
  • The IPC camelCase rule applies to anything new that crosses the boundary (CLAUDE.md) — though this change should not add commands.
  • Another session may be active in this repo. Check git diff before "repairing" unexpected changes.