feat(sites): on-demand site detail page content (INT-2)
- SiteReleaseDetail.page: description, MediaInfo/BDInfo, screenshots,
IMDb / Douban / TMDB ids, file list, tags, uploader, plus a parse probe
(labels found) for layout-change diagnosis. Generic NexusPHP rules
(field label -> next sibling) starting from the MovieClaw detail
selectors; site overrides under umt.detail.page (HHClub: div labels).
M-Team builds the same structure from descr BBCode + mediainfo.
- Fetched only on demand (drawer open without cache, "复核详情",
release.inspect); content in kv_cache region site.detail for the
site detail cache period (>= 24 h); within the window the site is not
requested again. External ids persist in source_release.raw._detail,
which list snapshots do not overwrite.
- Screenshots only as proxy paths /api/v1/img/site-detail/{rid}/{i}:
public-address check on every hop, no site credentials, magic-byte
image check, 8 MB cap. Poster proxy shares the same helper.
- Identity: site ids count as explicit-id evidence; when TMDB is
queried, a TMDB id goes straight to details and an IMDb id to
/find (no title search on a hit). Douban only for local recall.
- Release drawer "站点详情页" section; fixtures and tests for HHClub div
layout, rowhead layout, BBCode, cache/proxy/identity end to end.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6e9e8efec2
commit
04cffcd1f1
30 files changed
+1133
-64
No files matched your search
@@ -0,0 +1,107 @@
|
||||
import { HHANCLUB_DETAIL_PAGE_FULL } from "@umt/testkit";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hhanclubDefinition } from "../definitions/index.ts";
|
||||
import { looksLikeMediaInfo, pageFromBbcode } from "../detail-text.ts";
|
||||
import { getSiteDefinition } from "../registry.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 <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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user