- 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]>
102 lines
4.3 KiB
TypeScript
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];
|
|
}
|
|
}
|