Files
UHD_Media_Tool/packages/adapters/src/sites/README.md
T
rootandClaude Opus 5.5 6e9e8efec2 fix(sites): HHClub parsing, resource list layout and request restraint (re-landed)
Re-lands 901b01d, which was reverted wholesale by 28974d1 (BR-2 committed
from the shared index). The agent UI files 901b01d had rolled back are
left at their AGUI-2 versions; contracts, routes, db types and the runbook
part already reached main through later commits, so this carries the rest.

HHClub (憨憨) was checked read-only against a real account (one detail
request; list, user data and health came from the product's own fetch).

Parsing fixes:
- list titles carried the site's new-torrent marker "[新]" (58 of 100
  rows; the detail page title has none); stripped with a replace filter
- refreshing a detail overwrote seeders / leechers / snatched / category
  name / H&R with null because the HHClub div detail page lacks them;
  detail values now fall back to the list snapshot (mergeDetailOverList),
  and an unknown detail promotion keeps the list promotion
- level "贵宾" is VIP (umt.vipLevels)
- sanitized minimal fixture HHANCLUB_LIST_PAGE_2026 + tests; README marks
  HHClub as verified 2026-09-29
Size, counts, category ids/names, +08:00 time and subtitle were correct.

Request restraint:
- site definitions may carry umt.defaults; merged with settings.sites
  (longer intervals, fewer pages): new settings defaultSnapshotMaxPages 1,
  defaultHealthCheckIntervalSec 6 h, defaultDetailCacheSec 24 h; default
  burst 2. HHClub: snapshot 60 min page 1 only, health 6 h, search
  concurrency 1, detail cache 24 h, 1 token / 3 s burst 2
- periodic / manual snapshots are capped to that page count; scheduled
  health checks skip sites checked within the interval; detail refresh is
  served from cache inside the window; live searches on one site queue
- every rate-limiter token is counted per site per hour
  (site_request_count, migration 0046); GET /sites/{id}/request-stats;
  site overview and resource list show requests in the last 24 hours

Resource list: two-layer rows. Main line: title + spec tags (episodes,
resolution, source, codec, HDR, group, 国语/中字, site tags) + promotion
with time left + H&R. Second line: subtitle (one line, ellipsis) and
fixed-width category / size / seed-leech-snatched / upload time
(relative, absolute on hover) columns, stale badge, inventory overlay;
folds into a card on phones.

Also aligns identity_candidate.normalized_title order in db types with
the migrations (schema drift test).

Takes effect after the stack is rebuilt (migration 0046).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 16:00:34 +08:00

13 KiB
Raw Blame History

adapters/sites

PT 站点适配器,按 MoviePilot v3(提交 e6820a8)迁移:通用配置驱动 spider(观众等 NexusPHP 站)与馒头 M-Team API。仅对 @umt/testkit 的 mock 站点验证,未连接真实站点;选择器与 API 字段均待真实账号核验。

模块与上游对照

本项目 MoviePilot 上游 说明
spider/schema.ts IndexerDefinition SiteSpider.__init__ 读取的 indexer dict 键 + app/application/site/sites.pyi 站点定义 schema(domain / search.paths / params / batch / browse / category / torrents.list / torrents.fields DSL);umt 为本项目扩展
spider/spider.ts SiteSpider app/modules/indexer/spider/__init__.py SiteSpider getSearchUrl(=__get_search_url)、parse/getInfo(=parse/get_info 全部字段)、filterText/safeQuery、isLoginOrPermissionPage、selectMediaCategories、resolveCategoryMediaType、parseSizeMp(=foundation/size.parse_size)
spider/template.ts jinja2.Template nunjucks(Jinja 兼容)渲染 title/description/date 模板
spider/adapter.ts IndexerModule.search_torrents/refresh_torrents/__search_check/__clear_search_text、submission.download_torrent 请求、Cookie 注入、登录失效识别、下载校验
spider/detail.ts —(MP 通用 spider 无详情页) 本项目扩展,选择器在定义 umt.detail
definitions/audiences.json MP 站点定义在加密 user.sites.v3.bin,不可读 按 MP schema 手写;字段结构参考 MovieClaw audiences.yaml(data-ref,未复制);加站只需新增 JSON 并在 registry.ts 登记
mteam/spider.ts app/modules/indexer/spider/mtorrent.py MTorrentSpider mteamSearchParams(=__get_params)、parseMteamResult(=__parse_result)、系数表、标签表、分类组
mteam/adapter.ts MTorrentSpider.search、__get_download_url + submission._resolve_indirect_download_url genDlToken(id 走 query)→ 临时直链 → 不带 Cookie GET
mteam/client.ts MTorrentSpider.__process_response 另加业务 code/message 检查(MP 只看 HTTP 200)
rate-limit.ts SitesHelper.check(domain)(闭源流控) 站点级令牌桶,list/search/detail/download 共享

与 MP 的偏离(只限安全、数据保真与去重)

  1. 原始分类:spider/馒头结果保留 MP 的 category(电影/电视剧/音乐/未知 投影),另加 category_id 原值;SiteRelease.category 用原值 + 站点分类名(REQ §4.5,观众 403 综艺、馒头 404/405/407 不压成 tv)。
  2. 凭据:MP 候选携带 site_cookie,馒头 enclosure 为含 x-api-key 的 base64 JSON;本项目凭据只经 SecretResolver 在请求时注入,候选、raw、错误消息均不含 cookie/apikey/passkey/一次性直链(迁移账本 SA5/MT3)。
  3. 登录失效:MP 返回空列表并置 is_error;本项目抛 SiteError('auth_expired'),不把登录页当成 0 条正常结果(REQ §4.7.1)。3xx 不自动跟随,跳 login 视为失效。
  4. 系数 selector 模式用浮点(MP int() 对 0.5 抛异常丢整条,SA4)。
  5. H&R 三态:定义里没有 hr 字段时 MP 缺省 False;本项目映射为 'unknown'(REQ §8.3 保守处理)。spider 层仍与 MP 一致:未配置不输出 hit_and_run。
  6. HTML 解析用 htmlparser2 HTML 模式(与 lxml 一样不插入 <tbody>);:has("x") 引号写法自动去引号(cheerio-select 不接受)。未移植 MP 的 Rust 加速与字幕搜索分支。
  7. 馒头:显式 categoryIds 覆盖 mtype 分类组;freeOnly 为本地过滤;业务 message 判定 auth_invalid / rate_limited / not_found。

接口

SiteAdapter{ id, kind, displayName, definitionVersion, capabilities, list(page, filters), search(query, filters), detail(id), downloadTarget(id), healthCheck() },页码对外 1 起(MP 内部 0 起)。filters = { mtype?: 'movie'|'tv'|'music', categoryIds?, freeOnly?, page? }。

SiteRelease{ siteId, siteReleaseId, title, subtitle?, category{id,name}, sizeBytes, seeders, leechers, snatched, promotion{type, downloadFactor, uploadFactor, expiresAt}, hr, uploadedAt, detailUrl, downloadUrl?, infoHash?, imdbId?, doubanId?, tags[], raw(完整 MP 字段), observedAt }。促销类型由 MP 系数组合推出(0/1 free、0/2 2xfree、0.5/1 50%、0.5/2 2x50%、0.3 30%、1/2 2x、1/1 none),系数字段未配置为 unknown;expiresAt 为 MP freedate 按 umt.timezone 转 UTC。

错误 SiteError{kind}:auth_missing / auth_expired / auth_invalid(isAuthError,停止重试)、rate_limited(retryAfterMs,已顺延限流器)、parse_error、network、site_maintenance、not_found、unsupported。

观众定义待真实页面核验

行选择器、td:nth-child(4..8) 列号、span.torrent-subtitle-line、?cat= 分类、img.pro_* 促销 case 表、freedate 取促销图标 title、search_area=4 是否支持 IMDb、浏览 0 起分页、H&R 选择器(未配置 → unknown)、span.tags 标签、详情页 h1#top/td.rowhead/#kdescr。

馒头字段待真实账号核验

  1. 搜索体只发 MP 的 {keyword, categories, pageNumber, pageSize, visible}(无 mode);浏览时 keyword: null 是否被接受。
  2. 数值字段为字符串还是数字(两者兼容);totalPages 字段名。
  3. status.discount 全集(MP 表:FREE/PERCENT_50/PERCENT_70/_2X_FREE/_2X_PERCENT_50/_2X,其余按 1)、discountEndTime / createdDate 时区(按 +08:00;MP format_timestamp 对非数字字符串原样返回)。
  4. promotionRule{discount,endTime} 与 mallSingleFree{status,endDate} 是否出现在 search 结果中。
  5. labelsNew / labels(MP 表 "0"–"7")。
  6. /api/torrent/detail(表单 id,MovieClaw 字段结构)是否可用、是否含 descr。
  7. genDlToken(id 走 query,同 MP)直链有效期、次数与频率;返回空 data 的原因。
  8. 错误 message 文案(停用/封禁/key 无效/频繁/不存在)与 HTTP 状态码。
  9. categoryNames 为自行编制的粗分类名;默认网页域 tp.m-team.cc 需按账号实际域名配置。
  10. H&R:API 未提供,恒为 unknown。

W1-3c:MP 开源站点框架全量移植 + MovieClaw 站点定义

跟随更新工具与对照总表见 docs/porting/mp-sync-ledger.md(scripts/mp-sync/)。每个移植文件头有 SPDX-License-Identifier: GPL-3.0-only、@mp-source <上游路径> @ e6820a8 和函数对照。

专用 spider(spiders/,MP app/modules/indexer/spider/*.py)

parser 键(MP SPIDER_PARSER_CLASSES) 文件 凭据 下载
Haidan spiders/haidan.ts cookie 列表直链(仅内存),raw 脱敏
HDDolby spiders/hddolby.ts apiKey(+cookie) download.php?id=&downhash=(仅内存)
RousiPro spiders/rousi.ts apiKey(Bearer) 现场请求详情接口取 data.download_url
SunnyPT spiders/sunnypt.ts apiKey 现场 download-token 换直链
TNodeSpider spiders/tnode.ts cookie + 首页 CSRF(实例内缓存 24h) 直链
TorrentLeech spiders/torrentleech.ts cookie 直链;中文关键词按 MP 返回空
Yema spiders/yema.ts apiKey(Authorization) 现场 generateDownloadKey 换 token
mTorrent spiders/mtorrent.ts(包装 mteam/*) apiKey genDlToken

createSpecializedAdapterFor(def, deps) 实现统一 SiteAdapter(list = 空关键词搜索;detail 为 unsupported);定义 umt.kind = "api",schema 见 definitions/schema.ts SpecializedSiteDefinitionSchema。MP 的 [base64(含 apikey 的请求配置)]url 两段式 enclosure 不生成,下载时按同一配置现场换直链并校验 .torrent。mteam/spider.ts 已与 mtorrent.py 逐条核对(修正:纯数字时间戳按 +08:00 转换)。

站点用户信息解析器(user-parsers/,MP app/modules/indexer/parser/*.py)

20 个 SiteSchema 全部移植:NexusPhp、NexusAudiences、NexusHhanclub、NexusProject、NexusRabbit、Gazelle、Unit3d、DiscuzX、IPTorrents、FileList、Small Horse、Zhixing、Bitpt、TNode、TorrentLeech、HDDolby、RousiPro、SunnyPT、Yema、MTorrent。

  • 基类 SiteParserBase(base.ts)与 MP 相同的获取顺序:首页登录判定 → _parseSitePage → 基础信息 → 详情 → 未读消息(fetchMessages)→ 流量 → 做种分页(上限 50 页);cookie/apikey 两种请求模式;200/500/403 视为有内容;Cloudflare 挑战页返回空。
  • XPath 用 xpath + @xmldom/xmldom(dom.ts,htmlparser2 解析,不插入 tbody),表达式与 MP 逐字一致。
  • 入口 refreshSiteUserData({name, url, schema, cookie|apikey|token, fetch?, limiter?}) → SiteUserInfo{schema, domain, userId, username, userLevel, joinAt, upload, download, ratio, bonus, seeding, seedingSize, leeching, leechingSize, seedingInfo, messageUnread, messageUnreadContents, hrCount?, errMsg, raw};声明 schema 取不到 userid 时按 MP 顺序回退到其他 schema。
  • 写操作:MTorrent / Rousi / NexusRabbit 读消息后标记已读,只在 fetchMessages: true 时发生。

站点定义体系(definitions/)

  • 23 个站点 JSON 由 scripts/mp-sync/movieclaw-import.mjs 从 MovieClaw v0.27.0(c79bb1b0,MIT) 转换(见 definitions/NOTICE-MovieClaw.md),每个带 provenance;手工覆盖层 scripts/mp-sync/movieclaw-overrides/*.json(观众、馒头沿用 W1-3b 值,HHClub/CHD 补分类名)。不要直接改 JSON,改覆盖层后重跑脚本。
  • 加载时 zod 校验(definitions/schema.ts:IndexerDefinitionSchema / MteamDefinitionSchema / SpecializedSiteDefinitionSchema,按 umt.kind 分派);注册表 getSiteDefinition(id)、listSiteDefinitions({enabledOnly})、createSiteAdapter(def, deps)。
  • 新增字段:enabled、schema(MP 用户信息解析器)、provenance、umt.detail.fields(MovieClaw 逐字段详情选择器,支持 HHClub div 详情页)、umt.profile(MovieClaw 资料选择器,备用)、umt.login、umt.authSupported、umt.customClass。选择器扩展语法 ::attr(x) / ::text / ::next_sibling_text 由 spider/mc-selector.ts 求值。
状态 站点
启用,已实测 2026-09-29(真实账号只读核对) hhanclub 憨憨
启用(主力,仍须真实账号核验) audiences 观众、chdbits 彩虹岛、mteam 馒头
enabled:false,未实测 agsvpt、hdarea、hddolby、hdfans、hdhome、hdsky、hdtime、hdvideo、keepfrds、nicept、ourbits、piggo、pter、pthome、pttime、soulvoice、ssd、tjupt、ttg

未实测站点只经 schema 校验,选择器是 MovieClaw 作者在 2026-08/09 的实测值,未经本项目核验。TTG 需要自定义适配器(中文分类、/t/<id> 路径、做种/下载同列),通用 spider 结果不可靠;OurBits 的 cat[] 分类参数已由定义表达;hddolby 在 MovieClaw 中按 HTML 站点配置,MP 则用 API spider(HDDolby),两条路径待账号确认后二选一。

待真实站点核验(W1-3c 新增)

  1. HHClub(2026-09-29 已实测,见下节):仍未见到的只有促销(promotion-tag-*)与 H&R(div.torrent-info-hr)命中的实例——当次第 1 页 100 条全部无促销、无 H&R,类名与 Jackett 现行定义一致,暂不改。

HHClub 实测记录(2026-09-29)

真实账号只读核对,对站点共发 1 次详情请求(列表、用户数据、健康检测用添加站点时产品自己抓的结果;下载令牌只核对列表里的下载链接形态,没有下载)。

项 结果
列表 div.torrent-table-sub-info 卡片、分类 ?cat[]= 链接、大小 / 做种 / 下载 / 完成 正确,全为数字;分类 401–405/412 原始 ID 与名称都在
标题 有误,已修:标题链接文本末尾带新种标记「[新]」(100 条里 58 条;详情页标题没有),title 加 replace 过滤
副标题 正确,纯文本,无 HTML 残片
发布时间 正确:span[title] 绝对时间按 +08:00 转 UTC,相对时间只进 date_elapsed
详情页 标题 / 副标题取到;没有做种、完成、分类名、基本信息行,刷新详情会用 null 冲掉列表值,已修(服务端合并,见 mergeDetailOverList)
用户数据(NexusHhanclub) 正确;等级「贵宾」之前 vip 为 null,已加 umt.vipLevels
健康检测 / 登录标志 a[href='logout.php'] 正确
下载链接 列表给 download.php?id=…&passkey=…,入库时 passkey 脱敏
标签(官方 / 中字 / 国语等) 列表没有可用的标签选择器;界面按标题解析与副标题原文生成规格标签

访问默认(umt.defaults,与 settings.sites 取更保守的一方):快照每 60 分钟只抓第 1 页、健康检测 6 小时、用户数据 6 小时、实时搜索并发 1、详情缓存 24 小时;限流 3 秒 1 个令牌、突发 2。fixture:@umt/testkit 的 HHANCLUB_LIST_PAGE_2026(脱敏最小片段)。 2. CHDBits:域名 ptchdbits.co、font.subtitle、td.embedded > span[title] 促销截止、a[href='logout.php']。 3. 专用 spider 的 JSON 字段均按 MP 读取的键编写 fixture,真实响应待核;HDDolby hr 字段语义、page_number 起点;TorrentLeech 时区;Yema tagList 类型;直链缺失时的回退地址(TODO(verify))。 4. 用户信息解析器 fixture 均为最小自编 HTML/JSON;normalizeDatetime 不处理相对时间(MP 用 dateparser)。 5. 馒头 mode=adult 独立索引(MovieClaw custom/mteam.py)未实现。