Files
UHD_Media_Tool/docs/porting/mp-rust-logic-parity.md
rootandClaude Opus 5.5 36dc048e89 feat(parser,sites): W5-D port title/zhconv/site parsing gaps found by MP differential
Title recognition (parseRelease / parseReleasePath):
- anime prefix tags (★N月新番, [国漫], 【22年日剧】), category lead bracket, `A / B` cn aliases
- `Name [01]`, `[TV 04]`, `#13`, `名称 12.mkv`, `S01 13-14`, `E13-14` episodes
- lead bracket as title vs group, trailing [Group], YTS.MX/Hall_of_C style groups, 字幕组 suffix
- `[dot.release.name][…]` unbracketing, `Season 2017` as year, leading date not airDate,
  date-only parent dir does not merge as TV
- original disc: Blu-ray + AVC/HEVC/VC-1/MPEG-2 without x26x/H.26x -> BluRay-Disc
- `{[tmdbid=;type=;s=;e=]}` explicit block, bangumi/anilist ids
- bare [1080]/[5.1], DTS-HD MA5.1, £ multi-group, 合集, pinyin title -> cn from subtitle,
  streaming platform table, CHS/CHT/GB/BIG5 in subtitle
zh-map: add 迴滾魷瑯臥駭; document one-to-many chars kept out.
Sites: tbody branch + header-row tightening via movieclaw-import rowSelector (18 definitions),
row-level case/hr/labels include the row itself, subtitle branch (searchType: "subtitles"),
parseSizeMp banker's rounding.
Differential snapshots regenerated; MP test sample sources registered in ledger.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 23:09:14 +08:00

28 KiB
Raw Permalink Blame History

MoviePilot Rust 加速函数:产品逻辑迁移核对(差分测试)

上游 /opt/moviepilot-plus/upstream/MoviePilot @ e6820a8,moviepilot_rust 0.3.3。函数清单见 moviepilot-rust-transfer.md §1。 方法:用 MP 自己的 venv 调用上游实现生成期望值(scripts/mp-sync/gen-differential.py),我们的 TS 实现跑同样的输入,逐字段比较。已知差异写进快照的 deviations,测试断言"实际差异集合等于登记集合"。所以现在测试是通过的;以后补迁消掉一条差异,测试会失败,提醒删掉对应登记。 差异分类:一致 / intentional(故意偏离,README 或 porting 文档有记录)/ missing(漏迁)/ our_bug(疑似我们的 bug)/ mp_bug(疑似 MP 的缺陷,我们的结果更好)/ representation(只是表示方式不同)/ not_expressible(Slot 表达不了)。

0. 结论

Rust 函数 业务链路 差分规模 一致率 missing / our_bug / mp_bug
parse_metainfo_fast / parse_metainfo_path_fast 搜索/订阅/RSS 的标题识别;整理规划(MetaInfoPath);过滤器按集算体积 216 条(标题 186、路径 30)× 18 字段 字段级 92.9%;扣掉 intentional/representation/mp_bug 后 97.5%;整条完全一致 84 条(38.9%),产品等价 177 条(81.9%) 14 条 / 27 条 / 28 条样本
find_metainfo_fast 标题中的显式 ID 标签 11 条带标签 8/11 2 / 0 / 1
parse_filter_rule_fast / filter_torrents(_with_trace)_fast 搜索、订阅匹配、洗版、工作流过滤、下载排序 43 条规则;28 种子 × 23 条可完整表达规则 = 644 case 95.5%(615/644);另有 3 条只能近似表达的规则,84 case 里 31 处不一致,全是能力缺口 26 / 14 / 7(另 not_expressible 13)
parse_rss_items_fast 订阅刷新(站点 RSS 模式)、工作流 fetch_rss、插件 15 份 Feed / 36 条,245 字段 95.5%(234/245) 0 / 0 / 4(另 intentional 12)
parse_indexer_torrents_fast / parse_indexer_subtitles_fast 站点搜索/浏览(SiteSpider.parse);字幕搜索 72 样本 / 127 行,2108 字段 98.1%(2069/2108) 1(字幕分支)/ 4 / 35
zhconv_fast 识别查询名简体化、中文别名判定、音乐元数据 69 条 84.1%(58/69) 10 / 0 / 1
jieba_cut 仅用于"整理历史按标题模糊搜索"(含 Agent 工具) 静态对照 — 不需要迁

MP 自身两条路径也不一致:标题识别的 Python 实现和 Rust 快路径在 4 条样本上输出不同(meta-mp.json#rustMismatch:T-06、A-03、Z-06、Z-08);索引解析在 40/72 个样本上不同(indexer.json 的 mp_rust);RSS 的 Atom/RDF 命名空间也不同(Python 0 条,Rust 有结果)。过滤器的 Python 和 Rust 交叉核对 0 处不一致。我们的对照基准一律用 Python 参考实现。

1. 产物与复现

文件 内容
scripts/mp-sync/gen-differential.py {meta,zhconv,filter,rss,indexer,all} 生成器,必须用 $MP_ROOT/.venv/bin/python 运行。只读调用上游,不复制上游源码
packages/domain/test/differential/meta-{samples,mp,deviations}.json + meta-map.ts + meta.test.ts 标题识别。meta-map.ts 把双方输出规范化到同一形式(名称忽略大小写和标点、来源归到 REMUX/BluRay/WEB-DL/…、HDR 集合、编码族、音频去分隔符、发布组集合)
packages/domain/test/differential/filter-cases.json + filter.test.ts 过滤规则
packages/domain/test/differential/zhconv-*.json + zhconv.test.ts 繁简转换
packages/adapters/test/differential/{rss,indexer}.{json,test.ts}、diff-util.ts、inputs/**、export-inputs.ts RSS 和索引解析(§4、§5)。生成前先跑 deno run -A packages/adapters/test/differential/export-inputs.ts 导出 testkit fixture;生成器把时区固定为 UTC,并冻结 dateparser 的 now,重跑结果逐字节相同

样本来源:我们已有的 107 条(release-parser.test.ts 91 条标题加 16 条路径)、MP tests/cases/meta.py 的 76 条(只用标题字符串作输入语料,属于事实数据;这份语料的来源待登记进 ledger)、补充 33 条(综艺、动漫、多集、合集、国配、特效字幕、显式 ID)。

2. parse_metainfo_fast / parse_metainfo_path_fast(标题与路径识别)

用途:MetaInfo() 用于种子标题、站点副标题和文件名,在搜索、订阅匹配、RSS 刷新、过滤器按集算体积(total_episode)时调用。MetaInfoPath() 用于整理链路(transfer/request.py _build_path_meta)。Rust 只是快路径,返回 None 时回退 Python。

分组结果(216 条):

来源 条数 完全一致 产品等价 说明
我们已有样本 107 46 106 差异几乎都是 README 登记的故意偏离
MP 自带用例 76 29 43 our_bug 65 处里有 57 处在这里,主要是动漫/字幕组命名
补充 33 9 28 —

按字段看一致率:resource_effect 和 edition 100%;part 99.1%;year 98.6%;video_encode 96.8%;audio_encode 96.3%;begin_episode 89.8%;type 88.9%;begin_season 84.3%;name 81.9%;resource_team 72.7%(59 处差异中 52 处是故意兜底)。customization 双方都没配置,不参与比较。

2.1 our_bug(27 条样本,按根因归并)

# 输入(节选) MP 我们 建议
B1 【爪爪字幕组】★7月新番[欢迎来到…教室 第二季/…](MP-03/17/18/20/32/45/56/63) cn=欢迎来到实力至上主义的教室 cn=★7月新番欢迎来到… / 国漫寻剑 / 22年日剧据… 剥离动漫前缀标签:★N月新番★、N年日剧、国漫、新番
B2 [桜都字幕组] 异世界归来的舅舅 / Isekai Ojisan [01][1080p](MP-44/45、Z-07/27) E=1,T=TV en=Isekai Ojisan 01,无集号 名称后紧跟的 [NN] / [NN][分辨率] 识别为集号(我们已支持 [Group][Name][05],不支持 Name [05])
B3 [秋叶原冥途战争][Akiba Maid Sensou][2022]…[LeagueWEB](MP-05/13/27/57) grp=LeagueWEB grp=秋叶原冥途战争 开头方括号含中文或能解析成片名时不当作组;末尾 [已知组] 优先
B4 [Caligula.The.Ultimate.Cut.2023.2160p…-BHYS@OurBits][DIY中字原盘][…].iso(MP-74) 正常解析出 en、y、2160p、UHD BluRay、HEVC、DTS-HD MA 5.1 名称取成 DIY,技术字段全丢 迁 MP 的 BRACKET_DOT_TITLE_RE:方括号内是"点分隔发布名 + 年份 + 资源词"时去括号保留内容
B5 The Long Season 2017 2160p…(MP-01) en=The Long Season y=2017 第 2017 季,年份被吞 Season 后的数字 ≥1900 时按年份处理(MP 限制 Season 后 <3 位)
B6 2022.8.2.Twelve.Monkeys.1995…(MP-52),父目录 …Collection.20240501.FRDS(MP-73) 电影 按 airDate 判成 TV 标题开头的日期、以及和年份同时出现的日期,不当作播出日期;父目录的 airDate 不参与类型合并
B7 [YTS.MX] …(MP-76)、[Hall_of_C] 诛仙 …(MP-60) 名称干净 组名混进英文名 开头方括号里是单个 token(含 ./_)时当作组名剥离
B8 组长女儿与照料专员 / 组长女儿与保姆 …(MP-21);传颂之物/Utawarerumono/うたわれるもの(MP-55) 取其一 两个中文名被拼接,或三段没拆开 / 分隔的多名称按语言拆分,中文取一个
B9 哆啦A梦:大雄的宇宙小战争(MP-06) cn 含 A A 被拆成英文名 中文词中夹的单个拉丁字母并入中文名
B10 [Gal to Kyouryuu][02][BDRIP](MP-24) en=Gal To Kyouryuu en=BDRIP 资源词不能当名称
B11 DTS-HD MA5.1(MP-48)、E13-14(MP-51)、【极影字幕社】…GB(MP-63) DTS-HD MA 5.1、E13-14、组=极影字幕社 DTSHD、只有 E13、组缺失且 GB 进了名称 各补一条词元规则

2.2 missing(14 条样本)

# 输入 MP 我们 建议
M1 {[tmdbid=94997;type=tv;s=1;e=3-4]}House.of.the.Dragon…(Z-31) tmdb:94997,S1,E3-4,TV 整块进名称 迁 {[k=v;…]} 显式身份块(也见 §3)
M2 [bangumiid=400602](Z-32) bangumi:400602 无 ExplicitId 增加 bangumi/anilist
M3 #13(MP-25)、[TV 04](MP-57)、西部世界 12.mkv(MP-29)、S01 13-14(MP-50) 能识别集号 集号为空 补充 4 种集号写法
M4 Mai Xiang S01 …,副标题 麦香 | 全36集(MP-64) cn=麦香 无中文名 迁"英文名是拼音时从副标题第一个词取中文名"
M5 [1080](MP-05)、[5.1](MP-12) 1080p、5.1 空 补方括号内的裸分辨率和裸声道
M6 iP(T-13)、B-Global(MP-10) BBC iPlayer、B-Global 空 流媒体平台表补齐(MP 有约 270 条)
M7 cXcY@FRDS(MP-49/54)、指环王三部曲合集(Z-12) 两个组都识别;去掉"合集" 只有 FRDS;保留"合集" 支持 £ 分隔符;"合集/三部曲合集"列为干扰词

2.3 intentional(与 README 偏离表一致,125 处)

未知发布组兜底 52 条;第 N 期记为 issue 12 条;有集号无季号时推定第 1 季 23 条;播出日期 → airDate 且判为 TV 6 条;路径合并(文件无剧集信号、目录是剧集)3 条;辅助词字幕文件(P-07);附加内容目录(P-05);CD1/D02 作为 part;第八季 全12期 保留季号;先导片。每一条都能在 packages/domain/README.md 的偏离表或 moviepilot-metadata-recognition.md 里找到依据,没有发现未经记录的故意偏离。

2.4 mp_bug(28 条样本,保留我们的行为)

  • MetaAnime(anitopy 路径)不输出来源和编码(15 条)。
  • 不认 1x05、S01D02、Season.2、BD50、{doubanid=}。
  • 丢中文名(MP-13/44/55、Z-07)。
  • 把 [Sub] 当成名称。
  • AC3£ 没切开,音频丢失。
  • 把 7³ 拼进音频。
  • AVC-8bit 的结果 Python 和 Rust 不一致(Z-06)。

representation 共 10 处:name 的取法(我们有中文名就取中文,MP 只在中文名全是汉字时才取);HMAX 映射成 Max 还是 HBO Max;2Audio(s);flac_ac3 记成一条还是两条。

3. find_metainfo_fast(显式 ID 标签)

用途:从标题里剥离 [tmdbid=…]、{tmdb-…}、{[k=v;…]}、[bangumiid=…],得到 media_source/media_id 以及季、集、类型。在 _prepare_meta_input 里调用。差分并入 §2 的 media_id 字段:8 条 Emby 风格的 TMDB 标签([tmdbid=]、[tmdbid-]、[tmdb=]、{tmdb=}、{tmdb-})全部一致。不一致的 3 条:M1、M2 是漏迁;{doubanid=} 是 mp_bug,我们能认。另外我们多支持 imdb 和 tvdb,MP 不认。建议:把 {[…]} 块(含 type/s/e/g)和 bangumi 补上。按不变量 1,它们仍只作为证据加一级,不直接确认身份。

4. parse_rss_items_fast(RSS/Atom)

用途:RssHelper._parse_impl(application/rss.py)先走 Rust(最多 1001 条,截到 1000),返回 None 时回退 lxml。调用方:TorrentsChain.rss(domain)(订阅刷新的站点 RSS 模式)、工作流 fetch_rss、插件。解析失败时触发 __renew_rss_url 重新生成地址。

MP 字段语义:title 取第一个后代 title 的文本(不 strip,为空就跳过);description 取 description|summary;link 取文本,没有就取 href;enclosure 取 @url,缺失回退到 link;size 只取 enclosure@length 的纯数字(description 里的大小、Torznab/nyaa 的 size 都不读);pubDate/published/updated 用 dateutil.parse 后 astimezone(None)(不带时区的按宿主本地时区);dc:creator 输出为 nickname;不输出 guid 和 category。促销、HR、做种这些字段 RSS 路径都没有。

差分:15 份输入(testkit 2 份、手写 13 份),MP Python 解析出 36 条,逐字段比较标题、描述、链接、enclosure、大小、pubdate、作者,共 245 个字段,一致 234 个。登记 16 条(packages/adapters/test/differential/rss.json#deviations):

输入 MP 我们 分类 建议
带 xmlns 的标准 Atom、RSS 1.0(RDF) Python 0 条(Rust 有) 3 条 / 1 条 mp_bug 保持
纯文本"RSS 已过期"提示 在"不以 < 开头"处就提前返回,过期判断走不到,续期永远不触发 FeedError('expired') mp_bug 保持;续期逻辑我们自己做
条目内 <source><title> 排在前面 取到来源标题 取直接子元素 title mp_bug 保持
enclosure 带 passkey 原样 [REDACTED] intentional(SA5) —
带 DTD [] 整份拒绝 forbidden_dtd intentional —
值里有换行缩进、length=" 2048 " 保留空白,大小为 0 trim 后大小为 2048 intentional 在 rss/README 补登记
&nbsp; 实体 在此截断,标题变成 "Nbsp" 保留字面量 intentional P3:补 HTML 命名实体解码表
未闭合标签 容错恢复 2 条 1 条 intentional —
pubDate … CST、纯日期 2026-09-26 按宿主本地时区解释 按美国中部 -6 / UTC 午夜 intentional(两边都可能错) P2:不带时区的时间按站点 timezone 解释

RFC822、ISO、无秒、星期写错这几类两边一致;中文日期、"3 小时前"、纯数字两边都不解析。

5. parse_indexer_torrents_fast / parse_indexer_subtitles_fast(站点列表页)

用途:SiteSpider.parse(modules/indexer/spider/__init__.py)先走 Rust,返回 None 时回退 PyQuery 的 get_info。链路是 IndexerModule.search_torrents(关键词搜索)和 refresh_torrents(浏览 browse.list/fields);字幕分支 search_type=="subtitles" 走 get_subtitle_info 和 __fill_subtitle_ids,由 /api/search 字幕接口经 SearchChain.search_subtitles_* 调用。

MP 字段语义:size 用 parse_size(二进制单位,Python 银行家舍入);做种、下载、完成数取 / 前的部分,去掉逗号,不是纯数字时为 0;促销 case 按声明顺序取第一个命中(另有 selector 模式,用 int());freedate 是原样文本;HR 配置了就输出 bool;category 投影成电影/电视剧/音乐/未知;pubdate 是站点墙上时间;imdbid 用 re_search;labels 是列表。

能否独立调用:SiteSpider(indexer).parse(html) 和 RssHelper 注入假 HTTP 端口后可以离线调用。但 MP 的内置站点定义调不出来:它们在 Cython app/application/site/sites.*.so 和加密的 user.sites.v3.bin 里,未认证时 get_indexer() 返回 {},SitesHelper() 还依赖数据库事务执行器。所以双方共用我们的 definitions/*.json(与 MP 同一 schema,来自 MovieClaw)作为输入,MP 真实站点的选择器只能静态对照。

差分:72 个样本(21 个 NexusPHP 定义 × 3 个列表页、2 个登录页、3 个手写 DSL 配置、1 个字幕页),MP 解析出 127 行,比较 title、description、size、seeders、peers、grabs、促销系数、freedate、HR、分类、pubdate、链接、imdbid、labels 共 2108 个字段,一致 2069 个。登记 41 条(indexer.json#deviations):

输入 MP 我们 分类 建议
hdarea、hddolby、hdfans 等 9 个定义的列表选择器命中表头行 Jinja 把 None 渲染成标题 "None" 空串 mp_bug ×32 两边都会产出垃圾行:收紧这 9 个定义
相对时间 "2天"、"3小时前" 用 dateparser 按当前时刻换算,结果随抓取时间漂移 不写 pubdate mp_bug 保持(生成时冻结了 now)
促销 selector 取到 "0.5" int() 报错,整行丢失 保留 intentional(SA4) —
"2.5 B" 2(银行家舍入) 3 our_bug P3:10 分钟
行元素自身带 free_bg/sticky class PyQuery 查询包含行自身,得到 dl=0、hr=true、labels .find 只查后代 our_bug ×3 P1:先决定跟 Python 还是 Rust(MP 的 Rust 和我们一样),再改 spider/spider.ts 或在 README 登记
字幕页 解析出 3 条 没有这个分支 missing P1:补字幕分支,期望数据已在 indexer.json

静态发现(差分之外):table.torrents > tr:has(...) 这种写法在显式带 <tbody> 的页面上,我们和 MP Python 都取到 0 行,MP Rust 能取到 5 行。受影响的定义有 agsvpt、chdbits、hdhome、keepfrds、ourbits、pter、pthome、ssd、tjupt。建议在 scripts/mp-sync/movieclaw-overrides/ 统一补 , table.torrents > tbody > tr 这一支(audiences 已经这样写)。

6. parse_filter_rule_fast / filter_torrents(_with_trace)_fast(过滤规则)

调用点:FilterModule.filter_torrents(rule_groups, torrents, mediainfo)。先走 Rust,它返回 None 时回退 Python(规则解析在 app/application/rules.py,基于 pyparsing)。

  • 搜索:chain/search/result.py,按站点过滤。规则组用 SearchFilterRuleGroups。
  • 订阅匹配 / RSS 刷新:chain/subscribe/match.py,每个种子单独过滤。规则组依次取 subscribe.filter_groups、BestVersionFilterRuleGroups(洗版)、SubscribeFilterRuleGroups。洗版时 pri_order <= current_priority 的种子会被拒;current_priority == 100 表示已完成。
  • 其他调用:订阅搜索、工作流 filter 动作、规则测试接口、下载排序(把 pri_order 当排序键)。
  • 规则组选取:按 media_type 和 category 选组;多个组之间是 AND 关系。

语法:用 > 切出优先层,第 1 层 pri_order=100,依次递减。层内运算优先级为 ! > & > |,可以加括号。规则名不存在时视为不匹配。单条规则的判定顺序是:tmdb 条件命中就直接放行;然后在 title+description+labels 上跑 include/exclude 正则;接着检查 size_range(单位 MB,按每集算,即总大小除以 MetaInfo 得到的 total_episode)、seeders、downloadvolumefactor、publish_time(延迟 N 分钟)。

内置关键字 → Slot:

能表达 ✔ 近似 ~ 不能 ✗
4K/1080P/720P → hard.resolutions;BLU/REMUX/WEBDL 及其取反 → sources 允许列表加 unknownPolicy;BLURAY & !BLU → requireEncode CNSUB → requiredSubtitles:["zh"](缺华语片由 tmdb 放行这条);体积区间用总量而非每集;A > B 用多个 Slot 或 rankForSlot(按来源等级优先,不按层级) UHD、H265/H264(只有软偏好)、HDR/DOLBY/SDR(只有软偏好)、ATMOS/60FPS/3D、SPECSUB(特效字幕)、CNVOI/HKVOI(国配/粤配;我们把"国语"当音轨词剔掉)、GZ(官方标签)、FREE、做种数、publish_time、跨字段 OR、音乐的 13 条规则

43 条规则样例的结论:✔ 22、~ 6、✗ 15。完整表格见 filter-cases.json#rules,其中 verdict 和 note 是逐条结论。有代表性的几条:

  • 4K & !REMUX > 1080P & WEBDL ✔,用两层 Slot 表达。
  • SPECSUB & CNSUB & !BLU ~,SPECSUB 那部分丢了。
  • !DOLBY & !REMUX ~,只能表达 !REMUX。
  • CNSUB | 4K ✗,属于跨字段 OR。
  • FREE、SEED5、GZ ✗,这些是站点元数据。

差异(不一致共 61 处已登记,完整在 filter-cases.json#deviations):

输入 MP 我们 分类 建议
2160p UHD Blu-ray HEVC … -BeyondHD、1080p Blu-ray AVC DTS-HD MA BLU(原盘) BluRay 压制(isEncode) our_bug ×12 Blu-ray 加 AVC/HEVC/VC-1、且没有 x26x 时,判为 BluRay-Disc
副标题 CHS/ENG 中字 不识别 our_bug ×2 字幕词元补 CHS/CHT/GB/BIG5
S03 全10集 60G 配体积规则 按每集 6G 判 按总量 60G 判 missing ×8 Slot 加 sizeBasis: per_episode
华语片 + CNSUB tmdb 放行 18 个种子 拒绝 missing 允许条件依赖媒体上下文
2160p.HDR.HEVC-NOSRC(没有来源) 误命中 BLU 来源未知 mp_bug ×5 保持我们的做法
副标题只有 国语 算作中字 当作音轨 mp_bug ×2 保持;以后另加 audioLang 条件
单 Slot 的 4K > 1080P > 720P 4K 总排在前面 先比来源等级,共 44 个逆序对 intentional 要复现 MP 的语义,就用多层 Slot 或优先级阶梯

trace 对比:MP 的 filter_torrents_with_trace_fast 返回 ([(index, pri_order)], [str]),trace 是中文自由文本,只写进 debug 日志。例如 种子 X 不包含任何项 [...]、大小 5G 不在范围 10000-50000MB、做种人数 3 小于 5、规则 X 不存在。它不区分"字段未知"和"不满足";OR 分支里失败的原子也会被打印出来。我们的 SlotEvaluation 是结构化的:violations[{field, code, expected, actual}] 会列出全部违规,unknown_field 单独给出,accept 时带 tier/softRank/reasons。缺两样:命中的是第几层(对应 pri_order),以及逐层的失败原因。

7. zhconv_fast(繁简转换)

用途:app/foundation/text.py convert() 直接调用 moviepilot_rust.zhconv_fast,没有 Python 回退,原生扩展是硬依赖。调用点:

  • TMDB/豆瓣/IMDb 识别时,把 cn_name 简体化后加入查询名(themoviedb/__init__.py:285 等)。
  • TMDB 中文别名的"是否已是简体"判定(tmdbapi.py:981)。
  • MetaAnime 分类标签判定。
  • 音乐元数据简体化。

用的是 MediaWiki 词表,有词组级转换。

差分:69 条(动漫、港台片名、一对多字)中 58 条一致。

  • missing 10 条:其中 6 条是字表缺字(迴、滾、魷、瑯、臥、駭);4 条是一对多字(乾杯、瞭解、頭髮、理髮),我们刻意不收,MP 靠词组表处理。
  • mp_bug 1 条:慶餘年 转成了 庆馀年,官方简体片名是 庆余年。

我们的替代:identity/zh-map.ts 大约 930 对单字映射,只在 normalizeTitle 里用于召回比较。召回本来就会再经过 pg_trgm 相似度和别名表,一两个字不同不会直接失败。 建议:暂不引入 opencc-js(约 1 MB 词典,MIT 许可,需要按需加载)。先把字表补到覆盖 TMDB 中文标题的常用繁体字(把本差分的 missing 清零,约 0.5 小时)。等到要把"简体化后的查询名"作为 TMDB 查询词时(对应 MP _prepare_search_names)再评估 opencc。它能提供词组转换和台湾/香港用词转换(如 蜘蛛人 → 蜘蛛侠),代价是包体积和冷启动。

8. jieba_cut(中文分词)

用途:只有两处,都是"整理历史按标题模糊搜索":application/history.py:712 用 "%".join(jieba_cut(title)) 生成 SQL LIKE 模式,另一处是 Agent 工具 query_transfer_history。**不参与识别、匹配和过滤。**同样没有 Python 回退。 我们的替代:资产和别名检索用 pg_trgm(0004_content、0006_sources 迁移)加 normalizeTitle。三元组相似度天然不依赖分词,对中文短标题的召回不比"分词后 LIKE"差。 结论:不引入 jieba-wasm。它的代价是 wasm 约 4 MB、词典加载 100 ms 级,而收益只落在历史搜索这一个次要功能上。以后如果要做全文检索,优先考虑 PG 的 zhparser 或 pg_bigm,而不是在应用层分词。

9. 补迁清单(按优先级)

优先级 项 涉及文件 工作量 消除的登记
P0 原盘判定:Blu-ray 加 AVC/HEVC/VC-1 且无 x26x → BluRay-Disc;BDMV/BD25/50/66/100、COMPLETE BLURAY release-parser/parse.ts 0.5 天 filter our_bug ×12
P0 动漫/字幕组命名:前缀标签剥离(B1)、Name [NN] 集号(B2)、开头方括号是片名还是组名(B3、B7)、/ 多名称拆分(B8) release-parser/parse.ts、release-groups.ts 1.5 天 meta our_bug 约 40 处
P0 [点分隔发布名][…] 去括号(B4);Season 2017 按年份处理(B5);开头日期和目录日期不当 airDate(B6) parse.ts、path.ts 0.5 天 meta our_bug 约 15 处
P1 Slot 硬条件扩展:excludeSources、hdr require/exclude、codec 硬条件、sizeBasis: per_episode、requireUhd slot/types.ts、slot/evaluate.ts 1 天 filter missing ×8,规则可表达数 22 → 约 30
P1 字幕和音轨语言:CHS/CHT/GB/BIG5 识别为中字;国语、粤语作为 audioLanguages;特效字幕标记 parse.ts、types.ts、Slot 1 天 filter our_bug ×2,SPECSUB/CNVOI 可表达
P1 显式身份块 {[tmdbid;type;s;e;g]},以及 bangumi/anilist parse.ts(explicitIds) 0.5 天 meta missing ×2
P1 优先级阶梯:SlotSpec.tiers[],评估结果带 level,洗版时按 level 比较 slot/*、server acquisition.ts 1–1.5 天 filter R05 intentional
P2 集号写法 #13、[TV 04]、名称 12.mkv、S01 13-14;裸 [1080]、[5.1];MA5.1、E13-14 parse.ts 0.5 天 meta missing/our_bug 约 10 处
P2 站点元数据条件:minSeeders、freeOnly、publishDelay、labels(需要改 evaluateCandidate 的签名) slot/*、server 1 天 FREE/SEED/GZ 可表达
P2 流媒体平台表补齐;£ 多组分隔;"合集"作干扰词;拼音英文名从副标题取中文名 parse.ts、release-groups.ts 0.5 天 meta missing 约 6 处
P1 站点定义:9 个定义补 tbody 分支,9 个定义收紧表头 movieclaw-overrides/*.json 0.5 天 indexer mp_bug ×32 的根因
P1 选择器是否包含行元素自身的语义(case/hr/labels) sites/spider/spider.ts 0.5 天 indexer our_bug ×3
P1 字幕页解析分支(get_subtitle_info、__fill_subtitle_ids) spider/spider.ts、spider/schema.ts、adapter 接口 1–1.5 天 indexer missing ×1
P2 RSS 中不带时区的时间按站点时区解释 rss/parse.ts 0.5 天 rss intentional ×2
P3 zh-map 补字(§7);parseSizeMp 改银行家舍入;RSS 补 HTML 命名实体 identity/zh-map.ts、sites/spider、rss/parse.ts 2 小时 zhconv missing ×6、indexer our_bug ×1

不迁:MetaAnime 不输出来源和编码的行为(mp_bug);期号当集号、日期删除、未知组置空(与 README 偏离表一致);jieba。

10. 补迁后一致率(W5-D,2026-09-27)

按 §9 清单完成:P0 原盘判定、P0 动漫/字幕组命名、P0 [点分隔发布名]/Season 2017/开头日期与目录日期、P1 显式身份块与 bangumi/anilist、P2 集号写法与裸 [1080]/[5.1]/MA5.1/E13-14、P2 流媒体平台表(release-parser/platforms.ts)/£ 多组/"合集"/拼音英文名、副标题拉丁字幕标记 CHS/CHT/GB/BIG5、P1 站点定义 tbody 分支与表头收紧(movieclaw-import.mjs rowSelector,18 个定义重新生成)、P1 行级标记包含行自身、P1 字幕页解析分支(SiteSpider searchType: "subtitles")、P3 zh-map 补字与 parseSizeMp 银行家舍入。MP 自带样本来源(tests/cases/meta.py、tests/test_rust_accel.py)已登记进 scripts/mp-sync/ledger.json。

链路 补迁前 补迁后 补迁后 missing / our_bug 仍保留的登记
标题识别(216 条 × 18 字段) 字段级 92.9%(3611/3888);剔除 intentional/mp_bug/representation 后 97.4%;产品等价 177 条 字段级 94.4%(3670/3888);剔除后 100%(3670/3670);产品等价 216 条,完全一致 93 条 0 / 0(前 31 / 65 处) intentional 135、mp_bug 73、representation 10
显式 ID(find_metainfo) 8/11 10/11 0 / 0 mp_bug 1({doubanid=} MP 不认)
过滤规则(可完整表达 644 case) 95.7%(616/644) 97.8%(630/644) 7(每集体积 sizeBasis,属 Slot 能力,W5-C 范围)/ 0(前 our_bug 14) mp_bug 7;近似规则 84 case 中 31 处(missing 18、not_expressible 14,含新增 R25:t17)
站点索引解析 98.1%(2069/2108,41 条登记) 99.5%(2469/2481,13 条登记;tbody 分支让 9 个定义多解析出 45 行) 0 / 0(前 missing 1、our_bug 4) mp_bug 12、intentional 1
繁简(69 条) 84.1%(58/69) 92.8%(64/69);剔除 intentional/mp_bug 后 100% 0 / 0 intentional 4(乾/瞭/髮 一对多字,见 zh-map.ts 注释)、mp_bug 1(慶餘年)
RSS 95.5%(234/245) 未变 0 / 0 mp_bug 4、intentional 12

分类调整说明:修掉 our_bug 后,有 27 处差异暴露为 MP 侧缺陷,改登记为 mp_bug,主要是 MetaAnime 丢中文名([秋叶原冥途战争][Akiba Maid Sensou]…、★7月新番[即使如此依旧步步进逼/Soredemo…],MP 取 / 后段或整块删掉中英混排块,name 随之取英文名)、MetaAnime 不输出 Baha 平台/裸 [2160]/BDRIP 来源、把片名方括号当组名、把假名名当英文名、字幕页第 3 行 div:nth-child(3) 命中行自身。新增 intentional 10 处都是既有偏离类别:有集号无季号推定第 1 季(6)、开头方括号组名兜底([YTS.MX]、[Hall_of_C],2)、剪辑版本进 cut 字段(Caligula The Ultimate Cut,2)。综艺日期/期号、cut、未知组兜底等 REQ 要求的偏离全部保留。

取舍:行内查询的 descendant-or-self 语义只用于促销 case / hr / labels(NexusPHP 把 free_bg、sticky 挂在 <tr> 上);取值字段仍只查后代,和 MP Rust 快路径一致,避免 div:nth-child(n) 命中行本身。中文名A / 中文名B 与 MP 一样取后者。

遗留:字幕分支只到解析层(SiteSpider.parse + searchType),站点适配器接口(sites/types.ts 的搜索字幕方法)未接;过滤器的每集体积、tmdb 放行条件、SPECSUB 等属 Slot 能力扩展;RSS 不带时区时间按站点时区解释(P2)与 HTML 命名实体(P3)未做。