Files
rootandClaude Opus 5.5 39ec8185e9 feat(web): 资源搜索结果改版(规格标签、促销前置、数字对齐、按作品分组、规则判定)
参考 MP 资源卡片:促销放在标题前(免费用 success,其余折扣中性加粗,带剩余时间);
规格拆成中性小标签(季集 → 分辨率 → 来源 → 编码 → HDR → 音频 → 语言 → 制作组);
站点 / 大小 / 做种·下载 / 发布时间宽屏定宽对齐、窄屏折行,做种加粗、为 0 时弱化;
结果涉及多部作品时按作品分组(组头 = 作品名 + 年份 + 库存状态),只有一部时不重复作品名;
每行显示服务端规则判定(命中第 N 档 / 不满足 / 与库里版本比较)。保留工作台、委托智能体、下载。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 12:04:37 +08:00

204 lines
6.9 KiB
TypeScript

import type { SearchRelease, SearchRuleVerdict } from "@umt/contracts";
import { describe, expect, it } from "vitest";
import { groupByWork, hrBadge, promoBadge, releaseSpecTags, verdictView, vsLibraryLabel } from "./search-labels.ts";
// SEARCH-5 资源行:规格标签拆分与顺序、促销 / H&R、按作品分组、规则判定文案。
const E1 = "11111111-1111-4111-8111-111111111111";
const E2 = "22222222-2222-4222-8222-222222222222";
const SLOT = "33333333-3333-4333-8333-333333333333";
const parsed = (p: Partial<SearchRelease["parsed"]> = {}): SearchRelease["parsed"] => ({
titleCn: null,
titleEn: null,
year: null,
season: null,
episodes: null,
resolution: null,
source: null,
videoCodec: null,
hdr: [],
releaseGroup: null,
...p,
});
const rel = (key: string, p: Partial<SearchRelease> = {}): SearchRelease => ({
key,
sourceReleaseId: null,
siteId: E1,
siteName: "站",
siteReleaseId: key,
title: key,
subtitle: null,
category: { id: null, name: null },
sizeBytes: null,
seeders: null,
leechers: null,
snatched: null,
promotion: { type: "none", downloadFactor: 1, uploadFactor: 1, expiresAt: null },
free: false,
hr: false,
uploadedAt: null,
detailUrl: "http://x/",
observedAt: "2026-10-01T00:00:00.000Z",
parsed: parsed(),
mediaEntityId: null,
entityMatch: null,
entity: null,
inventory: null,
ruleVerdict: null,
...p,
});
const verdict = (v: Partial<SearchRuleVerdict> = {}): SearchRuleVerdict => ({
slotId: SLOT,
slotName: "主版本",
slotCount: 1,
outcome: "hit",
tier: 1,
tierName: "画质·第1层",
failed: [],
details: [],
vsLibrary: null,
...v,
});
describe("规格标签", () => {
it("固定顺序:季集 → 分辨率 → 来源 → 编码 → HDR → 音频 → 语言 → 制作组", () => {
const tags = releaseSpecTags(
parsed({
season: 1,
episodes: { start: 1, end: 8 },
releaseGroup: "CHDWEB",
subtitles: ["zh-Hans", "en"],
audioLangs: ["cmn"],
audio: ["DDP 5.1", "Atmos"],
hdr: ["DV", "HDR10"],
videoCodec: "H.265",
source: "WEB-DL",
resolution: "2160p",
}),
);
expect(tags.map((t) => t.label)).toEqual([
"S01E01-E08",
"2160p",
"WEB-DL",
"H.265",
"DV",
"HDR10",
"DDP 5.1",
"Atmos",
"国语",
"简中",
"英字",
"CHDWEB",
]);
expect(tags.map((t) => t.dim)).toEqual([
"episode",
"resolution",
"source",
"codec",
"hdr",
"hdr",
"audio",
"audio",
"lang",
"lang",
"lang",
"group",
]);
expect(tags[1]!.title).toBe("分辨率:2160p");
});
it("没解析出的维度不出标签;同维度同名去重;多季合集写 S01-S03", () => {
expect(releaseSpecTags(parsed())).toEqual([]);
expect(releaseSpecTags(parsed({ hdr: ["DV", "DV"] })).map((t) => t.label)).toEqual(["DV"]);
expect(releaseSpecTags(parsed({ season: 1, seasonEnd: 3 }))[0]!.label).toBe("S01-S03");
});
});
describe("促销与 H&R", () => {
it("免费 / 2x 免费用 success;50%、2x 上传中性;无促销不显示", () => {
expect(promoBadge(rel("a", { promotion: { ...rel("a").promotion, type: "free" }, free: true }))).toEqual({
label: "免费",
tone: "success",
});
expect(promoBadge(rel("a", { promotion: { ...rel("a").promotion, type: "2xfree" }, free: true }))).toEqual({
label: "2x 免费",
tone: "success",
});
expect(promoBadge(rel("a", { promotion: { ...rel("a").promotion, type: "50%" } }))).toEqual({
label: "50%",
tone: "neutral",
});
expect(promoBadge(rel("a", { promotion: { ...rel("a").promotion, type: "2x" } }))?.tone).toBe("neutral");
expect(promoBadge(rel("a"))).toBeNull();
});
it("H&R:有 → warning;未知 → 中性;没有 → 不显示", () => {
expect(hrBadge({ hr: true })).toEqual({ label: "H&R", tone: "warning" });
expect(hrBadge({ hr: "unknown" })).toEqual({ label: "H&R 未知", tone: "neutral" });
expect(hrBadge({ hr: false })).toBeNull();
});
});
describe("按作品分组", () => {
const work = (id: string, title: string) => ({ id, title, year: 2024, kind: "movie" as const });
it("多部作品按首次出现分组,未关联的放最后", () => {
const rows = [
rel("a", { entity: work(E1, "沙丘2"), entityMatch: "alias" }),
rel("b"),
rel("c", { entity: work(E2, "奥本海默"), entityMatch: "resolved" }),
rel("d", { entity: work(E1, "沙丘2"), entityMatch: "resolved" }),
];
const { grouped, groups } = groupByWork(rows);
expect(grouped).toBe(true);
expect(groups.map((g) => [g.key, g.items.map((r) => r.key)])).toEqual([
[E1, ["a", "d"]],
[E2, ["c"]],
[null, ["b"]],
]);
// 组里有已识别的,组头不再标「未确认」
expect(groups[0]!.entityMatch).toBe("resolved");
});
it("只有一部作品 / 全部未关联时不分组", () => {
expect(
groupByWork([rel("a", { entity: work(E1, "沙丘2") }), rel("b", { entity: work(E1, "沙丘2") })]),
).toMatchObject({
grouped: false,
groups: [{ key: E1 }],
});
expect(groupByWork([rel("a"), rel("b")])).toMatchObject({ grouped: false, groups: [{ key: null }] });
expect(groupByWork([])).toEqual({ grouped: false, groups: [] });
});
});
describe("规则判定文案", () => {
it("命中档位 / 满足 / 不满足 / 无法判断", () => {
expect(verdictView(verdict())).toMatchObject({ label: "命中第 1 档", tone: "success" });
expect(verdictView(verdict({ tier: null, tierName: null })).label).toBe("满足收录条件");
const miss = verdictView(
verdict({ outcome: "miss", tier: null, failed: ["分辨率", "来源"], details: ["分辨率 720p 不在允许列表"] }),
);
expect(miss).toMatchObject({ label: "不满足:分辨率、来源", tone: "neutral" });
expect(miss.title).toContain("分辨率 720p 不在允许列表");
expect(verdictView(verdict({ outcome: "miss", tier: null, failed: ["分辨率", "来源", "编码", "HDR"] })).label).toBe(
"不满足:分辨率、来源、编码 等 4 项",
);
expect(verdictView(verdict({ outcome: "unknown", tier: null, failed: ["分辨率"] })).label).toBe("无法判断:分辨率");
});
it("多个收录版本时带收录版本名", () => {
expect(verdictView(verdict({ slotCount: 2, slotName: "4K" })).label).toBe("「4K」命中第 1 档");
});
it("与库里版本比较", () => {
expect(vsLibraryLabel(verdict({ vsLibrary: { cmp: "higher", steps: 1 } }))).toBe("比库里版本高 1 档");
expect(vsLibraryLabel(verdict({ vsLibrary: { cmp: "higher", steps: null } }))).toBe("比库里版本好");
expect(vsLibraryLabel(verdict({ vsLibrary: { cmp: "same", steps: 0 } }))).toBe("与库里版本同档");
expect(vsLibraryLabel(verdict({ vsLibrary: { cmp: "lower", steps: 2 } }))).toBe("不如库里版本");
expect(vsLibraryLabel(verdict())).toBeNull();
});
});