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:
1 parent
d282212a6b
commit
970296218c
20 files changed
+1273
-81
No files matched your search
@@ -396,6 +396,90 @@ describe.skipIf(!up)("全局搜索", () => {
|
||||
expect(p2.nextCursor).toBeNull();
|
||||
});
|
||||
|
||||
it("快速筛选 qf(SEARCH-3):分面计数、同维度 OR / 跨维度 AND、分页一致", async () => {
|
||||
const q = (qf: string, extra = "") =>
|
||||
req(`/search/releases?q=Dune&qf=${encodeURIComponent(qf)}${extra}`).then((r) => r.body.data);
|
||||
const counts = (opts: Loose[]) => Object.fromEntries(opts.map((o) => [o.value, o.count]));
|
||||
const ids = (d: Loose) => d.items.map((i: Loose) => i.siteReleaseId).sort();
|
||||
|
||||
let d = await q("");
|
||||
expect(d.total).toBe(3);
|
||||
expect(counts(d.facets.resolution)).toEqual({ "2160p": 2, "1080p": 1, "720p": 0 });
|
||||
expect(counts(d.facets.videoCodec)).toEqual({ "H.265": 2, "H.264": 1, AV1: 0 });
|
||||
expect(counts(d.facets.releaseType)).toEqual({ disc: 0, remux: 1, encode: 0, webdl: 2, hdtv: 0 });
|
||||
expect(counts(d.facets.site)).toEqual({ [sites.nexus]: 2, [sites.mteam]: 1 });
|
||||
expect(d.facets.site.find((f: Loose) => f.value === sites.nexus).label).toBe("观众");
|
||||
expect(counts(d.facets.releaseGroup)).toEqual({ FraMeSToR: 1, GRP: 1, FLUX: 1 });
|
||||
expect(d.facets.hdr.find((f: Loose) => f.value === "SDR").count).toBe(2);
|
||||
expect(d.facets.season).toEqual([]);
|
||||
// 旧分面仍在
|
||||
expect(d.facets.resolutions.length).toBe(2);
|
||||
|
||||
d = await q("resolution:2160p");
|
||||
expect(ids(d)).toEqual(["812345", "990001"]);
|
||||
expect(d).toMatchObject({ total: 2, unfilteredTotal: 3 });
|
||||
// 本维度其它选项仍按其它条件计数
|
||||
expect(counts(d.facets.resolution)).toMatchObject({ "2160p": 2, "1080p": 1 });
|
||||
expect(counts(d.facets.releaseType)).toMatchObject({ remux: 1, webdl: 1 });
|
||||
|
||||
d = await q("resolution:2160p;releaseType:webdl");
|
||||
expect(ids(d)).toEqual(["990001"]);
|
||||
d = await q(`resolution:2160p,1080p;site:${sites.nexus}`);
|
||||
expect(ids(d)).toEqual(["812345", "900001"]);
|
||||
d = await q("releaseGroup:flux,grp");
|
||||
expect(ids(d)).toEqual(["900001", "990001"]);
|
||||
d = await q("hdr:DV");
|
||||
expect(ids(d)).toEqual(["812345"]);
|
||||
d = await q("videoCodec:AV1");
|
||||
expect(d.items).toEqual([]);
|
||||
expect(d.total).toBe(0);
|
||||
// 与旧参数叠加(AND)
|
||||
d = await q("resolution:2160p", "&source=REMUX");
|
||||
expect(ids(d)).toEqual(["812345"]);
|
||||
|
||||
// 分页:同一筛选下逐页取完 = 一次取完,且排序一致
|
||||
const whole = await q("videoCodec:H.265,H.264", "&sort=size&limit=50");
|
||||
const seen: string[] = [];
|
||||
let cursor: string | null = null;
|
||||
do {
|
||||
const p: Loose = await q("videoCodec:H.265,H.264", `&sort=size&limit=1${cursor ? `&cursor=${cursor}` : ""}`);
|
||||
expect(p.total).toBe(3);
|
||||
expect(p.facets.videoCodec).toEqual(whole.facets.videoCodec);
|
||||
seen.push(...p.items.map((i: Loose) => i.siteReleaseId));
|
||||
cursor = p.nextCursor;
|
||||
} while (cursor);
|
||||
expect(seen).toEqual(whole.items.map((i: Loose) => i.siteReleaseId));
|
||||
});
|
||||
|
||||
it("站点资源列表的分面基于本地数据,不请求站点", async () => {
|
||||
const before = [nexusSearches(), mteamSearches()];
|
||||
const list = (qf: string) =>
|
||||
req(`/sites/${sites.nexus}/releases?q=Dune&live=false&qf=${encodeURIComponent(qf)}`).then((r) => r.body.data);
|
||||
let d = await list("");
|
||||
expect(d.total).toBe(2);
|
||||
expect(d.unfilteredTotal).toBe(2);
|
||||
expect(d.facets.site).toEqual([{ value: sites.nexus, label: "观众", count: 2 }]);
|
||||
expect(d.facets.resolution.find((f: Loose) => f.value === "2160p").count).toBe(1);
|
||||
expect(d.facets.releaseType.find((f: Loose) => f.value === "remux").count).toBe(1);
|
||||
d = await list("resolution:1080p");
|
||||
expect(d.items.map((i: Loose) => i.siteReleaseId)).toEqual(["900001"]);
|
||||
expect(d).toMatchObject({ total: 1, unfilteredTotal: 2 });
|
||||
expect(d.facets.releaseGroup.map((f: Loose) => [f.value, f.count])).toEqual([
|
||||
["FraMeSToR", 0],
|
||||
["GRP", 1],
|
||||
]);
|
||||
// 分页
|
||||
const p1 = await req(`/sites/${sites.nexus}/releases?q=Dune&live=false&limit=1`).then((r) => r.body.data);
|
||||
expect(p1.items.length).toBe(1);
|
||||
const p2 = await req(`/sites/${sites.nexus}/releases?q=Dune&live=false&limit=1&cursor=${p1.nextCursor}`).then(
|
||||
(r) => r.body.data,
|
||||
);
|
||||
expect(p2.items.length).toBe(1);
|
||||
expect(p2.items[0].id).not.toBe(p1.items[0].id);
|
||||
expect(p2.nextCursor).toBeNull();
|
||||
expect([nexusSearches(), mteamSearches()]).toEqual(before);
|
||||
});
|
||||
|
||||
it("H&R 筛选:true 只要明确 H&R;false 只排除明确 H&R(unknown 保留)", () => {
|
||||
const base = (id: string, hr: boolean | "unknown") => ({ ...sample, key: id, siteReleaseId: id, hr });
|
||||
const items = [base("a", true), base("b", false), base("c", "unknown")];
|
||||
|
||||
Reference in new issue
Block a user