fix(sites): normalize HHClub RSS items, complete detail page parsing (HH-2)

- RSS: split NexusPHP "[category]name[subtitle][size][uploader]" titles in the
  site adapter layer (category id/name/media type, clean title, subtitle,
  size); snapshot fields win on merge; lazily renormalize old RSS rows and
  re-emit release.observed; unknown list fields show an em dash
- Detail: HHClub section layout (douban info block as description,
  screenshots section, IMDb/TMDB from MediaInfo lines, upload time field,
  on-demand viewfilelist.php once); site CDN screenshots get a Referer
- Site request count now includes site-bound RSS fetches
- Origin view shows "自动化" when idle and "Agent" only after recent
  automation actions (lastAutomationAt); agent step titles no longer get
  squeezed to a sliver by long notes

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 17:30:25 +08:00
1 parent 4a212ed19e
commit cabd772243
32 files changed
+816 -65

No files matched your search

@@ -1,8 +1,11 @@
import { HHANCLUB_DETAIL_PAGE_FULL } from "@umt/testkit";
import { HHANCLUB_DETAIL_PAGE_2026, HHANCLUB_DETAIL_PAGE_FULL, 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";
@@ -105,3 +108,58 @@ Format : AVC[/quote]
expect(looksLikeMediaInfo("General\n一段文字\nVideo")).toBe(false);
});
});
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) },
]);
});
});