import { REAL_DETAIL_HH_DV_ONLY, REAL_DETAIL_HH_INFO_CARD, REAL_RSS_DESCR_AWARDS, REAL_RSS_DESCR_CAST, REAL_RSS_DESCR_INFO_BLOCK, REAL_RSS_DESCR_PLAIN, SYNTH_BDINFO_FULL, SYNTH_BDINFO_QUICK, SYNTH_DESCR_BRACKETS, SYNTH_FILES_BDMV, SYNTH_FILES_DIRECTORY, SYNTH_MEDIAINFO_MULTI_TRACK, } from "@umt/testkit"; import { describe, expect, it } from "vitest"; import { cleanDescription, DEFAULT_DESCRIPTION_RULES, stripMarkup, structureDescription, } from "./detail-description.ts"; import { formatDetailPage } from "./detail-format.ts"; import { buildFileTree } from "./file-tree.ts"; import { parseMediaSummary } from "./mediainfo.ts"; const info = (text: string | null) => Object.fromEntries(structureDescription(text).infoBlock.map((x) => [x.key, x.value])); describe("简介结构化:真实缓存样本", () => { it("憨憨豆瓣信息块:「片名:」换行取值,卡片摘要与更新提示去掉,主演拆开", () => { const d = structureDescription(REAL_DETAIL_HH_INFO_CARD.descriptionText); expect(Object.fromEntries(d.infoBlock.map((x) => [x.key, x.value]))).toMatchObject({ 片名: "一瓯春", 年代: "2026", 片长: "45分钟", 类别: "爱情,古装", 导演: "潘安子 Anzi Pan", 编剧: "尤四姐", 主演: "许凯 Kai Xu / 周也 Ye Zhou / 白澍 Shu Bai", }); // 空值键(别名 / 标语)不出现 expect(d.infoBlock.map((x) => x.key)).not.toContain("别名"); expect(d.infoBlock.map((x) => x.key)).not.toContain("标语"); expect(d.paragraphs).toHaveLength(2); expect(d.paragraphs[0]).toMatch(/^“杀伐千面腹黑男”/); expect(d.paragraphs.join("\n")).not.toMatch(/该信息最后更新于|更新信息|^-{5,}|\(11053\)/m); }); it("◎ 信息块 + ◎简介:链接键去掉,简介进段落", () => { const d = structureDescription(REAL_RSS_DESCR_INFO_BLOCK); expect(d.infoBlock.map((x) => x.key)).toEqual([ "译名", "片名", "年代", "产地", "类别", "语言", "上映日期", "集数", "片长", "主演", ]); expect(info(REAL_RSS_DESCR_INFO_BLOCK).译名).toBe("Stay Low Profile, Sect Chief / 掌门低调点 3D动画版"); expect(d.paragraphs).toHaveLength(1); expect(d.paragraphs[0]).toMatch(/^大雪覆门/); expect(JSON.stringify(d)).not.toMatch(/<| |douban\.com/); }); it("主演多行续行合并;◎获奖情况进信息块;站点截断的半个标签去掉", () => { const i = info(REAL_RSS_DESCR_AWARDS); expect(i.主演).toBe( "张维为 Weiwei Zhang / 亚历山大·杜金 Alexander Dugin / 李世默 Eric Li / 范勇鹏 Yongpeng Fan / 余亮 Liang Yu", ); expect(i.获奖情况).toBe("第28届上海电视节白玉兰奖 (2023) / 综艺节目 最佳电视综艺节目(提名)…"); const d = structureDescription(REAL_RSS_DESCR_AWARDS); expect(d.paragraphs).toHaveLength(2); expect(d.paragraphs[1]).toMatch(/^《这就是中国》/); }); it("豆瓣评分 / 导演 / 编剧,长主演列表", () => { const i = info(REAL_RSS_DESCR_CAST); expect(i).toMatchObject({ 豆瓣评分: "5.4/10 from 11075 users", 导演: "潘安子 Anzi Pan", 编剧: "尤四姐" }); expect(i.主演!.split(" / ")).toHaveLength(16); expect(structureDescription(REAL_RSS_DESCR_CAST).paragraphs).toEqual([]); }); it("没有信息块:整段是正文,海报图去掉", () => { const d = structureDescription(REAL_RSS_DESCR_PLAIN); expect(d.infoBlock).toEqual([]); expect(d.paragraphs).toHaveLength(1); expect(d.paragraphs[0]).toMatch(/^21岁的玉雕师方雾/); }); it("只有 MediaInfo 的详情页:信息块和段落都为空", () => { expect(structureDescription(REAL_DETAIL_HH_DV_ONLY.descriptionText)).toEqual({ infoBlock: [], paragraphs: [] }); }); }); describe("简介清洗:【】『』、BBCode / HTML 残留、广告签名", () => { const d = structureDescription(SYNTH_DESCR_BRACKETS); it("【】『』键认得,正文里的「【第一版】」不当键", () => { expect(Object.fromEntries(d.infoBlock.map((x) => [x.key, x.value]))).toMatchObject({ 译名: "示例电影 / Example Movie", 字幕: "中英双语", IMDb评分: "8.1/10 from 12,345 users", 导演: "某导演 Some Director", 主演: "演员甲 Actor A / 演员乙 Actor B", }); expect(d.infoBlock.map((x) => x.key)).not.toContain("IMDb链接"); expect(structureDescription("◎简  介\n\n  【第一版】仙界至暗,众生皆材。").paragraphs).toEqual([ "【第一版】仙界至暗,众生皆材。", ]); }); it("段落:标记、实体、多余空行、转载声明 / 群号 / 感谢签名 / 分隔线都去掉", () => { expect(d.paragraphs).toEqual([ "一位科学家在沙漠深处发现了改变世界的秘密。<他必须>在时间耗尽前做出选择。", "影片获得多项提名,查看获奖名单。", ]); const cleaned = cleanDescription(SYNTH_DESCR_BRACKETS); expect(cleaned).not.toMatch(/\[\/?\w+|<\/?\w+| |QQ群|转载|感谢|\n{3,}|-{5,}/); }); it("规则可配置:追加整行规则", () => { const rules = { ...DEFAULT_DESCRIPTION_RULES, dropLines: [...DEFAULT_DESCRIPTION_RULES.dropLines, /^影片获得/] }; expect(structureDescription(SYNTH_DESCR_BRACKETS, rules).paragraphs).toHaveLength(1); }); it("英文硬换行接回一段", () => { expect( structureDescription("A scientist discovers a secret\nthat changes the world.\n\nSecond one.").paragraphs, ).toEqual(["A scientist discovers a secret that changes the world.", "Second one."]); }); it("stripMarkup:实体与数字引用", () => { expect(stripMarkup("a&bAB c[b]d[/b]")).toBe("a&bAB cd"); }); }); describe("MediaInfo / BDInfo 概要", () => { it("真实样本:中文摘要头 + General / Video / Audio", () => { expect(parseMediaSummary(REAL_DETAIL_HH_INFO_CARD.mediaInfo)).toEqual({ kind: "mediainfo", general: { fileName: "Spring.of.the.Blade.S01E18.2026.2160p.YK.WEB-DL.H.265.HFR.HQ.AAC2.0-HHWEB.mkv", container: "Matroska", duration: "43 min 55 s", overallBitrate: "15.6 Mb/s", size: "4.79 GiB", }, video: [ { codec: "HEVC", resolution: "3840×1608", frameRate: "60 fps", bitrate: "15.3 Mb/s", bitDepth: "10 bit", hdr: [], }, ], audio: [ { language: "中文", codec: "AAC", channels: "2.0", bitrate: "256 kb/s", title: "中文 [AAC 2.0]", isDefault: true, }, ], subtitles: [], }); }); it("真实样本:杜比视界 Profile 5", () => { const m = parseMediaSummary(REAL_DETAIL_HH_DV_ONLY.mediaInfo)!; expect(m.video[0]).toMatchObject({ hdr: ["Dolby Vision P5"], frameRate: "25 fps" }); }); it("多音轨多字幕:编码短名、声道、默认 / 强制、语言译名", () => { const m = parseMediaSummary(SYNTH_MEDIAINFO_MULTI_TRACK)!; expect(m.video[0]).toMatchObject({ resolution: "3840×2160", frameRate: "23.976 fps", hdr: ["Dolby Vision P7.6", "HDR10"], }); expect(m.audio.map((a) => [a.language, a.codec, a.channels, a.bitrate, a.isDefault])).toEqual([ ["英语", "TrueHD Atmos", "7.1", "4561 kb/s", true], ["中文", "AC-3", "5.1", "640 kb/s", false], ["英语", "AAC", "2.0", "192 kb/s", false], ]); expect(m.subtitles.map((s) => [s.language, s.format, s.isDefault, s.forced])).toEqual([ ["英语", "PGS", false, true], ["中文", "UTF-8", true, false], ["繁体中文", "ASS", false, false], ["英语", "PGS", false, false], ]); }); it("BDInfo 简版", () => { const m = parseMediaSummary(SYNTH_BDINFO_QUICK)!; expect(m.kind).toBe("bdinfo"); expect(m.general).toMatchObject({ container: "Blu-ray", duration: "2 h 22 min", size: "57.57 GiB", overallBitrate: "55.20 Mbps", }); expect(m.video.map((v) => [v.resolution, v.hdr])).toEqual([ ["2160p", ["HDR10"]], ["1080p", ["Dolby Vision FEL"]], ]); expect(m.audio[0]).toMatchObject({ language: "英语", codec: "TrueHD Atmos", channels: "7.1", bitrate: "4561 kbps", }); expect(m.subtitles.map((s) => s.language)).toEqual(["英语", "中文", "中文"]); }); it("BDInfo 完整报告表格", () => { const m = parseMediaSummary(SYNTH_BDINFO_FULL)!; expect(m.general.duration).toBe("52 min 10 s"); expect(m.video).toEqual([ { codec: "MPEG-4 AVC", resolution: "1080p", frameRate: "23.976 fps", bitrate: "31456 kbps", bitDepth: null, hdr: [], }, ]); expect(m.audio.map((a) => [a.language, a.codec, a.channels])).toEqual([ ["日语", "DTS-HD MA", "5.1"], ["日语", "LPCM", "2.0"], ]); expect(m.subtitles).toHaveLength(2); }); it("认不出返回 null", () => { expect(parseMediaSummary("just some text")).toBeNull(); expect(parseMediaSummary(null)).toBeNull(); }); }); describe("文件清单 → 目录树", () => { it("真实样本:8 集平铺,全是主视频", () => { const t = buildFileTree(REAL_DETAIL_HH_INFO_CARD.files); expect(t).toHaveLength(REAL_DETAIL_HH_INFO_CARD.files.length); expect(t.every((r) => !r.dir && r.depth === 0 && r.role === "main")).toBe(true); }); it("目录型:目录在前、大小求和,主视频 / sample / 字幕 / 附件", () => { const t = buildFileTree(SYNTH_FILES_DIRECTORY); expect(t[0]).toMatchObject({ dir: true, depth: 0, fileCount: 6, sizeBytes: 15_085_574_144 }); const role = (name: string) => t.find((r) => r.name === name)?.role; expect(role("Example.Movie.2023.1080p.BluRay.x264-GROUP.mkv")).toBe("main"); expect(role("example.movie.sample.mkv")).toBe("sample"); expect(role("Example.Movie.2023.chs.srt")).toBe("subtitle"); expect(role("poster.jpg")).toBe("extra"); expect(t.map((r) => `${" ".repeat(r.depth)}${r.name}`).slice(0, 4)).toEqual([ "Example.Movie.2023.1080p.BluRay.x264-GROUP", " Sample", " example.movie.sample.mkv", " Subs", ]); }); it("原盘:最大的 m2ts 是正片,小片段不是", () => { const t = buildFileTree(SYNTH_FILES_BDMV); expect(t.find((r) => r.name === "00001.m2ts")?.role).toBe("main"); expect(t.find((r) => r.name === "00002.m2ts")?.role).toBe("video"); }); }); describe("formatDetailPage", () => { it("组合四部分", () => { const f = formatDetailPage(REAL_DETAIL_HH_INFO_CARD); expect(f.infoBlock.length).toBeGreaterThan(5); expect(f.paragraphs).toHaveLength(2); expect(f.media?.kind).toBe("mediainfo"); expect(f.fileTree).toHaveLength(8); }); });