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