feat(adapters): port MoviePilot open site framework (7 spiders, 20 user parsers) + MovieClaw v0.27.0 site definitions + mp-sync tools (W1-3c)

- sites/spiders: Haidan/HDDolby/RousiPro/SunnyPT/TNode/TorrentLeech/Yema (+mTorrent wrapper), specialized SiteAdapter, indirect download resolved at download time
- sites/user-parsers: SiteParserBase + all 20 MP SiteSchema parsers (lxml XPath via xpath+xmldom), refreshSiteUserData with schema fallback
- sites/definitions: zod SiteDefinition schemas, 23 MovieClaw v0.27.0 (MIT) definitions via scripts/mp-sync/movieclaw-import.mjs, registry getSiteDefinition/listSiteDefinitions, NOTICE
- scripts/mp-sync: ledger.json (upstream sha256), check.mjs, build-ledger.mjs; docs/porting/mp-sync-ledger.md
- license-and-provenance: add copied-mit kind, re-anchor MovieClaw rows to v0.27.0

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 18:39:27 +08:00
1 parent cf85481d95
commit d9f2af2f3c
166 files changed
+16577 -186

No files matched your search

@@ -0,0 +1,58 @@
/**
* SPDX-License-Identifier: GPL-3.0-only
* 移植自 MoviePilot(https://github.com/jxxghp/MoviePilot,GPLv3),本文件按 GPLv3 条款提供。
* @mp-source app/modules/indexer/parser/nexus_project.py @ e6820a8
*
* NexusProject 站点用户信息解析器,继承 NexusPHP 通用解析。
*
* 函数对照(`NexusProjectSiteUserInfo`):`_parse_site_page` → `_parseSitePage`(兼容 `user_detail.php?uid=` 变种)、
* `_parse_user_traffic_info` → `_parseUserTrafficInfo`(兼容 `span#user_info` 无 B 后缀的流量文本)。正则逐字保留。
*/
import { reSearch } from "./common.ts";
import { parseHtml } from "./dom.ts";
import { NexusPhpSiteUserInfo, round3 } from "./nexus-php.ts";
import { SiteSchema, type SiteSchemaValue } from "./types.ts";
export class NexusProjectSiteUserInfo extends NexusPhpSiteUserInfo {
static override schema: SiteSchemaValue | null = SiteSchema.NexusProject;
protected override _parseSitePage(htmlText: string): void {
const text = this._prepareHtmlText(htmlText);
let userDetail = reSearch(/userdetails\.php\?id=(\d+)/, text);
if (userDetail?.[0].trim()) {
this._userDetailPage = userDetail[0].trim().replace(/^\/+/, "");
this.userid = userDetail[1]!;
} else {
// 兼容部分 NexusProject 变种站点(如 star-space.net)的 p_user/user_detail.php?uid= 用户定位格式
userDetail = reSearch(/user_detail\.php\?uid=(\d+)/, text);
if (userDetail?.[0].trim()) {
this._userDetailPage = userDetail[0].trim().replace(/^\/+/, "");
this.userid = userDetail[1]!;
const uname = reSearch(/user_detail\.php\?uid=\d+[^>]*?>\s*<[^>]*>([^<]+)</, text);
if (uname) this.username = uname[1]!.trim();
}
}
this._torrentSeedingPage = `viewusertorrents.php?id=${this.userid}&show=seeding`;
}
protected override _parseUserTrafficInfo(htmlText: string): void {
// 兼容 span#user_info / span#user_info_no_hover:"上传:445.11 G" / "下载:29.61 G"(单位无 B 后缀)
try {
const html = parseHtml(htmlText);
if (html) {
const bodyText = html
.strings('//span[@id="user_info"]//text() | //span[@id="user_info_no_hover"]//text()')
.join(" ");
let m = reSearch(/上传[::]\s*([\d.,]+)\s*([GMKT]?i?B?)/i, bodyText);
if (m) this.upload = this.numFilesize(`${m[1]!.replaceAll(",", "")} ${m[2]!.toUpperCase()}B`);
m = reSearch(/下载[::]\s*([\d.,]+)\s*([GMKT]?i?B?)/i, bodyText);
if (m) this.download = this.numFilesize(`${m[1]!.replaceAll(",", "")} ${m[2]!.toUpperCase()}B`);
if (this.upload && this.download) this.ratio = round3(this.upload / this.download);
if (this.upload || this.download) return;
}
} catch {
// 与 MP 一致:吞掉异常回落到通用解析
}
super._parseUserTrafficInfo(htmlText);
}
}