feat(search): multi-select quick filters for search results and site releases (SEARCH-3)

- domain release-facets: bucket resolution / codec / release type / site / promotion / HDR /
  release group / season / episode; OR within a dimension, AND across; disjunctive counts
- GET /search/releases and GET /sites/{id}/releases accept qf and return facets; site list
  facets come from local source_release rows only (no site requests), paging stays consistent
- web QuickFilterBar shared by search results and site resources tab: per-dimension popover
  (bottom sheet on phones) with counts, removable tags, clear all, localStorage memory per page
- runbook: quick filter params and bucket definitions

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 20:52:29 +08:00
1 parent d282212a6b
commit 970296218c
20 files changed
+1273 -81

No files matched your search

+4 -3
View File
@@ -34,7 +34,7 @@ import type { ModuleApp, ServerContext } from "../types.ts";
import { matchActions } from "./actions.ts";
import { InternalDispatcher } from "./dispatch.ts";
import { localEntities, raceAbort, searchLocalMedia, TmdbSearch, withInventory } from "./media.ts";
import { decorate, facets, filterReleases, ReleaseFanout, sortReleases } from "./releases.ts";
import { decorate, facets, quickFilter, ReleaseFanout, sortReleases } from "./releases.ts";
import {
buildWorks,
detailSeasons,
@@ -278,7 +278,8 @@ export function registerSearchRoutes(app: ModuleApp, ctx: ServerContext, d: Sear
const terms = await releaseTerms(q);
const hit = await fanout.search(terms, await resolveSites(q.sites), { refresh: q.refresh === "true" });
const all = hit.fanout.items;
const filtered = sortReleases(filterReleases(all, q), q.sort, q.order);
const quick = quickFilter(all, q, d.now());
const filtered = sortReleases(quick.items, q.sort, q.order);
const offset = decodeCursor(q.cursor);
const pageItems = filtered.slice(offset, offset + q.limit);
const next = offset + q.limit < filtered.length ? encodeCursor(offset + q.limit) : null;
@@ -290,7 +291,7 @@ export function registerSearchRoutes(app: ModuleApp, ctx: ServerContext, d: Sear
nextCursor: next,
total: filtered.length,
unfilteredTotal: all.length,
facets: facets(all),
facets: { ...facets(all), ...quick.facets },
partial: failed.length > 0,
sites: hit.fanout.sites,
failedSites: failed,