Files
UHD_Media_Tool/packages/adapters/src/sites/user-parsers/ipt.ts
T
rootandClaude Opus 5.5 d9f2af2f3c 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]>
2026-09-27 18:39:27 +08:00

102 lines
4.3 KiB
TypeScript

/**
* 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];
}
}