import { HHANCLUB_DETAIL_PAGE_2026, HHANCLUB_DETAIL_PAGE_FULL, HHANCLUB_DETAIL_PAGE_REAL_2026, HHANCLUB_FILELIST_FRAGMENT, } from "@umt/testkit"; import { describe, expect, it } from "vitest"; import { hhanclubDefinition } from "../definitions/index.ts"; import { looksLikeMediaInfo, pageFromBbcode } from "../detail-text.ts"; import { createSiteRateLimiter } from "../rate-limit.ts"; import { getSiteDefinition } from "../registry.ts"; import { staticSecretResolver } from "../types.ts"; import { createSpiderSiteAdapter } from "./adapter.ts"; import { parseDetailPage } from "./detail.ts"; import type { IndexerDefinition } from "./schema.ts"; const OBSERVED = "2026-09-29T00:00:00.000Z"; describe("详情页扩展内容(HHClub div 模板)", () => { const { kind, detail } = parseDetailPage(HHANCLUB_DETAIL_PAGE_FULL, hhanclubDefinition, "91002", OBSERVED); const page = detail!.page!; it("外部 ID、上传者、标签", () => { expect(kind).toBe("ok"); expect(page).toMatchObject({ imdbId: "tt14452776", doubanId: "36000001", tmdb: { type: "tv", id: "136315" }, uploader: "uploader01", tags: ["官方", "中字", "完结"], }); expect(detail!.imdbId).toBe("tt14452776"); expect(detail!.tags).toEqual(["官方", "中字", "完结"]); }); it("MediaInfo 从简介的代码块取出,简介里不再重复", () => { expect(page.mediaInfo).toMatch(/^General\nUnique ID : 1234\n/); expect(page.mediaInfo).toContain("Format : HEVC"); expect(page.descriptionText).toContain("◎译  名 示例剧集"); expect(page.descriptionText).not.toContain("Unique ID"); }); it("截图:相对地址补全、优先 data-src、过滤表情与占位图", () => { expect(page.screenshots).toEqual([ "https://hhanclub.net/attachments/202609/poster.jpg", "https://img.example-host.com/i/shot1.png", "https://img.example-host.com/i/shot2.jpg", ]); }); it("文件清单:「名称 (大小)」", () => { expect(page.files).toEqual([ { name: "Example.Show.S01E01.mkv", sizeBytes: Math.round(1.04 * 1024 ** 3) }, { name: "Example.Show.S01E02.mkv", sizeBytes: Math.round(1.02 * 1024 ** 3) }, ]); }); it("诊断:记录页面上的字段标签", () => { expect(page.probe.labels).toEqual(expect.arrayContaining(["标题", "副标题", "标签", "上传者", "文件列表"])); expect(page.probe.hasDescription).toBe(true); }); }); describe("详情页扩展内容(标准 NexusPHP rowhead 表格)", () => { const def = getSiteDefinition("audiences") as IndexerDefinition; const html = `退出

Some.Movie.2024.1080p.BluRay.x264-GRP

副标题某电影
基本信息大小:8.00 GB   类型: 电影
标签中字DIY
上传者匿名
简介
一段简介
第二行
DISC INFO:
Disc Title: X
Size: 1
Protection: AACS
Playlist: 00800.MPLS
Length: 2:00:00
Video: MPEG-4 AVC
Total Bitrate: 30 Mbps
Some.Movie.mkv8.00 GB
`; it("rowhead 标签、BDInfo、#filelist 表格", () => { const { detail } = parseDetailPage(html, def, "1", OBSERVED); const page = detail!.page!; expect(page.tags).toEqual(["中字", "DIY"]); expect(page.uploader).toBe("匿名"); expect(page.mediaInfo).toContain("Disc Title: X"); expect(page.descriptionText).toBe("一段简介\n第二行"); expect(page.screenshots).toEqual(["https://img.example-host.com/a.png"]); expect(page.files).toEqual([{ name: "Some.Movie.mkv", sizeBytes: 8 * 1024 ** 3 }]); expect(page.imdbId).toBeNull(); }); }); describe("M-Team BBCode 简介", () => { it("截图取 [img],MediaInfo 取引用块,外部 ID 从正文取", () => { const descr = `[img]https://img.example-host.com/p.jpg[/img] ◎简介 这是简介 https://www.imdb.com/title/tt15239678/ [quote]General Unique ID : 1 Complete name : a.mkv Format : Matroska File size : 1 GiB Video Format : AVC[/quote] [img=https://img.example-host.com/s.png]`; const page = pageFromBbcode(descr, "https://kp.m-team.cc/", { tags: ["中字"] }); expect(page.screenshots).toEqual(["https://img.example-host.com/p.jpg", "https://img.example-host.com/s.png"]); expect(page.mediaInfo).toMatch(/^General\n/); expect(page.descriptionText).toBe("◎简介 这是简介 https://www.imdb.com/title/tt15239678/"); expect(page.imdbId).toBe("tt15239678"); expect(page.tags).toEqual(["中字"]); }); it("MediaInfo 识别不误判普通简介", () => { expect(looksLikeMediaInfo("General\n一段文字\nVideo")).toBe(false); }); }); describe("憨憨真实详情页(2026-10-02 摘录)", () => { const { detail } = parseDetailPage(HHANCLUB_DETAIL_PAGE_REAL_2026, hhanclubDefinition, "219860", OBSERVED); const page = detail!.page!; it("MediaInfo 只取 #mediainfo-raw 原文,不带摘要表", () => { expect(page.mediaInfo!.startsWith("General\nComplete name")).toBe(true); expect(page.mediaInfo).not.toContain("体 积"); }); it("豆瓣块一项一行;去掉分隔线、空值项、标题行与更新提示;链接之间已有逗号不再补分隔", () => { const t = page.descriptionText!; expect(t.split("\n").slice(0, 3)).toEqual([ "片名: 斗罗大陆Ⅱ绝世唐门 年番1", "别名: 斗罗大陆2,斗罗大陆2:绝世唐门\u200e,斗罗大陆2绝世唐门,绝世唐门动画版", "年代: 2023", ]); expect(t).toContain("\n导演: 沈乐平 Leping Shen\n"); expect(t).toContain("编剧: 沈乐平 Leping Shen,唐家三少 Wei Zhang"); expect(t).toContain("主演:\n梁达伟 Dawei Liang / 蒋丽 Li Jiang"); expect(t).not.toMatch(/-{5,}|标语|该信息最后更新|\(23691\)/); expect(page.doubanId).toBe("35567787"); }); }); describe("详情页扩展内容(憨憨 2026 真实结构:分节 div,无 #kdescr)", () => { const { kind, detail } = parseDetailPage(HHANCLUB_DETAIL_PAGE_2026, hhanclubDefinition, "91003", OBSERVED); const page = detail!.page!; it("MediaInfo 取「mediainfo」一节;IMDb / TMDB 从 MediaInfo 行兜底,豆瓣取页面链接", () => { expect(kind).toBe("ok"); expect(page.mediaInfo).toContain("Complete name"); expect(page).toMatchObject({ imdbId: "tt45950001", tmdb: { type: "tv", id: "290001" }, doubanId: "36000002" }); expect(detail!.imdbId).toBe("tt45950001"); }); it("简介取豆瓣信息块,小标签补冒号、一项一行", () => { expect(page.descriptionText).toContain("片名:示例剧集"); expect(page.descriptionText).toContain("类别:爱情 / 古装"); expect(page.descriptionText).toContain("一段剧情简介"); }); it("截图取「截图」一节(优先 data-src),不含海报与占位图", () => { expect(page.screenshots).toEqual([ "https://pic.hhanclub.net/img/2026/09/29/shot1.png", "https://img3.pixhost.cc/images/1/shot2.jpg", ]); }); it("上传者、标签、发布时间(字段文本兜底)", () => { expect(page.uploader).toBe("匿名"); expect(page.tags).toEqual(["官方", "国语", "中字", "杜比视界"]); expect(detail!.uploadedAt).toBe("2026-09-29T08:42:57.000Z"); expect(page.files).toEqual([]); }); it("文件清单不在页面上 → 按需请求 viewfilelist.php 一次", async () => { const paths: string[] = []; const fetch = (async (input: RequestInfo | URL) => { const u = new URL(String(input)); paths.push(`${u.pathname}${u.search}`); const body = u.pathname === "/viewfilelist.php" ? HHANCLUB_FILELIST_FRAGMENT : HHANCLUB_DETAIL_PAGE_2026; return new Response(body, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } }); }) as typeof globalThis.fetch; const limiter = createSiteRateLimiter({ minIntervalMs: 0 }); const a = createSpiderSiteAdapter(hhanclubDefinition, { fetch, limiter, secrets: staticSecretResolver({ hhanclub: { cookie: "c=1" } }), }); const d = await a.detail("91003"); expect(paths).toEqual(["/details.php?id=91003&hit=1", "/viewfilelist.php?id=91003"]); expect(limiter.stats.granted).toBe(2); expect(d.page!.files).toEqual([ { name: "Example.Show.S01E18.2026.2160p.mkv", sizeBytes: Math.round(5.04 * 1024 ** 3) }, { name: "Example.Show.S01E19.2026.2160p.mkv", sizeBytes: Math.round(4.98 * 1024 ** 3) }, ]); }); });