Files
UHD_Media_Tool/packages/adapters/src/sites/user-parsers/hddolby.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

134 lines
5.3 KiB
TypeScript

/**
* SPDX-License-Identifier: GPL-3.0-only
* 移植自 MoviePilot(https://github.com/jxxghp/MoviePilot,GPLv3),本文件按 GPLv3 条款提供。
* @mp-source app/modules/indexer/parser/hddolby.py @ e6820a8
*
* HDDolby 用户数据解析器(MP `HDDolbySiteUserInfo`,apikey 模式)。
* 函数对照:`HDDolby_sysRoleList` → `HDDOLBY_SYS_ROLE_LIST`、`_parse_site_page` → `_parseSitePage`(api.<域>)、
* `_parse_logged_in`(恒 True)、`_parse_user_base_info` → `_parseUserBaseInfo`(api/v1/user/data)、
* `_parse_user_torrent_seeding_info` → `_parseUserTorrentSeedingInfo`(api/v1/user/peers)、
* `_pase_unread_msgs` → `_paseUnreadMsgs`(API 只给未读数,正文借 NexusPHP 解析器用 Cookie 读网页)。
* 注:`_user_basic_params = {}` 在 Python 中为假值,故 profile/peers 两个接口实际为 GET(与 MP 一致)。
*/
import { type NextPage, SiteParserBase } from "./base.ts";
import { extractDomain, pyFloat, pyInt } from "./common.ts";
import { NexusPhpSiteUserInfo } from "./nexus-php.ts";
import { SiteSchema, type SiteSchemaValue } from "./types.ts";
type Json = Record<string, unknown>;
/** 用户级别字典(MP `HDDolby_sysRoleList`) */
export const HDDOLBY_SYS_ROLE_LIST: Record<string, string> = {
"0": "Peasant",
"1": "User",
"2": "Power User",
"3": "Elite User",
"4": "Crazy User",
"5": "Insane User",
"6": "Veteran User",
"7": "Extreme User",
"8": "Ultimate User",
"9": "Nexus Master",
"10": "VIP",
"11": "Retiree",
"12": "Helper",
"13": "Seeder",
"14": "Transferrer",
"15": "Uploader",
"16": "Torrent Manager",
"17": "Forum Moderator",
"18": "Coder",
"19": "Moderator",
"20": "Administrator",
"21": "Sysop",
"22": "Staff Leader",
};
export class HDDolbySiteUserInfo extends SiteParserBase {
static override schema: SiteSchemaValue | null = SiteSchema.HDDolby;
override requestMode = "apikey" as const;
protected _parseSitePage(_htmlText: string): void {
this._baseUrl = `https://api.${extractDomain(this._baseUrl)}`;
this._userTrafficPage = null;
this._userDetailPage = null;
this._userBasicPage = "api/v1/user/data";
this._userBasicParams = {};
this._userBasicHeaders = { "Content-Type": "application/json", Accept: "application/json, text/plain, */*" };
this._sysMailUnreadPage = null;
this._userMailUnreadPage = null;
this._mailUnreadParams = {};
this._torrentSeedingPage = "api/v1/user/peers";
this._torrentSeedingParams = {};
this._torrentSeedingHeaders = { "Content-Type": "application/json", Accept: "application/json, text/plain, */*" };
this._additionHeaders = { "x-api-key": this.apikey ?? "" };
}
protected override _parseLoggedIn(_htmlText: unknown): boolean {
return true;
}
protected _parseUserBaseInfo(htmlText: string): void {
if (!htmlText) return;
const detail = JSON.parse(htmlText) as Json | null;
if (detail?.status !== 0) return;
const infos = detail.data as Json[] | null;
if (!infos?.length) return;
const u = infos[0]!;
this.userid = u.id == null ? null : String(u.id);
this.username = (u.username as string) ?? null;
this.userLevel = HDDOLBY_SYS_ROLE_LIST[String(u.class || "1")] ?? null;
this.joinAt = (u.added as string) ?? null;
this.upload = pyInt(u.uploaded || "0");
this.download = pyInt(u.downloaded || "0");
this.ratio = this.download ? Math.round((this.upload / this.download) * 100) / 100 : 0;
this.bonus = pyFloat(u.seedbonus || "0");
this.messageUnread = pyInt(u.unread_messages || "0");
}
protected _parseUserTrafficInfo(_htmlText: string): void {}
protected _parseUserDetailInfo(_htmlText: string): void {}
protected _parseUserTorrentSeedingInfo(htmlText: string, _multiPage = false): NextPage {
if (!htmlText) return null;
const seedingInfo = JSON.parse(htmlText) as Json | null;
if (seedingInfo?.status !== 0) return null;
const torrents = (seedingInfo.data ?? []) as Json[];
let pageSeedingSize = 0;
const pageSeedingInfo: Array<[number, number]> = [];
for (const info of torrents) {
const size = Number(info.size ?? 0);
const seeder = Number(info.seeders || 1);
pageSeedingSize += size;
pageSeedingInfo.push([seeder, size]);
}
this.seeding += torrents.length;
this.seedingSize += pageSeedingSize;
this.seedingInfo.push(...pageSeedingInfo);
return null;
}
protected _parseMessageUnreadLinks(_htmlText: string, _msgLinks: string[]): NextPage {
return null;
}
protected _parseMessageContent(_htmlText: string): [string | null, string | null, string | null] {
return [null, null, null];
}
/** API 仅返回未读数量,正文需通过 NexusPHP 网页接口读取(需 Cookie) */
protected override async _paseUnreadMsgs(): Promise<void> {
if (!this._siteCookie) return;
const nexus = new NexusPhpSiteUserInfo({ ...this._opts, cookie: this._siteCookie });
nexus.messageUnread = this.messageUnread;
nexus.messageReadForce = this.messageUnread > 0;
await (nexus as unknown as { _paseUnreadMsgs(): Promise<void> })._paseUnreadMsgs();
this.messageUnreadContents = [...nexus.messageUnreadContents];
this.httpErrors.push(...nexus.httpErrors);
if (this.messageUnreadContents.length && !this.messageUnread) {
this.messageUnread = this.messageUnreadContents.length;
}
}
}