Files
UHD_Media_Tool/docs/porting/mp-rust-logic-parity.md
T
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

242 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)未做。