# 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 的识别链(按实际调用顺序) 1. `MetaInfo(title, subtitle)`:副标题**只取季集**(`init_subtitle`),不取片名。RSS 标题里内嵌的描述方括号整体丢弃;开头的分类方括号 `[动漫]` 被 `_prepare_meta_input` 去掉。所以憨憨标题在 MP 里只有英文名。 2. `TheMovieDbModule._prepare_search_names`:`[cn_name, 简体 cn_name, en_name]` 去重,逐个名称识别,首个命中即停。 3. `_build_match_plan`(`app/modules/themoviedb/__init__.py`): - 剧集:`match(name, year, tv, season_year=year, season_number)`,再退到无年份; - 其他:电影 + 年份 → 剧集 + 年份 → `match_multi`(无类型无年份时直接 multi)。 4. `TmdbApi.match`(`tmdbapi.py`):季年份步骤优先(`__search_tv_by_season`:首播年 = 季年份,或详情里该季 air_date 年份 = 季年份),再按年份 `[y, y+1, y-1]` 逐个搜;每次搜索结果按日期降序、按年份过滤后,**标题 / 原标题规范化后完全相等**即命中;否则**逐个拉详情**用别名 + 各语言译名(`__get_names`)比对,首个相等即命中。 5. `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`)。 重新录制: ```sh # 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`)都登记成站点别名,后续资源再经这些别名以"别名 + 年份精确"自动并入,链式扩散。修复: 1. 解析:分类前缀识别为 `category`,不进片名。 2. 比对:通用词(分类词 / 单字 / ≤2 个拉丁字符 / 纯数字 / 停用词)不作为查询名,也不作为候选别名参与比对;只靠别名命中需要年份相容或第二种文字的名称佐证。 3. 登记:自动绑定只在标题精确命中(或别名命中且有第二种文字佐证)时登记站点别名,通用词不登记。 4. 标题更新:快照 upsert 在标题 / 副标题变化时重算 `parsed` 并发 `release.observed(isNew=false)`;Feed 标题修正同样;路由对 `isNew=false` 带 `force` 重新识别(触发来源仍是 observed,DEC-1 不变),重新识别后不能确认的撤销原自动绑定。 5. 存量:`scripts/identity-reevaluate.ts --rollback-wrong`(见 `docs/runbook/identity.md`)。在用户库副本上演练:解除 23 条误并入、删除 7 个站点别名(「电视剧」「动漫」「综艺」与 4 个链式英文名),资源退回池并排入立即复查。 ## 7. 已知差距 - 别名回退限次(前 3 + 直接命中前 2):MP 会把全部候选的详情拉一遍。候选多且目标排得靠后时,我们可能拿不到别名证据而进池(不会误绑)。 - 同名多候选我们进池,MP 取日期最新的一个:MP 在这类场景"答对"时算我们漏。本批样本未出现。 - 剧集组(episode group)季年份未迁移(同 `match.ts` 头注释)。 - 副标题片名只取第一段;第一段是简介句子的站点(非憨憨格式)会被 `cnTitleFromSubtitle` 的长度 / 空白 / 句读守卫挡掉,但守卫是启发式。