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:
1 parent
cf85481d95
commit
d9f2af2f3c
166 files changed
+16577
-186
No files matched your search
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
* 移植自 MoviePilot(https://github.com/jxxghp/MoviePilot,GPLv3),本文件按 GPLv3 条款提供。
|
||||
* @mp-source app/modules/indexer/parser/ipt.py @ e6820a8
|
||||
*
|
||||
* IPTorrents 站点用户数据解析器(MP `IptSiteUserInfo`)。
|
||||
* 函数对照:`_parse_user_base_info` → `_parseUserBaseInfo`、`_parse_site_page`(空)、
|
||||
* `_parse_user_detail_info` → `_parseUserDetailInfo`、`_parse_user_torrent_seeding_info` → `_parseUserTorrentSeedingInfo`、
|
||||
* 其余为空实现。XPath 逐字保留;统计块下标越界时与 MP 一样抛 IndexError。
|
||||
*/
|
||||
import { type NextPage, SiteParserBase } from "./base.ts";
|
||||
import { normalizeDatetime, parseFloatMp, parseIntMp, reSearch } from "./common.ts";
|
||||
import { hasChildElements, parseHtml } from "./dom.ts";
|
||||
import { SiteSchema, type SiteSchemaValue } from "./types.ts";
|
||||
|
||||
/** Python 列表下标(越界抛 IndexError,保持 MP 失败语义) */
|
||||
function at<T>(arr: T[], i: number): T {
|
||||
const v = i < 0 ? arr[arr.length + i] : arr[i];
|
||||
if (v === undefined) throw new Error("IndexError: list index out of range");
|
||||
return v;
|
||||
}
|
||||
|
||||
export class IptSiteUserInfo extends SiteParserBase {
|
||||
static override schema: SiteSchemaValue | null = SiteSchema.Ipt;
|
||||
|
||||
protected _parseUserBaseInfo(htmlText: string): void {
|
||||
const html = parseHtml(this._prepareHtmlText(htmlText));
|
||||
if (!html) return;
|
||||
const names = html.strings('//a[contains(@href, "/u/")]//text()');
|
||||
const ids = html.strings('//a[contains(@href, "/u/")]/@href');
|
||||
if (names.length) this.username = String(at(names, -1));
|
||||
if (ids.length) {
|
||||
const m = reSearch(/\/u\/(\d+)/, ids[0]);
|
||||
if (m?.[0].trim()) {
|
||||
this.userid = m[1]!;
|
||||
this._userDetailPage = `user.php?u=${this.userid}`;
|
||||
this._torrentSeedingPage = `peers?u=${this.userid}`;
|
||||
}
|
||||
}
|
||||
const stats = html.nodes('//div[@class = "stats"]/div/div');
|
||||
if (stats.length) {
|
||||
const s = stats[0]!;
|
||||
const spans = s.strings("span/text()");
|
||||
const links = s.nodes("a");
|
||||
this.upload = this.numFilesize(String(at(spans, 1)).trim());
|
||||
this.download = this.numFilesize(String(at(spans, 2)).trim());
|
||||
this.seeding = parseIntMp(at(at(links, 2).strings("text()"), 0));
|
||||
this.leeching = parseIntMp(at(at(links, 2).strings("text()"), 1));
|
||||
this.ratio = parseFloatMp(String(at(spans, 0)).trim().replaceAll("-", "0"));
|
||||
this.bonus = parseFloatMp(at(at(links, 3).strings("text()"), 0));
|
||||
}
|
||||
}
|
||||
|
||||
protected _parseSitePage(_htmlText: string): void {}
|
||||
|
||||
protected _parseUserDetailInfo(htmlText: string): void {
|
||||
const html = parseHtml(htmlText);
|
||||
if (!hasChildElements(html)) return;
|
||||
const levels = html.strings('//tr/th[text()="Class"]/following-sibling::td[1]/text()');
|
||||
if (levels.length) this.userLevel = levels[0]!.trim();
|
||||
const joinAt = html.strings('//tr/th[text()="Join date"]/following-sibling::td[1]/text()');
|
||||
if (joinAt.length) this.joinAt = normalizeDatetime(joinAt[0]!.split(" (")[0]) ?? null;
|
||||
}
|
||||
|
||||
protected _parseUserTorrentSeedingInfo(htmlText: string, _multiPage = false): NextPage {
|
||||
const html = parseHtml(htmlText);
|
||||
if (!hasChildElements(html)) return null;
|
||||
let seedingEndPos = 3;
|
||||
if (html.nodes('//tr/td[text() = "Leechers"]').length) {
|
||||
seedingEndPos = html.nodes('//tr/td[text() = "Leechers"]/../preceding-sibling::tr').length + 1;
|
||||
seedingEndPos = seedingEndPos - 3;
|
||||
}
|
||||
let pageSeeding = 0;
|
||||
let pageSeedingSize = 0;
|
||||
const torrents = html.strings('//tr/td[text() = "Seeders"]/../following-sibling::tr/td[position()=6]/text()');
|
||||
if (torrents.length) {
|
||||
pageSeeding = seedingEndPos;
|
||||
// Python 切片 [:n](n 为负时从尾部截)
|
||||
for (let perSize of torrents.slice(0, seedingEndPos)) {
|
||||
if (perSize.includes("(") && perSize.includes(")")) {
|
||||
perSize = perSize.split("(").at(-1)!;
|
||||
perSize = perSize.split(")")[0]!;
|
||||
}
|
||||
pageSeedingSize += this.numFilesize(perSize);
|
||||
}
|
||||
}
|
||||
this.seeding = pageSeeding;
|
||||
this.seedingSize = pageSeedingSize;
|
||||
return null;
|
||||
}
|
||||
|
||||
protected _parseUserTrafficInfo(_htmlText: string): void {}
|
||||
|
||||
protected _parseMessageUnreadLinks(_htmlText: string, _msgLinks: string[]): NextPage {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected _parseMessageContent(_htmlText: string): [string | null, string | null, string | null] {
|
||||
return [null, null, null];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user