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

+14 -7
View File
@@ -3,6 +3,9 @@ import { Id, NonNegInt, Timestamp } from "./common.ts";
import { MediaCredit, MediaEntityStatus, MediaKind } from "./content.ts";
import { ManualAcquisitionRequest } from "./execution.ts";
import { InventoryEntry, MediaServerInventory, SlotStatusCounts } from "./organization.ts";
import { FacetCount, QuickFilterParam, ReleaseQuickFacets } from "./release-facets.ts";
export { FacetCount, QuickFilterParam, ReleaseQuickFacets } from "./release-facets.ts";
// 全局搜索(W9-G,对标 MP 顶栏搜索 + 资源搜索页):
// GET /search 聚合本地作品 / TMDB 候选 / 跨站资源 / 页面与操作;GET /search/releases 是资源结果页(筛选/排序/游标,复用同一缓存)。
@@ -68,6 +71,8 @@ export const SearchReleaseParsed = z.object({
titleEn: z.string().nullable(),
year: z.number().int().nullable(),
season: z.number().int().nullable(),
/** 多季合集的最后一季(S01-S03 = 3);单季 / 电影为 null */
seasonEnd: z.number().int().nullable().optional(),
episodes: z.object({ start: z.number().int(), end: z.number().int() }).nullable(),
resolution: z.string().nullable(),
source: z.string().nullable(),
@@ -264,10 +269,12 @@ export const ReleaseSearchQuery = z.object({
sites: csv,
/** 逗号分隔的站点 ID:只筛选结果(仍查 sites 里的全部站点,分面计数不变) */
siteFilter: csv,
/** 逗号分隔:2160p,1080p… */
/** 逗号分隔的解析原值:2160p,1080p…(旧参数;快速筛选用 qf) */
resolution: csv,
/** 逗号分隔:REMUX,WEB-DL,BluRay… */
/** 逗号分隔的解析原值:REMUX,WEB-DL,BluRay…(旧参数;快速筛选用 qf) */
source: csv,
/** 快速筛选(SEARCH-3):`维度:值,值;维度:值`,同维度 OR、不同维度 AND,见 ReleaseQuickFacets */
qf: QuickFilterParam,
/** 字节 */
minSize: z.coerce.number().int().nonnegative().optional(),
maxSize: z.coerce.number().int().nonnegative().optional(),
@@ -285,9 +292,6 @@ export const ReleaseSearchQuery = z.object({
});
export type ReleaseSearchQuery = z.infer<typeof ReleaseSearchQuery>;
export const FacetCount = z.object({ value: z.string(), label: z.string(), count: NonNegInt });
export type FacetCount = z.infer<typeof FacetCount>;
export const ReleaseSearchPage = z
.object({
q: z.string(),
@@ -299,8 +303,11 @@ export const ReleaseSearchPage = z
total: NonNegInt,
/** 未筛选的全部条数 */
unfilteredTotal: NonNegInt,
/** 筛选项计数(基于未筛选的全部结果) */
facets: z.object({
/**
* sites / resolutions / sources:旧的筛选项计数(基于未筛选的全部结果);
* 其余为快速筛选分面(SEARCH-3,在其它筛选条件之后、按其它维度的已选条件计数)
*/
facets: ReleaseQuickFacets.extend({
sites: z.array(FacetCount),
resolutions: z.array(FacetCount),
sources: z.array(FacetCount),