Files
UHD_Media_Tool/packages/adapters/src/sites/spider/detail-page.test.ts
T

195 lines
8.7 KiB
TypeScript

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 = `<html><body><a href="logout.php">退出</a><h1 id="top">Some.Movie.2024.1080p.BluRay.x264-GRP</h1>
<table>
<tr><td class="rowhead">副标题</td><td class="rowfollow">某电影</td></tr>
<tr><td class="rowhead">基本信息</td><td class="rowfollow"><b>大小:</b>8.00 GB &nbsp; <b>类型:</b> 电影</td></tr>
<tr><td class="rowhead">标签</td><td class="rowfollow"><span>中字</span><span>DIY</span></td></tr>
<tr><td class="rowhead">上传者</td><td class="rowfollow"><i>匿名</i></td></tr>
<tr><td class="rowhead">简介</td><td class="rowfollow"><div id="kdescr">一段简介<br>第二行
<div class="codemain">DISC INFO:<br>Disc Title: X<br>Size: 1<br>Protection: AACS<br>Playlist: 00800.MPLS<br>Length: 2:00:00<br>Video: MPEG-4 AVC<br>Total Bitrate: 30 Mbps</div>
<img src="https://img.example-host.com/a.png"></div></td></tr>
</table>
<table id="filelist"><tr><td>Some.Movie.mkv</td><td>8.00 GB</td></tr></table></body></html>`;
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) },
]);
});
});