参考 MP 资源卡片:促销放在标题前(免费用 success,其余折扣中性加粗,带剩余时间); 规格拆成中性小标签(季集 → 分辨率 → 来源 → 编码 → HDR → 音频 → 语言 → 制作组); 站点 / 大小 / 做种·下载 / 发布时间宽屏定宽对齐、窄屏折行,做种加粗、为 0 时弱化; 结果涉及多部作品时按作品分组(组头 = 作品名 + 年份 + 库存状态),只有一部时不重复作品名; 每行显示服务端规则判定(命中第 N 档 / 不满足 / 与库里版本比较)。保留工作台、委托智能体、下载。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
204 lines
6.9 KiB
TypeScript
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();
|
|
});
|
|
});
|