Add audioCodec, subtitle, dub, feature, year, category, size and seeders facets with disjunctive counts; parser records dub languages (audioLangs). Co-Authored-By: Claude Opus 5.5 <[email protected]>
306 lines
12 KiB
TypeScript
306 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import {
|
||
applyFacets,
|
||
audioBuckets,
|
||
type FacetInput,
|
||
formatFacetParam,
|
||
parseFacetParam,
|
||
parseRelease,
|
||
promotionBuckets,
|
||
releaseTypeBucket,
|
||
seedersBucket,
|
||
sizeBucket,
|
||
subtitleBuckets,
|
||
} from "../src/index.ts";
|
||
|
||
// SEARCH-3 快速筛选:档位、计数(按其它维度已选条件)、同维度 OR / 跨维度 AND、qf 编解码
|
||
|
||
const NOW = Date.parse("2026-09-29T00:00:00Z");
|
||
const none = { type: "none", downloadFactor: 1, uploadFactor: 1, expiresAt: null };
|
||
const mk = (id: string, o: Partial<FacetInput> = {}): FacetInput & { id: string } => ({
|
||
id,
|
||
siteId: "a",
|
||
siteName: "站 A",
|
||
resolution: "1080p",
|
||
source: "WEB-DL",
|
||
videoCodec: "H.264",
|
||
hdr: [],
|
||
releaseGroup: null,
|
||
season: null,
|
||
episodes: null,
|
||
promotion: none,
|
||
...o,
|
||
});
|
||
|
||
const items = [
|
||
mk("1", {
|
||
resolution: "2160p",
|
||
source: "REMUX",
|
||
videoCodec: "H.265",
|
||
hdr: ["DV", "HDR10"],
|
||
releaseGroup: "FraMeSToR",
|
||
}),
|
||
mk("2", {
|
||
resolution: "2160p",
|
||
source: "WEB-DL",
|
||
videoCodec: "H.265",
|
||
hdr: ["HDR10"],
|
||
releaseGroup: "FLUX",
|
||
siteId: "b",
|
||
siteName: "站 B",
|
||
}),
|
||
mk("3", {
|
||
resolution: "1080p",
|
||
source: "BluRay",
|
||
videoCodec: "H.264",
|
||
releaseGroup: "flux",
|
||
promotion: { type: "free", downloadFactor: 0, uploadFactor: 1, expiresAt: null },
|
||
}),
|
||
mk("4", {
|
||
resolution: "720p",
|
||
source: "HDTV",
|
||
videoCodec: null,
|
||
promotion: { type: "2xfree", downloadFactor: 0, uploadFactor: 2, expiresAt: null },
|
||
}),
|
||
mk("5", { resolution: null, source: "BluRay-Disc", videoCodec: "AV1", siteId: "b", siteName: "站 B" }),
|
||
];
|
||
const run = (sel: Parameters<typeof applyFacets>[2]) => applyFacets(items, (x) => x, sel, { now: NOW });
|
||
const ids = (r: { items: { id: string }[] }) => r.items.map((i) => i.id);
|
||
const counts = (opts: { value: string; count: number }[]) => Object.fromEntries(opts.map((o) => [o.value, o.count]));
|
||
|
||
describe("release-facets", () => {
|
||
it("未筛选:各维度计数等于全部结果里的档位分布", () => {
|
||
const r = run({});
|
||
expect(ids(r)).toEqual(["1", "2", "3", "4", "5"]);
|
||
expect(counts(r.facets.resolution)).toEqual({ "2160p": 2, "1080p": 1, "720p": 1, other: 1 });
|
||
expect(counts(r.facets.videoCodec)).toEqual({ "H.265": 2, "H.264": 1, AV1: 1, other: 1 });
|
||
expect(counts(r.facets.releaseType)).toEqual({ disc: 1, remux: 1, encode: 1, webdl: 1, hdtv: 1 });
|
||
expect(r.facets.releaseType.map((o) => o.label)).toEqual(["原盘", "Remux", "蓝光压制", "WEB-DL", "HDTV"]);
|
||
expect(counts(r.facets.hdr)).toEqual({ DV: 1, HDR10: 2, "HDR10+": 0, SDR: 3 });
|
||
expect(counts(r.facets.promotion)).toEqual({ free: 2, "2x": 1, "50%": 0, none: 3 });
|
||
expect(r.facets.site).toEqual([
|
||
{ value: "a", label: "站 A", count: 3 },
|
||
{ value: "b", label: "站 B", count: 2 },
|
||
]);
|
||
// 制作组不分大小写合并
|
||
expect(r.facets.releaseGroup).toEqual([
|
||
{ value: "FLUX", label: "FLUX", count: 2 },
|
||
{ value: "FraMeSToR", label: "FraMeSToR", count: 1 },
|
||
]);
|
||
expect(r.facets.season).toEqual([]);
|
||
});
|
||
|
||
it("同维度 OR、不同维度 AND;计数按其它维度的已选条件", () => {
|
||
const r = run({ resolution: ["2160p", "1080p"], videoCodec: ["H.265"] });
|
||
expect(ids(r)).toEqual(["1", "2"]);
|
||
// 分辨率的计数只受编码条件影响:1080p 在 H.265 下没有结果 → 0
|
||
expect(counts(r.facets.resolution)).toEqual({ "2160p": 2, "1080p": 0, "720p": 0, other: 0 });
|
||
// 编码计数只受分辨率条件影响
|
||
expect(counts(r.facets.videoCodec)).toEqual({ "H.265": 2, "H.264": 1, AV1: 0, other: 0 });
|
||
// 其它维度按全部条件
|
||
expect(counts(r.facets.releaseType)).toMatchObject({ remux: 1, webdl: 1, encode: 0 });
|
||
expect(ids(run({ site: ["b"], releaseType: ["disc", "webdl"] }))).toEqual(["2", "5"]);
|
||
expect(ids(run({ releaseGroup: ["flux"] }))).toEqual(["2", "3"]);
|
||
expect(ids(run({ hdr: ["SDR"] }))).toEqual(["3", "4", "5"]);
|
||
expect(ids(run({ promotion: ["2x"] }))).toEqual(["4"]);
|
||
});
|
||
|
||
it("H&R 三态分面:缺省按未知;未知档只在有资源时列出", () => {
|
||
const hrItems = [mk("y", { hr: true }), mk("n", { hr: false }), mk("n2", { hr: false }), mk("u")];
|
||
const r = applyFacets(hrItems, (x) => x, {}, { now: NOW });
|
||
expect(r.facets.hr).toEqual([
|
||
{ value: "yes", label: "有", count: 1 },
|
||
{ value: "no", label: "无", count: 2 },
|
||
{ value: "unknown", label: "未知", count: 1 },
|
||
]);
|
||
expect(ids(applyFacets(hrItems, (x) => x, { hr: ["no", "unknown"] }, { now: NOW }))).toEqual(["n", "n2", "u"]);
|
||
const known = applyFacets([mk("y", { hr: true })], (x) => x, {}, { now: NOW });
|
||
expect(known.facets.hr.map((o) => o.value)).toEqual(["yes", "no"]);
|
||
});
|
||
|
||
it("已选但没有结果的值仍列出(计数 0)", () => {
|
||
const r = run({ releaseGroup: ["NoSuch"], site: ["zzz"] });
|
||
expect(r.items).toEqual([]);
|
||
expect(r.facets.releaseGroup.find((o) => o.value === "NoSuch")).toEqual({
|
||
value: "NoSuch",
|
||
label: "NoSuch",
|
||
count: 0,
|
||
});
|
||
expect(r.facets.site.find((o) => o.value === "zzz")?.count).toBe(0);
|
||
});
|
||
|
||
it("制作组只列前 20(已选的补在后面)", () => {
|
||
const many = Array.from({ length: 25 }, (_, i) => mk(String(i), { releaseGroup: `G${i}` }));
|
||
const r = applyFacets(many, (x) => x, { releaseGroup: ["g9"] }, { now: NOW });
|
||
expect(r.facets.releaseGroup.length).toBe(21);
|
||
expect(r.facets.releaseGroup.at(-1)?.value).toBe("G9");
|
||
});
|
||
|
||
it("季 / 集:多季合集展开,整季资源记为整季", () => {
|
||
const tv = [
|
||
mk("s1", { season: 1, episodes: null }),
|
||
mk("s1e2", { season: 1, episodes: { start: 2, end: 3 } }),
|
||
mk("s13", { season: 1, seasonEnd: 3, episodes: null }),
|
||
mk("movie"),
|
||
];
|
||
const r = applyFacets(tv, (x) => x, { season: ["2"] }, { now: NOW });
|
||
expect(ids(r)).toEqual(["s13"]);
|
||
expect(r.facets.season).toEqual([
|
||
{ value: "1", label: "S01", count: 3 },
|
||
{ value: "2", label: "S02", count: 1 },
|
||
{ value: "3", label: "S03", count: 1 },
|
||
]);
|
||
expect(r.facets.episode).toEqual([
|
||
{ value: "pack", label: "整季", count: 1 },
|
||
{ value: "2", label: "E02", count: 0 },
|
||
{ value: "3", label: "E03", count: 0 },
|
||
]);
|
||
expect(ids(applyFacets(tv, (x) => x, { season: ["1"], episode: ["3"] }, { now: NOW }))).toEqual(["s1e2"]);
|
||
});
|
||
|
||
it("促销与版本类型档位", () => {
|
||
expect(promotionBuckets({ type: "2x50%", downloadFactor: 0.5, uploadFactor: 2, expiresAt: null }, NOW)).toEqual([
|
||
"2x",
|
||
"50%",
|
||
]);
|
||
expect(promotionBuckets({ type: "30%", downloadFactor: 0.3, uploadFactor: 1, expiresAt: null }, NOW)).toEqual([
|
||
"other",
|
||
]);
|
||
expect(
|
||
promotionBuckets({ type: "unknown", downloadFactor: null, uploadFactor: null, expiresAt: null }, NOW),
|
||
).toEqual(["none"]);
|
||
// 促销已过期 → 无
|
||
expect(
|
||
promotionBuckets({ type: "free", downloadFactor: 0, uploadFactor: 1, expiresAt: "2026-09-28T00:00:00Z" }, NOW),
|
||
).toEqual(["none"]);
|
||
expect(["BluRay-Disc", "REMUX", "BluRay", "WEB-DL", "HDTV", "WEBRip", null].map(releaseTypeBucket)).toEqual([
|
||
"disc",
|
||
"remux",
|
||
"encode",
|
||
"webdl",
|
||
"hdtv",
|
||
"other",
|
||
"other",
|
||
]);
|
||
});
|
||
|
||
it("qf 参数编解码", () => {
|
||
const sel = { resolution: ["2160p", "1080p"], promotion: ["50%"], releaseGroup: ["A;B,C:D"] };
|
||
const s = formatFacetParam(sel);
|
||
expect(s).toBe("resolution:2160p,1080p;promotion:50%25;releaseGroup:A%3BB%2CC%3AD");
|
||
expect(parseFacetParam(s)).toEqual(sel);
|
||
expect(parseFacetParam("bogus:1;hdr:;hdr:DV,DV")).toEqual({ hdr: ["DV"] });
|
||
});
|
||
|
||
// ── SEARCH-4:对齐 MP 后新增的维度 ──
|
||
|
||
const GB = 1024 ** 3;
|
||
const rich = [
|
||
mk("a", {
|
||
audio: ["TrueHD", "Atmos 7.1"],
|
||
subtitles: ["zh-Hans", "en"],
|
||
audioLangs: ["cmn"],
|
||
bitDepth: 10,
|
||
year: 2024,
|
||
category: "电影",
|
||
sizeBytes: 60 * GB,
|
||
seeders: 80,
|
||
}),
|
||
mk("b", {
|
||
audio: ["DDP 5.1"],
|
||
subtitles: ["zh-Hant"],
|
||
audioLangs: ["cmn", "yue"],
|
||
fps: 60,
|
||
year: 2023,
|
||
category: "电影",
|
||
sizeBytes: 8 * GB,
|
||
seeders: 0,
|
||
}),
|
||
mk("c", { audio: ["DTS-HD MA 5.1", "2Audios"], year: 2024, category: "剧集", sizeBytes: 3 * GB, seeders: 12 }),
|
||
mk("d", { audio: [], cut: "Extended", sizeBytes: null, seeders: null }),
|
||
];
|
||
const run2 = (sel: Parameters<typeof applyFacets>[2]) => applyFacets(rich, (x) => x, sel, { now: NOW });
|
||
|
||
it("新维度档位与计数:音频 / 字幕 / 配音 / 特性 / 年份 / 分类 / 大小 / 做种", () => {
|
||
const r = run2({});
|
||
expect(counts(r.facets.audioCodec)).toEqual({
|
||
Atmos: 1,
|
||
TrueHD: 1,
|
||
"DTS-HD": 1,
|
||
DTS: 0,
|
||
DDP: 1,
|
||
DD: 0,
|
||
AAC: 0,
|
||
unknown: 1,
|
||
});
|
||
expect(r.facets.audioCodec.find((o) => o.value === "DDP")?.label).toBe("DD+");
|
||
expect(counts(r.facets.subtitle)).toEqual({ zh: 2, "zh-Hans": 1, "zh-Hant": 1, en: 1, unknown: 2 });
|
||
expect(counts(r.facets.dub)).toEqual({ cmn: 2, yue: 1, unknown: 2 });
|
||
expect(counts(r.facets.feature)).toEqual({ "10bit": 1, hfr: 1, cut: 1 });
|
||
// 年份新的在前;分类按条数
|
||
expect(r.facets.year.map((o) => [o.value, o.count])).toEqual([
|
||
["2024", 2],
|
||
["2023", 1],
|
||
]);
|
||
expect(r.facets.category.map((o) => [o.label, o.count])).toEqual([
|
||
["电影", 2],
|
||
["剧集", 1],
|
||
]);
|
||
expect(counts(r.facets.size)).toEqual({
|
||
lt1: 0,
|
||
"1-5": 1,
|
||
"5-10": 1,
|
||
"10-20": 0,
|
||
"20-50": 0,
|
||
"50+": 1,
|
||
unknown: 1,
|
||
});
|
||
expect(counts(r.facets.seeders)).toEqual({ "0": 1, "1-9": 0, "10-49": 1, "50+": 1, unknown: 1 });
|
||
});
|
||
|
||
it("新维度组合:同维度 OR、跨维度 AND,计数按其它维度已选条件", () => {
|
||
// 中文字幕 AND 国语 → a、b;再加 Atmos → 只剩 a
|
||
expect(ids(run2({ subtitle: ["zh"], dub: ["cmn"] }))).toEqual(["a", "b"]);
|
||
const r = run2({ subtitle: ["zh"], dub: ["cmn"], audioCodec: ["Atmos"] });
|
||
expect(ids(r)).toEqual(["a"]);
|
||
// 音频维度自己的计数不受已选 Atmos 影响:DD+ 仍可多出 b
|
||
expect(counts(r.facets.audioCodec).DDP).toBe(1);
|
||
// 同维度 OR:Atmos 或 DTS-HD
|
||
expect(ids(run2({ audioCodec: ["Atmos", "DTS-HD"] }))).toEqual(["a", "c"]);
|
||
// 年份 × 分类 × 做种
|
||
expect(ids(run2({ year: ["2024"], category: ["电影", "剧集"], seeders: ["10-49", "50+"] }))).toEqual(["a", "c"]);
|
||
expect(ids(run2({ size: ["unknown"], feature: ["cut"] }))).toEqual(["d"]);
|
||
// 已选但当前没有资源的年份列出、计数 0
|
||
expect(run2({ year: ["1999"] }).facets.year.find((o) => o.value === "1999")).toEqual({
|
||
value: "1999",
|
||
label: "1999",
|
||
count: 0,
|
||
});
|
||
});
|
||
|
||
it("新维度的归档函数", () => {
|
||
expect(audioBuckets(["E-AC3", "AC3 2.0", "PCM", "Opus", "5.1"])).toEqual(["DDP", "DD", "LPCM", "other"]);
|
||
expect(audioBuckets(["2Audios"])).toEqual(["unknown"]);
|
||
expect(subtitleBuckets(["zh"])).toEqual(["zh"]);
|
||
expect(subtitleBuckets(null)).toEqual(["unknown"]);
|
||
expect([0.5 * GB, GB, 49.9 * GB, 50 * GB, null, -1].map(sizeBucket)).toEqual([
|
||
"lt1",
|
||
"1-5",
|
||
"20-50",
|
||
"50+",
|
||
"unknown",
|
||
"unknown",
|
||
]);
|
||
expect([0, 9, 10, 50, null].map(seedersBucket)).toEqual(["0", "1-9", "10-49", "50+", "unknown"]);
|
||
});
|
||
|
||
it("解析器补齐配音语言(MP 的 CNVOI / HKVOI)", () => {
|
||
expect(parseRelease("Movie 2020 1080p 国语中字 WEB-DL").audioLangs).toEqual(["cmn"]);
|
||
expect(parseRelease("某剧 第01集 国粤双语 1080p").audioLangs).toEqual(["cmn", "yue"]);
|
||
expect(parseRelease("Movie.2020.1080p.BluRay.x264-GRP").audioLangs).toBeUndefined();
|
||
expect(
|
||
parseRelease("Movie.2020.2160p.BluRay.x265-GRP", { subtitle: "电影 | 国英双语 简繁字幕" }).audioLangs,
|
||
).toEqual(["cmn", "en"]);
|
||
});
|
||
});
|