11 KiB
MoviePilot 身份匹配(TMDB 识别)对照与差分(ID-2)
只记录上游行为与本项目的取舍,不复制上游源码。上游快照与许可见
moviepilot-metadata-recognition.md§0(e6820a8,GPLv3)。 标题解析(MetaInfo ↔ parseRelease)的字段级差分见moviepilot-metadata-recognition.md与packages/domain/test/differential/meta-*;本文只管"解析结果 → 哪个 TMDB 条目"。
1. 结论
| 项 | 值 |
|---|---|
| 差分集 | 憨憨真实标题 113 条(50 部作品):主标题 + 副标题 77 条,RSS「[分类]英文名 … -GRP[中文名 / … | 第N集 | …][体积]」21 条,短剧 15 条;同一标题只留一条 |
| MP 侧 | 99 条命中 TMDB,14 条无结果(短剧) |
修前(c3aaf90) |
100 / 113 与 MP 一致(88.5%);13 条不一致全是我们漏:RSS 分类前缀被当成中文名(武神主宰 / 三角洲 / 掌门低调点 6 条)、副标题中文名不参与(档案、猪猪侠 5 条)、中英名各命中一部(A Prophet)、英文名只在 TMDB 别名里而旧链路不取详情(China Now) |
| 修后 | 112 / 113 一致;剩 1 条是 MP 误识别(短剧《人中之龙第一季》被 MP 按译名配到《如龙》2024),我们不自动绑定。非故意偏离项一致率 100%(MP 缺陷且我们正确的计为一致) |
| 真机误识别回归 | 用户栈里被「电视剧」别名并入《东北有个周东北》的资源(32 条标题 / 副标题组合,含只有英文名的变体)全部识别回各自作品或不并入 |
| TMDB 请求 | 上行 191 次(预算 200),全部经本地缓存代理;测试离线回放 tmdb-fixtures.json |
2. MP 的识别链(按实际调用顺序)
MetaInfo(title, subtitle):副标题只取季集(init_subtitle),不取片名。RSS 标题里内嵌的描述方括号整体丢弃;开头的分类方括号[动漫]被_prepare_meta_input去掉。所以憨憨标题在 MP 里只有英文名。TheMovieDbModule._prepare_search_names:[cn_name, 简体 cn_name, en_name]去重,逐个名称识别,首个命中即停。_build_match_plan(app/modules/themoviedb/__init__.py):- 剧集:
match(name, year, tv, season_year=year, season_number),再退到无年份; - 其他:电影 + 年份 → 剧集 + 年份 →
match_multi(无类型无年份时直接 multi)。
- 剧集:
TmdbApi.match(tmdbapi.py):季年份步骤优先(__search_tv_by_season:首播年 = 季年份,或详情里该季 air_date 年份 = 季年份),再按年份[y, y+1, y-1]逐个搜;每次搜索结果按日期降序、按年份过滤后,标题 / 原标题规范化后完全相等即命中;否则逐个拉详情用别名 + 各语言译名(__get_names)比对,首个相等即命中。match_multi:电影在前、日期降序,同样先标题后别名。
特点:没有"证据强弱",第一个名称相等的候选就是答案;中英名只按顺序尝试,不会互相佐证;通用词(Archives)与续作序号没有特别处理,靠年份过滤和"第一个"兜底。
3. 我们的链路(修后)
parseRelease → recallTmdbCandidates(packages/adapters/src/tmdb/recall.ts)→ tmdbToCandidate(sources-identity/service/tmdb-candidate.ts)→ resolveIdentity(packages/domain/src/identity/match.ts)。DEC-1 的触发(何时查 TMDB)与池 / 合并事项逻辑没改。
| 环节 | MP | 我们 | 性质 |
|---|---|---|---|
分类前缀 [电视剧] [音乐] … |
去掉 | 去掉,记为 parsed.category(词表:电影 / 电视剧 / 动漫 / 综艺 / 纪录片 / 音乐 / 短剧 / 体育 / 游戏 / 软件 / 学习 / 其他) |
修复(修前当成中文名) |
RSS 内嵌描述 -GRP[中文名 / … | 第N集 | …][体积] |
丢弃 | 拆出来当副标题用(补中文名、别名、季集) | 增强 |
| 副标题片名 | 不取 | 第一段按 / 拆:主标题无中文名时第一个中文名作 titleCn(去季号后缀),其余进 altTitles;字幕 / 音轨 / 简介字段不算 |
增强(元数据差分登记为 intentional) |
Travis - 12 Memories |
— | - 12 后紧跟普通单词时不是集号 |
修复 |
| 搜索名称 | cn → 简体 cn → en,逐个短路 | cn → en,两个都搜,候选合成一个池;通用词(分类词 / 单字 / 停用词)不搜 | 故意偏离:收集后打分,同名多候选进池而不是取第一个 |
| 搜索语言 | TMDB_LOCALE 单语言 |
首选语言单语言(searchRaw) |
对齐(修前是 zh-CN + en-US 各搜一遍) |
| 类型 | 剧集线索只搜 tv;否则 movie → tv → multi | 剧集线索只搜 tv;否则 movie + tv | 对齐 |
| 别名回退 | 标题对不上时对全部候选逐个拉详情 | 年份相容的候选按年份相近度取前 3 个拉详情,直接命中的前 2 个也拉(看另一种文字的名称能否佐证);详情失败保留搜索结果 | 故意偏离:限次 |
| 标题 vs 别名 | 同等对待 | 主标题 / 原标题 / 其他语言本地化标题 = exact;alternative_titles + translations + 副标题别名 = alias | 故意偏离:别名证据弱一级 |
| 年份 | 年份过滤 [y, y+1, y-1],季年份优先 |
精确 / 季首播年 / ±1 / 未知 / 不符(不符直接拒绝) | 对齐 |
| 续作 / 章节 | 无 | Chapter III、Part Two、第3章、Ⅲ 解析成序号,候选名里有序号且不一致 → 拒绝,一致 → 加分 |
增强 |
| 季号 | 只用于季年份 | 明写的季号不在候选(取了详情)的季列表里 → 扣分 | 增强 |
| 通用词 | 无 | 单个英文词只靠别名命中、且池里 ≥3 个候选名称含该词(Archives)→ 封顶待确认;中文名命中则不受影响 |
增强 |
| 别名佐证 | 无 | 只靠别名命中时需要年份相容或第二种文字的名称也命中,否则封顶待确认 | 增强(人中之龙 → 《如龙》即此类) |
| 多候选 | 取第一个 | 同等级时:年份精确优先 → 命中的名称文字种数(中文 / 拉丁)→ 分值;严格占优才自动确认,完全对称进池 | 故意偏离 |
3.1 评分权重表(DEFAULT_MATCH_WEIGHTS,可由 MatchOptions.weights 覆盖)
| 证据 | 分值 |
|---|---|
| 标题 exact / alias / partial / none | 4 / 3 / 1 / 0 |
| 年份 exact / season_year / ±1 / 未知 | 3 / 3 / 2 / 0(不符直接拒绝) |
| 年份未知但明写季号且类型一致 | +2 |
| 第二种文字的名称也命中 | +1 |
| 续作序号一致 | +1(冲突直接拒绝) |
| 明写季号不在候选季列表 | −3 |
| 定级阈值 | ≥7 rule_confirmed,≥5 multi_evidence,≥1 needs_confirm |
硬规则优先于分值:年份不符 / 续作冲突 / 标题无关 → 拒绝;部分包含、通用词只靠别名、别名缺佐证、类型冲突 → 封顶 needs_confirm;显式 ID 提升一级。默认值下 v0.1 的判定不变(identity.test.ts 原样通过)。分值不是概率,只用于定级和同级排序。
4. 差分方法
- 样本:
packages/domain/test/differential/identity/samples.json,2026-09-29 从用户栈source_release(憨憨)只读导出,同标题去重;非短剧全收(98 条 / 35 部),短剧取前 15 部。 - MP 侧:
gen-identity-mp.py(MP 仓库.venv,MetaInfo未注入 Rust 加速器,走 Python 参考实现;名称识别直接调TheMovieDbModule._prepare_search_names+_search_by_name,不含识别缓存、插件辅助识别与详情补全),结果mp.json。 - 我们侧:
apps/server/test/sources-identity/identity-differential.lib.ts(与生产同一组函数),identity-differential.run.ts录制,结果ours.json。 - TMDB:两侧都经本地缓存代理(会话临时目录,不入库):同一请求只上行一次;搜索结果集与
language无关,所以 zh-CN 为空时其他语言直接判空;带年份的搜索先取无年份结果,总数 ≤20 时按日期年份本地过滤;上行计数封顶。上行 191 次后改为离线,离线未命中的 13 个详情请求按"取不到详情"处理(录进 fixture 为 404,回放同样走这条分支)。 - 修前基线:在
c3aaf90的工作树里用旧链路(searchAll双语言、旧打分)跑同一批样本。基线需要的 en-US 搜索由代理从 zh-CN 结果 + 已缓存详情的英文译名合成,48 个未缓存请求按空结果近似——基线数字是近似值,修后数字是实测回放。 - 归因:
deviations.json(mp_defect/intentional/known_gap);测试要求未登记的样本全部与 MP 一致。 - 回归测试:
identity-differential.test.ts(unit 组,不连 PG、不连 TMDB),含 brief 三类案例与真机误识别场景(poisoned-local.json)。
重新录制:
# 1. 起缓存代理(TMDB key 只从 .local/admin-credentials 读),见会话记录;然后
cd /opt/moviepilot-plus/upstream/MoviePilot && PYTHONPATH=. TMDB_PROXY=http://127.0.0.1:18731 \
.venv/bin/python /opt/uhd_media_tool/packages/domain/test/differential/identity/gen-identity-mp.py
TMDB_PROXY=http://127.0.0.1:18731 deno run -A apps/server/test/sources-identity/identity-differential.run.ts
5. brief 三类案例
| 案例 | 修前 | 修后 |
|---|---|---|
[动漫]Martial Master S01 2020 …[武神主宰 / … ](3 条) |
分类词「动漫」当中文名,英文名在 zh-CN 结果里对不上 → 无匹配 | 中文名 武神主宰 精确 + 2020 精确 → rule_confirmed,2023 的同名剧年份不符被拒 → 自动确认 tv:110181 |
Mononoke The Movie Chapter III … 2026 |
生产里判歧义 | 候选 1322562 的英文译名精确 + 年份精确 + 第 3 章序号一致 → 自动确认;第 1 / 2 章若在池里按序号冲突拒绝 |
Archives S01 2009(副标题 档案 / 档案解密) |
只有英文名,部分包含 → 待确认 | 中文名 档案 精确 + 2009 → 自动确认 tv:201401;没有副标题时 Archives 只靠别名且池里撞名 → 待确认 |
6. 真机误识别(2026-09-29,用户栈)
憨憨 RSS 标题带分类前缀,修前解析器把「电视剧」当中文名;第一条这样的资源经本地别名命中《东北有个周东北》后,bindAuto 把「电视剧」和各资源的英文名(Irreplaceable、Spark of the Long March、A Prophet、Spring of the Blade)都登记成站点别名,后续资源再经这些别名以"别名 + 年份精确"自动并入,链式扩散。修复:
- 解析:分类前缀识别为
category,不进片名。 - 比对:通用词(分类词 / 单字 / ≤2 个拉丁字符 / 纯数字 / 停用词)不作为查询名,也不作为候选别名参与比对;只靠别名命中需要年份相容或第二种文字的名称佐证。
- 登记:自动绑定只在标题精确命中(或别名命中且有第二种文字佐证)时登记站点别名,通用词不登记。
- 标题更新:快照 upsert 在标题 / 副标题变化时重算
parsed并发release.observed(isNew=false);Feed 标题修正同样;路由对isNew=false带force重新识别(触发来源仍是 observed,DEC-1 不变),重新识别后不能确认的撤销原自动绑定。 - 存量:
scripts/identity-reevaluate.ts --rollback-wrong(见docs/runbook/identity.md)。在用户库副本上演练:解除 23 条误并入、删除 7 个站点别名(「电视剧」「动漫」「综艺」与 4 个链式英文名),资源退回池并排入立即复查。
7. 已知差距
- 别名回退限次(前 3 + 直接命中前 2):MP 会把全部候选的详情拉一遍。候选多且目标排得靠后时,我们可能拿不到别名证据而进池(不会误绑)。
- 同名多候选我们进池,MP 取日期最新的一个:MP 在这类场景"答对"时算我们漏。本批样本未出现。
- 剧集组(episode group)季年份未迁移(同
match.ts头注释)。 - 副标题片名只取第一段;第一段是简介句子的站点(非憨憨格式)会被
cnTitleFromSubtitle的长度 / 空白 / 句读守卫挡掉,但守卫是启发式。