fix(adapters): 观众详情页按真实页面适配(DET-2)
- 促销:详情 h1 是 font.free / font.halfdown 等 + 剩余时间 span[title],新增 umt.detail.promo 覆盖列表促销字段(以前一律判成无促销并覆盖列表的免费) - 标题去掉 h1 里的「剩余时间:」;发布时间不再取标题区的促销到期时间 - 做种 / 下载 / 完成数、种子 Hash:umt.detail.fields.seeders / leechers / snatched / infoHash - 副标题去掉评分徽标;标签只取最内层;上传者取 span.torrent-uploader-wrap,标签精确匹配(避开「发布者带宽」) - 简介:去掉站点声明(page.descriptionStrip)与 MediaInfo 控件(codetop / show / hide),源码换行不再算一行 - 截图排除豆瓣海报与站点横幅 - 列表副标题改取 span.torrent-subtitle-text(以前前面粘着标签文字) - 脱敏真实样本:详情 2 页进 differential/detail.test.ts,列表 1 页进 MP 差分 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
322ecb2f8c
commit
92830c3ef3
17 files changed
+1301
-69
No files matched your search
@@ -43,17 +43,35 @@ const normLabel = (t: string) =>
|
||||
.replace(/[::]+$/, "")
|
||||
.toLowerCase();
|
||||
|
||||
/** 保留换行的文本:<br> 与块级元素换行 */
|
||||
/** 换行占位:先把 <br> / 块级换行记成它,再去掉 HTML 源码里的换行(浏览器按空白处理),最后还原 */
|
||||
const BR = "\u0000";
|
||||
|
||||
/**
|
||||
* 保留换行的文本:<br> 与块级元素换行。源码换行不算换行(观众的简介是 `<br />\r\n`,
|
||||
* 两者都算会让每行之间多一空行);<pre> 里的换行保留。
|
||||
*/
|
||||
function blockText($: CheerioAPI, el: El): string {
|
||||
const c = el.clone();
|
||||
c.find("script, style").remove();
|
||||
c.find("br").replaceWith("\n");
|
||||
c.find("div, p, li, tr, pre, fieldset, blockquote, h1, h2, h3, h4, h5, h6").each((_, n) => {
|
||||
$(n).append("\n");
|
||||
c.find("pre, textarea").each((_, n) => {
|
||||
$(n).text($(n).text().replace(/\r?\n/g, BR));
|
||||
});
|
||||
return tidyText(c.text());
|
||||
c.find("br").replaceWith(BR);
|
||||
c.find("div, p, li, tr, pre, fieldset, blockquote, h1, h2, h3, h4, h5, h6").each((_, n) => {
|
||||
$(n).append(BR);
|
||||
});
|
||||
const text = c
|
||||
.text()
|
||||
.replace(/\r?\n/g, " ")
|
||||
// `<br />\r\n下一行` 的源码换行变成了行首一个空格
|
||||
.replace(new RegExp(`${BR} `, "g"), BR)
|
||||
.replaceAll(BR, "\n");
|
||||
return tidyText(text);
|
||||
}
|
||||
|
||||
/** NexusPHP MediaInfo 控件的外壳:标题栏 div.codetop、摘要表 div.show、原文 div.hide(观众三者相邻) */
|
||||
const MI_WIDGET = "div.codetop, div.show, div.hide";
|
||||
|
||||
const SIZE_TAIL = /^(.*?)\s*[(([]\s*([\d.,]+\s*[KMGTP]?i?B)\s*[))\]]\s*$/i;
|
||||
const HAS_EXT = /\.[A-Za-z0-9]{2,5}$/;
|
||||
|
||||
@@ -113,8 +131,9 @@ export function extractDetailPage($: CheerioAPI, conf: DetailConf, base: string)
|
||||
};
|
||||
addLabels(conf.rowLabel, (e) => e.nextAll(conf.rowValue).first());
|
||||
for (const sel of p.labelSelectors ?? []) addLabels(sel, (e) => e.next());
|
||||
const byLabel = (names: string[]): El | null =>
|
||||
pairs.find((x) => names.some((n) => x.label.startsWith(normLabel(n))))?.value ?? null;
|
||||
const byLabel = (names: string[], exact = false): El | null =>
|
||||
pairs.find((x) => names.some((n) => (exact ? x.label === normLabel(n) : x.label.startsWith(normLabel(n)))))
|
||||
?.value ?? null;
|
||||
page.probe.labels = [...new Set(pairs.map((x) => x.label))].slice(0, 40);
|
||||
|
||||
// 简介容器
|
||||
@@ -170,10 +189,30 @@ export function extractDetailPage($: CheerioAPI, conf: DetailConf, base: string)
|
||||
page.probe.imageCount = imgs.length;
|
||||
page.screenshots = imgs.slice(0, MAX_SCREENSHOTS);
|
||||
|
||||
// 简介纯文本:去掉 MediaInfo 块与图片
|
||||
// 简介纯文本:去掉站点固定内容、MediaInfo 块(连同控件标题栏 / 摘要表)与图片
|
||||
if (desc?.length) {
|
||||
const c = desc.clone();
|
||||
if (miBlockIndex >= 0) c.find(MI_BLOCKS).eq(miBlockIndex).remove();
|
||||
if (miBlockIndex >= 0) {
|
||||
const isMi = (n: Parameters<typeof $>[0]) => looksLikeMediaInfo(blockText($, $(n)));
|
||||
// 同一份 MediaInfo 常出现两次(摘要 div.show 与原文 div.hide):另外去掉最内层的 MediaInfo 块,
|
||||
// 在控件外壳(div.codetop / div.show / div.hide)里时连同相邻外壳一起去掉
|
||||
const extra = c
|
||||
.find(MI_BLOCKS)
|
||||
.filter((_, n) => isMi(n) && !$(n).find(MI_BLOCKS).toArray().some(isMi))
|
||||
.map((_, n) => {
|
||||
const e = $(n);
|
||||
const shell = e.parent().is(MI_WIDGET) ? e.parent() : e;
|
||||
const out = [shell.get(0)!];
|
||||
if (!shell.is(MI_WIDGET)) return out;
|
||||
for (let x = shell.prev(); x.is(MI_WIDGET) && out.length < 3; x = x.prev()) out.push(x.get(0)!);
|
||||
for (let x = shell.next(); x.is(MI_WIDGET) && out.length < 4; x = x.next()) out.push(x.get(0)!);
|
||||
return out;
|
||||
})
|
||||
.get();
|
||||
c.find(MI_BLOCKS).eq(miBlockIndex).remove();
|
||||
for (const w of extra) $(w).remove();
|
||||
}
|
||||
if (p.descriptionStrip) c.find(p.descriptionStrip).remove();
|
||||
c.find("img").remove();
|
||||
const t = blockText($, c);
|
||||
if (t) page.descriptionText = t.slice(0, 20_000);
|
||||
@@ -221,17 +260,16 @@ export function extractDetailPage($: CheerioAPI, conf: DetailConf, base: string)
|
||||
// 标签
|
||||
const tagEl = p.tags ? $(p.tags) : byLabel(LABELS.tags);
|
||||
if (tagEl?.length) {
|
||||
const parts = tagEl.find("span, a").length
|
||||
? tagEl
|
||||
.find("span, a")
|
||||
.map((_, n) => $(n).text().trim())
|
||||
.get()
|
||||
: tagEl.text().split(/[\s,,|/]+/);
|
||||
// 只取最内层的 span / a(观众外面还包了一层 span.detail-tag-list,取外层会把全部标签连成一个)
|
||||
const leaves = tagEl.find("span, a").filter((_, n) => $(n).find("span, a").length === 0);
|
||||
const parts = leaves.length ? leaves.map((_, n) => $(n).text().trim()).get() : tagEl.text().split(/[\s,,|/]+/);
|
||||
page.tags = [...new Set(parts.map((t) => t.trim()).filter((t) => t && t.length <= 20))].slice(0, 20);
|
||||
}
|
||||
|
||||
// 上传者
|
||||
const upEl = p.uploader ? $(p.uploader).first() : byLabel(LABELS.uploader);
|
||||
// 上传者标签精确匹配:观众另有「发布者带宽」一行
|
||||
const upSel = p.uploader ? $(p.uploader).first() : null;
|
||||
const upEl = upSel?.length ? upSel : byLabel(LABELS.uploader, true);
|
||||
if (upEl?.length) {
|
||||
const a = upEl.is("a") ? upEl : upEl.find("a[href*='userdetails.php']").first();
|
||||
const name = (a.length ? a.text() : upEl.text()).replace(/\s+/g, " ").trim();
|
||||
|
||||
Reference in new issue
Block a user