Merge branch 'id3'
Co-Authored-By: Claude Fable 5.1 <[email protected]> # Conflicts: # CHANGELOG.md
This commit is contained in:
commit
a9511a75f6
23 files changed
+1320
-14
No files matched your search
@@ -26,6 +26,9 @@
|
||||
3. 查了 TMDB 的,召回 TMDB 候选(`recallTmdbCandidates`:中文名 → 英文名,按类型单语言搜索,标题对不上的按年份相近度拉前几个详情比对别名 / 译名),
|
||||
交给 `domain.resolveIdentity` 打分:auto 建档 / 关联并发 `release.resolved`;否则进池。打分规则与 MP 的对照见
|
||||
`docs/porting/moviepilot-identity-match.md`(ID-2)。
|
||||
站点列表 / 详情页给出的 IMDb / TMDB ID(`raw._site` / `raw._detail`)作为 `trustedIds` 参与打分(ID-3):候选按这个 ID 取来时,
|
||||
标题对不上也至少 `multi_evidence`(可自动确认);年份冲突降为 `needs_confirm`;续作序号 / 类型冲突不放宽。
|
||||
标题认不准时走「识别升级链」(见下节)。
|
||||
4. 自动绑定时把站点标题登记为作品别名,只在标题精确命中(或别名命中且另一种文字的名称也命中)时登记,分类词 / 单字 / 停用词不登记。
|
||||
FIX-5 守卫(`service/alias-guard.ts`):逐个名称再查一遍,以下任一成立就不登记,原因记在 `resolution_attempt.best.aliases.skipped`
|
||||
(`added` 是登记了的):证据等级不是 `rule_confirmed`(`low_evidence`);资源年份与作品年份差超过 1 年且不是季年份(`year_conflict`);
|
||||
@@ -39,6 +42,62 @@
|
||||
搜索模块想让搜索结果直接识别,入队 `resolveObservation` 时传 `trigger: "search"`(目前搜索结果经 `release.observed` 进来,
|
||||
按 observed 处理;命中订阅别名的照样会查)。
|
||||
|
||||
## 识别升级链:用站点详情页的 ID(ID-3)
|
||||
|
||||
用户反馈(2026-09-30):"既然能抓 PT 站详情页,详情有 TMDB、IMDb 或豆瓣,为啥还判断不了是什么资源?" 以前详情页只在用户打开时才抓,
|
||||
标题识别不出就直接进待识别 / 待处理。现在标题识别结果是 `ambiguous` / `no_match` 时按下面的链再认一次
|
||||
(`service/id-escalation.ts`、`resolve.ts escalateIdentity`):
|
||||
|
||||
1. **抓详情**(只在资源还没抓过详情时,`detail_fetched_at` 为空):走 `refreshReleaseDetail`——站点详情缓存、共享限流器、请求计数、
|
||||
凭据失效处理都照旧;失败不阻塞识别,记在 `raw._escalation {at, status, error}`,6 小时内不再为识别重抓。
|
||||
2. **新拿到的 IMDb / TMDB ID** → TMDB 详情 / `find` → 按 `trustedIds` 打分(列表上已有的 ID 在第一轮就用过)。
|
||||
3. 还不能确认且有**豆瓣 ID** → `kv_cache` region `douban.subject` 里缓存的豆瓣条目(标题 / 别名 / 年份)→ 按条目名称 + 年份召回 TMDB →
|
||||
ID-2 打分(对照 MP `chain/media/projection.py` 豆瓣 → TMDB:原名 / 中文名 / 英文名 + 年份)。**不请求豆瓣**;条目来源只有两个:
|
||||
站点详情页的豆瓣信息块(◎片名 / ◎译名 / ◎年代,详情页同时给了豆瓣 ID 时写入,`source = site_detail`)和元数据补全(META-2)
|
||||
网页搜索结果里 `douban.com/subject/<id>` 的搜索标题与摘要(`source = web_snippet`,不覆盖 site_detail 的)。缓存 30 天。
|
||||
4. 确定(auto)→ 照常绑定:回填资源、建 / 关联作品、关闭同资源的池条目并从合并事项里摘除(组空即关)。站点标题登记为别名时
|
||||
按资源**自己的标题**重新判断(靠 ID / 豆瓣名确认、标题对不上的不登记),FIX-5 守卫照旧。
|
||||
|
||||
证据记在 `source_release.resolution_evidence.escalation` 与池条目 `query.escalation`:`detail`(`fetched` / `skipped:<原因>` /
|
||||
`deferred:cap_reached` / `failed:fetch_failed` / `not_now:batch_ingest`)、`via`(`site_id` / `douban` / null)、`ids`、`pending`、`douban`。
|
||||
|
||||
### 什么时候抓详情
|
||||
|
||||
| 触发 | 抓详情? |
|
||||
|---|---|
|
||||
| 搜索结果(`trigger: "search"`,搜索完成后逐条入队识别) | 是 |
|
||||
| 手动「重新识别」、Agent `identity.resolve`(`manual`) | 是 |
|
||||
| 池条目「元数据补全」之前(`POST /identity-pool/{id}/enrich`) | 是:资源没抓过详情或有豆瓣 ID 时先按手动重新识别走一次;认出来就返回 409「已识别为《X》,不需要补全」,不消耗补全预算 |
|
||||
| 订阅相关的候选(命中订阅 / 本地作品别名,`tracked_alias`)的池复查(`review`) | 是 |
|
||||
| RSS / 列表批量入库(`observed`) | 否。订阅相关的推迟(`not_now:batch_ingest`,15 分钟内复查时再抓);其余不抓 |
|
||||
| 上次因限额推迟(`pending`)的复查 | 是 |
|
||||
|
||||
每站每小时上限 `identity.detailIdsPerSiteHour`(设置 → 元数据 → 资源识别「识别时每站每小时最多抓详情页」,缺省 10,0 = 不抓),
|
||||
计数在 `kv_cache` region `identity.detail_ids`(key `<siteId>:<小时>`,原子自增,2 小时过期)。达到上限记 `deferred:cap_reached`、
|
||||
15 分钟内复查。这些详情请求同样计入站点请求统计(`site_request_count`)。
|
||||
|
||||
### 待处理
|
||||
|
||||
- 升级链推迟中(`pending`:每站每小时限额已满 / 入库时不抓):不开任何身份事项,之前开的从合并事项摘除。抓了但失败的不算推迟(不阻塞,照常进池 / 开事项)。
|
||||
- 资源上已有 IMDb / TMDB / 豆瓣 ID 却仍无结果:不开「没识别出作品」(`identity_unresolved`),只留在池里,可手动指定。
|
||||
- 有候选的歧义(`identity_ambiguous`)照旧开;资源被识别后(升级链 / 复查 / 手动)自动从合并事项摘除,组空即关闭。
|
||||
|
||||
### 排查
|
||||
|
||||
```sql
|
||||
-- 最近为识别抓详情的资源
|
||||
select id, title, raw->'_escalation' esc, resolution_evidence->'escalation' ev from source_release
|
||||
where raw ? '_escalation' order by (raw->'_escalation'->>'at') desc limit 20;
|
||||
-- 这一小时各站点用了多少名额
|
||||
select key, value from kv_cache where region = 'identity.detail_ids' and expires_at > now();
|
||||
-- 缓存的豆瓣条目
|
||||
select key, value->>'title' title, value->>'year' y, value->>'source' src from kv_cache where region = 'douban.subject' limit 20;
|
||||
```
|
||||
|
||||
测试:`apps/server/test/sources-identity/id-escalation.test.ts`(观众 DET-2 / 憨憨 HH-2 详情样本经注入 fetch、TMDB mock,零真实请求);
|
||||
差分组 `packages/domain/test/differential/identity/site-ids-*.json`(MP `search/result.py _match_source`:种子 IMDb 与作品一致即命中;
|
||||
我们年份 / 类型冲突时只作候选,登记为 intentional,见 `site-ids-deviations.json`),`packages/domain/test/identity-id3.test.ts`。
|
||||
|
||||
## 接管任务按文件名识别(QB-1)
|
||||
|
||||
用户手动加进 qB、贴了管理标签的任务(`download_task.origin = adopted`,没有 `source_release` / 计划)下载完成后,`verifyAndImport`
|
||||
@@ -63,7 +122,8 @@
|
||||
|---|---|
|
||||
| `unmatched` | 否 |
|
||||
| `ambiguous` / `proposed`(查过 TMDB,有候选) | 是,`identity_ambiguous`,级别 warning |
|
||||
| `no_match` / 复查已停的 `lookup_failed` | 只有本地召回有候选(与本地作品相关)时开 `identity_unresolved`,级别 warning |
|
||||
| `no_match` / 复查已停的 `lookup_failed` | 只有本地召回有候选(与本地作品相关)、且资源上没有站点给的 IMDb / TMDB / 豆瓣 ID 时开 `identity_unresolved`,级别 warning |
|
||||
| 识别升级链推迟中(ID-3) | 否(之后会抓详情再认) |
|
||||
|
||||
合并:dedupeKey `identity-group:<siteId>:<归一化标题>`,同来源同标题的资源并成一条。成员 = `identity_candidate.decision_id`
|
||||
指向该事项且仍 `pending` 的条目。标题 1 条时写资源原标题,多条时写「来源「X」的 N 条资源待识别 / 待确认身份:<标题>」,
|
||||
|
||||
@@ -112,9 +112,10 @@
|
||||
|
||||
资源详情抽屉的「站点详情页」一节:简介、MediaInfo / BDInfo、截图、IMDb / 豆瓣 / TMDB 链接、文件清单、标签、上传者。
|
||||
|
||||
- 只按需抓:打开抽屉时如果没有缓存,前端自动调一次 `GET /sites/{id}/releases/{rid}?refresh=true`(同一资源本次会话只自动一次),或点「复核详情」;`release.inspect` 的 `refreshDetail` 同一路径。列表快照、搜索不抓详情。
|
||||
- 只按需抓:打开抽屉时如果没有缓存,前端自动调一次 `GET /sites/{id}/releases/{rid}?refresh=true`(同一资源本次会话只自动一次),或点「复核详情」;`release.inspect` 的 `refreshDetail` 同一路径。列表快照、搜索本身不抓详情;标题识别认不准时,识别升级链(ID-3)会为取 ID 抓一次(同一路径、同一限流器,每站每小时有上限,见 `docs/runbook/identity.md`「识别升级链」)。
|
||||
- 缓存:内容放 PG `kv_cache` region `site.detail`,key = `source_release.id`,TTL 取站点详情缓存期(至少 24 小时)。缓存期内 `detail_fetched_at` 新鲜且缓存还在就不请求站点;kv_cache 被清掉(过期 / 重启丢 UNLOGGED 表)后下次刷新会重新抓一次。
|
||||
- 外部 ID 另写 `source_release.raw._detail`(`imdbId / doubanId / tmdb{type,id}`),列表快照 upsert 不覆盖它。身份解析把它和列表给的 `raw._site.imdbId / doubanId` 一起当显式 ID:本地召回查已接受的外部身份;需要查 TMDB 时,有 TMDB ID 直接取详情,否则 IMDb ID 走 `/find/{imdb}?external_source=imdb_id`,命中就不再按标题搜索;TMDB 不支持豆瓣 ID,豆瓣只用于本地召回。是否查 TMDB 仍按惰性识别规则(DEC-1),外部 ID 本身不触发查询。
|
||||
- 外部 ID 另写 `source_release.raw._detail`(`imdbId / doubanId / tmdb{type,id}`),列表快照 upsert 不覆盖它。身份解析把它和列表给的 `raw._site.imdbId / doubanId` 一起当显式 ID:本地召回查已接受的外部身份;需要查 TMDB 时,有 TMDB ID 直接取详情,否则 IMDb ID 走 `/find/{imdb}?external_source=imdb_id`,命中就不再按标题搜索;TMDB 不支持豆瓣 ID:豆瓣 ID 用于本地召回,识别升级链(ID-3)里再经缓存的豆瓣条目(标题 + 年份)匹配 TMDB。是否查 TMDB 仍按惰性识别规则(DEC-1),外部 ID 本身不触发查询。
|
||||
- 列表页外部 ID(ID-3 核对):NexusPHP 定义的 `torrents.fields.imdbid`(MP 字段)全部站点都有;豆瓣是 UMT 扩展字段 `torrents.fields.doubanid`(MP 不读,索引差分只比 MP 字段),写法同 imdbid。现有列表样本里只有观众 `rl-audiences-list` 带 ID 徽标(10 行豆瓣、5 行 IMDb),已给观众补 `doubanid`(`a[href*="douban.com/subject/"]` + `re_search subject/(\d+)`,覆盖层 `scripts/mp-sync/movieclaw-overrides/audiences.json`,测试 `packages/adapters/test/differential/list-ids.test.ts`)。憨憨 / 彩虹岛的列表样本没有 IMDb / 豆瓣链接(ID 只在详情页),其它站点没有列表样本,暂不加选择器。
|
||||
- 截图:响应里只有代理路径 `/api/v1/img/site-detail/{releaseId}/{index}`,服务端从缓存取原始地址,公网校验(每跳重定向复核)、不带站点凭据、按文件头认图片(不放行 SVG)、≤ 8 MB,浏览器缓存 24 小时。站内附件如果需要登录才能看会显示为裂图,不会为截图消耗站点请求。
|
||||
- 憨憨详情页(HH-2 按真实页面 `probe.labels` 核对):没有 `#kdescr`,内容分节为「标签 div + 相邻值 div」:`mediainfo`、`海报`、`豆瓣`(内有 `span.font-bold` 小标签 片名 / 年代 / 类别 / 导演 …)、`截图`、`种子文件`(只有「查看列表」按钮)、`发布者`、`发布时间`。对应规则:简介没有容器时取「豆瓣 / 影片信息 / IMDb」信息块,小标签补冒号、一项一行;截图取「截图」一节的 img(`data-src` 优先,海报不算截图),没有再取简介里的图;IMDb 依次取页面链接 → 简介 / 信息块 / MediaInfo 里的 `imdb.com/title/tt…` 或 `IMDB : tt…` 行(只收 `tt` + 7~8 位,不收孤立 tt 串),TMDB 取链接或 `TMDB : tv/294990` 行,豆瓣取链接或 `douban.com/subject/…`;兜底出的 IMDb / 豆瓣也回填到 `SiteRelease.imdbId / doubanId`;发布时间没有 `span[title]` 时取「发布时间 / 上传时间」字段文本。文件清单:定义 `umt.detail.page.fileListPath`(憨憨 `viewfilelist.php?id={id}`)配置了且页面上没有内联清单时,同一次详情刷新里再请求 1 次(走同一限流器、计数),失败只是没有清单。fixture `HHANCLUB_DETAIL_PAGE_2026` / `HHANCLUB_FILELIST_FRAGMENT`:标签名称与顺序来自真实页面,嵌套按已验证的标题 / 副标题结构推定,部署后打开一条憨憨资源的详情抽屉核对一次。
|
||||
- 观众详情页(DET-2 按真实页面核对,样本 `packages/adapters/test/differential/inputs/detail/rl-audiences-detail-*.html.txt` 已脱敏,回归测试 `test/differential/detail.test.ts`;列表页样本 `inputs/indexer/rl-audiences-list.html.txt` 进 MP 差分):标准 rowhead 表格,但有几处与通用 NexusPHP 不同,写在定义里。h1 的促销是 `font.free / font.halfdown …`(列表是 `img.pro_*`)加「剩余时间:」和到期 `span[title]`,用 `umt.detail.promo` 覆盖列表的促销字段;h1 的「剩余时间:」文本不进标题,标题区里的 `span[title]` 也不当发布时间(发布时间取「下载」行「发布于」的 span)。没有「同伴」行,做种 / 下载 / 完成数取基本信息行右侧 `#seeders / #leechers / a[title=完成]`(`umt.detail.fields.seeders / leechers / snatched`);种子 Hash 取 `#copyTorrentHashLink[data-hash]`(`fields.infoHash`,回填 `infoHash`)。副标题行右侧有豆瓣 / IMDb / 烂番茄 / Metacritic 评分徽标(`[title*=评分]`),解析时去掉;评分本身本工具没有字段,不收。标签是 `span.detail-tag-list > span.tags`,只取最内层。上传者在「下载」行的 `span.torrent-uploader-wrap`(匿名发布为「匿名」);另有「发布者带宽」一行,所以上传者标签改为精确匹配。简介开头的站点声明 `#ad_torrentdetail` 用 `umt.detail.page.descriptionStrip` 去掉;MediaInfo / BDInfo 是 `div.codetop`(文件名 / BDInfo)+ `div.show`(摘要表或同一份 BDInfo)+ `div.hide > div.codemain`(原文),三者都从简介里去掉;简介是 `<br />\r\n`,源码换行不再算一行(以前每行之间多一空行)。截图排除豆瓣海报(站点经 `dbimgs.audiences.me/?…doubanio.com…poster` 代理)与站点横幅 `/banners/`。IMDb / 豆瓣 ID 取简介里的链接;TMDB 页面上没有;单种 H&R 标记页面上没有(列表也没有),保持「未知」。文件清单不在页面上(点「N个文件」后 AJAX 加载,端点未核对),暂不另发请求。列表副标题改取 `span.torrent-subtitle-text`(`span.torrent-subtitle-line` 里先是标签,以前副标题前面会粘着「官方中字DIY…」)。
|
||||
|
||||
Reference in new issue
Block a user