357 lines
16 KiB
TypeScript
357 lines
16 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
evaluateCandidate,
|
|
type ParsedRelease,
|
|
parseRelease,
|
|
parseRuleString,
|
|
type QualityFacts,
|
|
type RuleGroupSpec,
|
|
rankForSlot,
|
|
type SlotSpec,
|
|
seasonReleaseKey,
|
|
selectAcquisitionPolicy,
|
|
selectSeasonRelease,
|
|
} from "../src/index.ts";
|
|
|
|
const release = (title: string, facts: Partial<ParsedRelease> = {}): ParsedRelease => ({
|
|
...parseRelease(title),
|
|
...facts,
|
|
});
|
|
const group = (name: string, rule: string): RuleGroupSpec => ({ name, layers: parseRuleString(rule).layers });
|
|
const current: QualityFacts = release("Show.S01E01.1080p.WEB-DL.H265-OTHER");
|
|
const target = release("Show.S01E02.1080p.WEB-DL.H265-CHD");
|
|
const consistent: SlotSpec = {
|
|
name: "正片",
|
|
hard: { resolutions: ["1080p", "2160p"] },
|
|
rules: { groups: [group("允许收录", "1080P | 4K")] },
|
|
upgrade: { enabled: true, consistency: "season_release" },
|
|
};
|
|
|
|
describe("按分类选择默认收录策略", () => {
|
|
const policies = [
|
|
{ id: "global", scope: {} },
|
|
{ id: "series", scope: { mediaKinds: ["series" as const] } },
|
|
{ id: "animation", scope: { categories: { tv: ["动漫剧集"] } } },
|
|
];
|
|
|
|
it("订阅显式指定的策略覆盖类型和分类范围", () => {
|
|
expect(
|
|
selectAcquisitionPolicy(policies, { explicitId: "series", mediaKind: "movie", category: "动画电影" })?.id,
|
|
).toBe("series");
|
|
});
|
|
|
|
it("显式指定的策略不存在时不悄悄回退", () => {
|
|
expect(
|
|
selectAcquisitionPolicy(policies, { explicitId: "missing", mediaKind: "series", category: "动漫剧集" }),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("分类匹配优先于类型默认,类型默认优先于全局默认", () => {
|
|
expect(selectAcquisitionPolicy(policies, { mediaKind: "series", category: "动漫剧集" })?.id).toBe("animation");
|
|
expect(selectAcquisitionPolicy(policies, { mediaKind: "series", category: "华语剧集" })?.id).toBe("series");
|
|
expect(selectAcquisitionPolicy(policies, { mediaKind: "movie", category: "动画电影" })?.id).toBe("global");
|
|
});
|
|
|
|
it.each(["movie", "series", "variety"] as const)("%s 不串用另一类型的同名分类策略", (mediaKind) => {
|
|
const scoped = [
|
|
{ id: "variety", scope: { mediaKinds: ["variety" as const], categories: { tv: ["同名分类"] } } },
|
|
{ id: "movie", scope: { categories: { movie: ["同名分类"] } } },
|
|
{ id: "series", scope: { mediaKinds: ["series" as const], categories: { tv: ["同名分类"] } } },
|
|
];
|
|
expect(selectAcquisitionPolicy(scoped, { mediaKind, category: "同名分类" })?.id).toBe(mediaKind);
|
|
});
|
|
|
|
it("同一范围按传入的最新发布顺序取第一个", () => {
|
|
const latest = { id: "latest", scope: { categories: { tv: ["动漫剧集"] } } };
|
|
const old = { id: "old", scope: { categories: { tv: ["动漫剧集"] } } };
|
|
expect(selectAcquisitionPolicy([latest, old], { mediaKind: "series", category: "动漫剧集" })).toBe(latest);
|
|
expect(selectAcquisitionPolicy([old, latest], { mediaKind: "series", category: "动漫剧集" })).toBe(old);
|
|
});
|
|
|
|
it("分类未知时跳过分类限定策略,仍可使用类型默认", () => {
|
|
expect(selectAcquisitionPolicy(policies, { mediaKind: "series", category: null })?.id).toBe("series");
|
|
});
|
|
|
|
it("类型或分类没有匹配且无默认时返回 undefined", () => {
|
|
const scoped = policies.slice(1);
|
|
expect(selectAcquisitionPolicy(scoped, { mediaKind: "movie", category: "动漫剧集" })).toBeUndefined();
|
|
expect(selectAcquisitionPolicy([policies[2]!], { mediaKind: "series", category: null })).toBeUndefined();
|
|
expect(selectAcquisitionPolicy([], { mediaKind: "variety", category: "综艺" })).toBeUndefined();
|
|
});
|
|
|
|
it("空的分类列表仍按类型默认处理", () => {
|
|
const typeDefault = { id: "type", scope: { mediaKinds: ["series" as const], categories: { movie: [], tv: [] } } };
|
|
expect(selectAcquisitionPolicy([policies[0]!, typeDefault], { mediaKind: "series", category: null })).toBe(
|
|
typeDefault,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("整季同套的规格与目标选择", () => {
|
|
it("发布组大小写、联合组顺序、HDR 顺序不产生新的版本身份", () => {
|
|
const first = {
|
|
...target,
|
|
releaseGroup: { name: "CHD@FLUX", source: "suffix" as const },
|
|
hdr: ["DV", "HDR10"] as const,
|
|
};
|
|
const second = {
|
|
...target,
|
|
releaseGroup: { name: "flux@chd", source: "known" as const },
|
|
hdr: ["HDR10", "DV"] as const,
|
|
};
|
|
expect(seasonReleaseKey({ ...first, hdr: [...first.hdr] })).toBe(
|
|
seasonReleaseKey({ ...second, hdr: [...second.hdr] }),
|
|
);
|
|
});
|
|
|
|
it.each(["resolution", "source", "videoCodec", "releaseGroup"] as const)(
|
|
"缺少 %s 时不声称属于完整成套版本",
|
|
(field) => {
|
|
expect(seasonReleaseKey({ ...target, [field]: undefined })).toBeNull();
|
|
},
|
|
);
|
|
|
|
it("来源、分辨率、编码和剪辑不同分别是不同的成套版本", () => {
|
|
for (const change of [
|
|
{ source: "BluRay" as const },
|
|
{ resolution: "2160p" as const },
|
|
{ videoCodec: "H.264" as const },
|
|
{ cut: "Extended" as const },
|
|
{ hdr: ["DV" as const] },
|
|
]) {
|
|
expect(seasonReleaseKey({ ...target, ...change })).not.toBe(seasonReleaseKey(target));
|
|
}
|
|
});
|
|
|
|
const unrestricted: SlotSpec = { name: "主版本", hard: {} };
|
|
it.each([
|
|
{
|
|
name: "来源等级优先于更高分辨率和软偏好",
|
|
spec: { ...unrestricted, soft: { groups: ["CHD"] } },
|
|
weaker: release("Show.S01E01.2160p.WEB-DL.H265-CHD"),
|
|
better: release("Show.S01E02.1080p.BluRay.H264-OTHER"),
|
|
},
|
|
{
|
|
name: "同来源时分辨率优先于软偏好",
|
|
spec: { ...unrestricted, soft: { groups: ["CHD"] } },
|
|
weaker: target,
|
|
better: release("Show.S01E02.2160p.WEB-DL.H265-OTHER"),
|
|
},
|
|
{
|
|
name: "同档规格按发布组软偏好排序",
|
|
spec: { ...unrestricted, soft: { groups: ["CHD", "OTHER"] } },
|
|
weaker: release("Show.S01E01.1080p.WEB-DL.H265-OTHER"),
|
|
better: target,
|
|
},
|
|
{
|
|
name: "同档规格按 HDR 软偏好排序",
|
|
spec: { ...unrestricted, soft: { hdr: ["DV" as const, "HDR10" as const] } },
|
|
weaker: release("Show.S01E01.1080p.WEB-DL.HDR.H265-CHD"),
|
|
better: release("Show.S01E02.1080p.WEB-DL.DV.H265-CHD"),
|
|
},
|
|
{
|
|
name: "同档规格按编码软偏好排序",
|
|
spec: { ...unrestricted, soft: { codecs: ["H.265" as const, "H.264" as const] } },
|
|
weaker: release("Show.S01E01.1080p.WEB-DL.H264-CHD"),
|
|
better: target,
|
|
},
|
|
])("$name,成套选择与候选排序一致", ({ spec, weaker, better }) => {
|
|
expect(rankForSlot(spec, [weaker, better])[0]?.candidate).toBe(better);
|
|
expect(selectSeasonRelease(spec, [weaker, better])).toBe(better);
|
|
expect(selectSeasonRelease(spec, [better, weaker])).toBe(better);
|
|
});
|
|
|
|
it("组合优先级先于来源和分辨率等级", () => {
|
|
const spec: SlotSpec = { ...unrestricted, rules: { groups: [group("来源偏好", "WEBDL > BLURAY")] } };
|
|
const blue = release("Show.S01E01.2160p.BluRay.H265-OTHER");
|
|
expect(rankForSlot(spec, [blue, target])[0]?.candidate).toBe(target);
|
|
expect(selectSeasonRelease(spec, [blue, target])).toBe(target);
|
|
});
|
|
|
|
it("规格不完整的版本不参与成套选择", () => {
|
|
const incomplete = { ...target, videoCodec: undefined, pri: 1000 };
|
|
expect(selectSeasonRelease(unrestricted, [incomplete, target])).toBe(target);
|
|
expect(selectSeasonRelease(unrestricted, [incomplete])).toBeUndefined();
|
|
});
|
|
|
|
it("同档没有偏好时目标不随数据库返回顺序反复变化", () => {
|
|
const other = release("Show.S01E02.1080p.WEB-DL.H265-OTHER");
|
|
expect(selectSeasonRelease(unrestricted, [target, other])).toBe(selectSeasonRelease(unrestricted, [other, target]));
|
|
});
|
|
});
|
|
|
|
describe("追更和洗版分别使用规则", () => {
|
|
const phased: SlotSpec = {
|
|
name: "主版本",
|
|
hard: { resolutions: ["1080p", "2160p"] },
|
|
rules: { groups: [group("追更", "WEBDL")] },
|
|
upgradeRules: { groups: [group("洗版", "BLURAY > WEBDL")] },
|
|
upgrade: { enabled: true },
|
|
};
|
|
const blue = release("Show.S01E02.1080p.BluRay.H265-CHD");
|
|
|
|
it("补齐缺集按追更规则,洗版规则不提前过滤", () => {
|
|
expect(evaluateCandidate(phased, target)).toMatchObject({ decision: "accept", pri: 100 });
|
|
expect(evaluateCandidate(phased, blue)).toMatchObject({
|
|
decision: "reject",
|
|
violations: [expect.objectContaining({ field: "rule", expected: "追更" })],
|
|
});
|
|
});
|
|
|
|
it("已有当前资产时改用洗版规则,并按洗版规则重算当前档位", () => {
|
|
expect(evaluateCandidate(phased, blue, current)).toMatchObject({ decision: "upgrade", pri: 100, fromPri: 99 });
|
|
expect(evaluateCandidate(phased, target, current)).toMatchObject({
|
|
decision: "reject",
|
|
violations: [expect.objectContaining({ code: "not_higher_priority" })],
|
|
});
|
|
});
|
|
|
|
it("洗版专用硬条件不阻止追更,但在已有资产时生效", () => {
|
|
const spec: SlotSpec = { ...phased, upgradeHard: { resolutions: ["2160p"] } };
|
|
expect(evaluateCandidate(spec, target).decision).toBe("accept");
|
|
expect(evaluateCandidate(spec, blue, current)).toMatchObject({
|
|
decision: "reject",
|
|
violations: [expect.objectContaining({ field: "resolution", code: "not_allowed" })],
|
|
});
|
|
});
|
|
|
|
it.each(["1080p", "2160p"] as const)("允许分辨率 %s 在追更和洗版阶段均可使用", (resolution) => {
|
|
const spec: SlotSpec = { name: "主版本", hard: phased.hard, upgrade: { enabled: true } };
|
|
const candidate = release(`Show.S01E02.${resolution}.BluRay.H265-CHD`);
|
|
expect(evaluateCandidate(spec, candidate).decision).toBe("accept");
|
|
expect(evaluateCandidate(spec, candidate, current).decision).toBe("upgrade");
|
|
});
|
|
|
|
it.each(["720p", "4320p"] as const)("范围外的分辨率 %s 不因更优来源或整季对齐而放行", (resolution) => {
|
|
const candidate = release(`Show.S01E02.${resolution}.BluRay.H265-CHD`);
|
|
expect(evaluateCandidate(consistent, candidate).decision).toBe("reject");
|
|
expect(evaluateCandidate(consistent, candidate, current, { seasonReleaseTarget: candidate })).toMatchObject({
|
|
decision: "reject",
|
|
violations: expect.arrayContaining([expect.objectContaining({ field: "resolution", code: "not_allowed" })]),
|
|
});
|
|
});
|
|
|
|
it("分辨率未知时保持待补充,不把未知当作允许范围", () => {
|
|
expect(evaluateCandidate(phased, { ...target, resolution: undefined })).toMatchObject({
|
|
decision: "unknown_field",
|
|
missing: ["resolution"],
|
|
});
|
|
});
|
|
|
|
it("组合条件的编码未知时,不能因 NOT 自动命中低档", () => {
|
|
const spec: SlotSpec = { name: "主版本", hard: {}, rules: { groups: [group("编码", "H265 > !H265")] } };
|
|
expect(evaluateCandidate(spec, release("Show.S01E02.1080p.WEB-DL-CHD"))).toMatchObject({
|
|
decision: "unknown_field",
|
|
missing: ["codec"],
|
|
});
|
|
});
|
|
|
|
it("高档分辨率已明确不满足时,无关的未知编码不阻止已匹配的低档", () => {
|
|
const spec: SlotSpec = {
|
|
name: "主版本",
|
|
hard: {},
|
|
rules: { groups: [group("组合档位", "4K & H265 > 1080P")] },
|
|
};
|
|
expect(evaluateCandidate(spec, release("Show.S01E02.1080p.WEB-DL-CHD"))).toMatchObject({
|
|
decision: "accept",
|
|
pri: 99,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("整季同套的同档对齐不绕过升级限制", () => {
|
|
it("同档的不同发布组可以逐集对齐到选定的整季版本", () => {
|
|
expect(evaluateCandidate(consistent, target, current, { seasonReleaseTarget: target })).toMatchObject({
|
|
decision: "upgrade",
|
|
pri: 100,
|
|
fromPri: 100,
|
|
reasons: expect.arrayContaining(["season_release_aligned"]),
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{ name: "来源", source: "BluRay" as const, resolution: "1080p" as const },
|
|
{ name: "分辨率", source: "WEB-DL" as const, resolution: "2160p" as const },
|
|
])("同档对齐不能降低当前文件的$name", ({ source, resolution }) => {
|
|
expect(
|
|
evaluateCandidate(consistent, target, { ...current, source, resolution }, { seasonReleaseTarget: target }),
|
|
).toMatchObject({ decision: "reject", violations: [expect.objectContaining({ code: "lower_tier" })] });
|
|
});
|
|
|
|
it("当前文件已经同套时不重复整理或下载", () => {
|
|
expect(evaluateCandidate(consistent, target, target, { seasonReleaseTarget: target }).decision).toBe("reject");
|
|
const normalizedCurrent = { ...target, releaseGroup: { name: "chd", source: "known" as const } };
|
|
expect(evaluateCandidate(consistent, target, normalizedCurrent, { seasonReleaseTarget: target }).decision).toBe(
|
|
"reject",
|
|
);
|
|
});
|
|
|
|
it("另一套同档资源不能替换已选定的当季目标", () => {
|
|
const other = release("Show.S01E02.1080p.WEB-DL.H265-FLUX");
|
|
expect(evaluateCandidate(consistent, other, current, { seasonReleaseTarget: target })).toMatchObject({
|
|
decision: "reject",
|
|
violations: [expect.objectContaining({ code: "different_season_release" })],
|
|
});
|
|
});
|
|
|
|
it("关闭升级时,同档对齐也不能发起替换", () => {
|
|
const spec = { ...consistent, upgrade: { ...consistent.upgrade!, enabled: false } };
|
|
expect(evaluateCandidate(spec, target, current, { seasonReleaseTarget: target })).toMatchObject({
|
|
decision: "reject",
|
|
violations: [expect.objectContaining({ code: "upgrade_disabled" })],
|
|
});
|
|
});
|
|
|
|
it("全集洗版不能因同档对齐而接受单集或播出日期资源", () => {
|
|
const spec: SlotSpec = { ...consistent, upgrade: { ...consistent.upgrade!, mode: "full_pack" } };
|
|
for (const candidate of [target, { ...target, episodes: undefined, airDate: "20261013" }]) {
|
|
expect(evaluateCandidate(spec, candidate, current, { seasonReleaseTarget: candidate })).toMatchObject({
|
|
decision: "reject",
|
|
violations: [expect.objectContaining({ code: "not_full_pack" })],
|
|
});
|
|
}
|
|
const pack = release("Show.S01.1080p.WEB-DL.H265-CHD");
|
|
expect(pack.episodes).toBeUndefined();
|
|
expect(evaluateCandidate(spec, pack, current, { seasonReleaseTarget: pack }).decision).toBe("upgrade");
|
|
});
|
|
|
|
it("不合格蓝光压制组不能因同档对齐绕过名单", () => {
|
|
const spec: SlotSpec = {
|
|
...consistent,
|
|
upgrade: { ...consistent.upgrade!, encodeGroupAllowlist: ["CHD"] },
|
|
};
|
|
const old = release("Show.S01E01.1080p.BluRay.x265-OTHER");
|
|
const unqualified = release("Show.S01E02.1080p.BluRay.x265-BADGRP");
|
|
expect(unqualified.isEncode).toBe(true);
|
|
expect(evaluateCandidate(spec, unqualified, old, { seasonReleaseTarget: unqualified })).toMatchObject({
|
|
decision: "reject",
|
|
violations: [expect.objectContaining({ code: "encode_not_qualified" })],
|
|
});
|
|
const qualified = release("Show.S01E02.1080p.BluRay.x265-chd");
|
|
expect(evaluateCandidate(spec, qualified, old, { seasonReleaseTarget: qualified }).decision).toBe("upgrade");
|
|
});
|
|
|
|
it.each([
|
|
{ field: "resolution", missing: "resolution" },
|
|
{ field: "source", missing: "source" },
|
|
{ field: "videoCodec", missing: "codec" },
|
|
{ field: "releaseGroup", missing: "group" },
|
|
] as const)("候选缺少 $field 时不执行成套替换", ({ field, missing }) => {
|
|
const candidate = { ...target, [field]: undefined };
|
|
expect(evaluateCandidate(consistent, candidate, current, { seasonReleaseTarget: target })).toMatchObject({
|
|
decision: "unknown_field",
|
|
missing: expect.arrayContaining([missing]),
|
|
});
|
|
});
|
|
|
|
it("季目标缺失或规格不完整时等待,不自行认定当前候选是目标", () => {
|
|
expect(evaluateCandidate(consistent, target, current)).toMatchObject({
|
|
decision: "unknown_field",
|
|
missing: ["current_asset"],
|
|
});
|
|
expect(
|
|
evaluateCandidate(consistent, target, current, { seasonReleaseTarget: { ...target, videoCodec: undefined } }),
|
|
).toMatchObject({ decision: "unknown_field", missing: ["current_asset"] });
|
|
});
|
|
});
|