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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 23:09:14 +08:00
1 parent 8352e5e881
commit 36dc048e89
41 files changed
+3111 -2372

No files matched your search

+14
View File
@@ -380,6 +380,20 @@
"sha256": "e7952ed40b7b5a66d95fe1b07e1d3915601b47c2936ff10ec6880c8a030d0b8f",
"ours": ["packages/adapters/src/sites/user-parsers/types.ts"],
"source": "tag"
},
{
"upstream": "tests/cases/meta.py",
"commit": "e6820a83ab05e8a00eb0e14adf0f357faa965c7e",
"sha256": "3af375cf2c433db9f2dbfd78cb5798718edf26d7414d4ace08a49a02d80a7884",
"ours": ["packages/domain/test/differential/meta-mp.json", "packages/domain/test/differential/meta-samples.json"],
"source": "manual"
},
{
"upstream": "tests/test_rust_accel.py",
"commit": "e6820a83ab05e8a00eb0e14adf0f357faa965c7e",
"sha256": "0259f245d27832c2ef685178a180dc7ea33f9106b5b5029f6737f199c556bbec",
"ours": ["packages/adapters/test/differential/indexer.json", "scripts/mp-sync/gen-differential.py"],
"source": "manual"
}
]
}
+11 -1
View File
@@ -59,6 +59,16 @@
},
{ "upstream": "app/modules/filemanager/transhandler.py", "ours": ["packages/adapters/src/fs/move.ts"] },
{ "upstream": "app/modules/emby/emby.py", "ours": ["packages/adapters/src/emby/client.ts"] },
{ "upstream": "app/application/mediaserver.py", "ours": ["packages/adapters/src/emby/client.ts"] }
{ "upstream": "app/application/mediaserver.py", "ours": ["packages/adapters/src/emby/client.ts"] },
{
"upstream": "tests/cases/meta.py",
"ours": ["packages/domain/test/differential/meta-samples.json", "packages/domain/test/differential/meta-mp.json"],
"note": "MP 自带标题识别用例:76 条标题字符串作差分输入语料(事实数据,不复制断言代码);上游变化时核对新增/修改的用例并重跑 gen-differential.py meta"
},
{
"upstream": "tests/test_rust_accel.py",
"ours": ["scripts/mp-sync/gen-differential.py", "packages/adapters/test/differential/indexer.json"],
"note": "字幕页 HTML 与字幕字段配置取自 test_rust_subtitle_parser_is_several_times_faster_than_python,作 hhanclub-subtitles@inline 差分样本"
}
]
}
+27 -1
View File
@@ -238,7 +238,7 @@ function convertNexus(y, file) {
batch: { delimiter: " ", space_replace: "_" },
browse: { path: `${s.torrent_list_path}?page={page}`, start: Number(s.page_offset) === -1 ? 0 : 1 },
category: { movie: item("movie"), tv: item("tv"), music: item("music") },
torrents: { list: { selector: s.torrent_row_css }, fields },
torrents: { list: { selector: rowSelector(s.torrent_row_css) }, fields },
umt: {
version: `${id}@mc-${REF}`,
kind: "nexusphp",
@@ -343,6 +343,32 @@ function convertApi(y, file) {
function isObj(v) {
return v && typeof v === "object" && !Array.isArray(v);
}
/**
* 列表行选择器规范化(W5-D,见 docs/porting/mp-rust-logic-parity.md §5):
* - `table.torrents > tr…` 在显式带 <tbody> 的页面上取不到行(cheerio 与 MP PyQuery 同),补 `> tbody > tr…` 分支;
* - 不带条件的 `table.torrents > tr` / `table.torrents tr` 会命中表头行,产出标题为空的垃圾行,收紧为"含 td.rowfollow 数据单元格的行"(NexusPHP 表头是 td.colhead;MP PyQuery 的 :has 只接受简单选择器,不能写属性选择器)。
*/
const ROW_HAS_DETAILS = ":has(td.rowfollow)";
function rowSelector(css) {
if (!css || !/^\s*table\.torrents[\s>]/.test(css)) return css;
const parts = [];
const add = (p) => {
if (!parts.includes(p)) parts.push(p);
};
for (const raw of String(css).split(",")) {
const part = raw.trim();
const m = /^table\.torrents\s*(?:>\s*)?(?:tbody\s*>\s*)?tr(.*)$/.exec(part);
if (!m) {
add(part);
continue;
}
const cond = m[1].trim() === "" ? ROW_HAS_DETAILS : m[1].trim();
add(`table.torrents > tr${cond}`);
add(`table.torrents > tbody > tr${cond}`);
}
return parts.join(", ");
}
function deepMerge(base, over, path = "") {
if (!isObj(base) || !isObj(over)) return over;
const out = { ...base };