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:
1 parent
8352e5e881
commit
36dc048e89
41 files changed
+3083
-2344
No files matched your search
@@ -220,3 +220,22 @@ RFC822、ISO、无秒、星期写错这几类两边一致;中文日期、"3 小
|
|||||||
| P3 | zh-map 补字(§7);`parseSizeMp` 改银行家舍入;RSS 补 HTML 命名实体 | `identity/zh-map.ts`、`sites/spider`、`rss/parse.ts` | 2 小时 | zhconv missing ×6、indexer our_bug ×1 |
|
| 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。
|
**不迁**: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)未做。
|
||||||
@@ -34,7 +34,7 @@ node scripts/mp-sync/movieclaw-import.mjs --check # 只比较,有差异退出
|
|||||||
## 3. 当前对照总表
|
## 3. 当前对照总表
|
||||||
|
|
||||||
<!-- mp-sync:table:begin -->
|
<!-- mp-sync:table:begin -->
|
||||||
共 49 个上游文件(`adapters/` = `packages/adapters/src/`)。
|
共 51 个上游文件(`adapters/` = `packages/adapters/src/`)。
|
||||||
|
|
||||||
| 上游文件 | 本项目文件 | 移植提交 | sha256(前 12 位) | 来源 |
|
| 上游文件 | 本项目文件 | 移植提交 | sha256(前 12 位) | 来源 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
@@ -87,6 +87,8 @@ node scripts/mp-sync/movieclaw-import.mjs --check # 只比较,有差异退出
|
|||||||
| `app/modules/qbittorrent/qbittorrent.py` | `adapters/qbittorrent/client.ts`<br>`adapters/qbittorrent/state.ts` | `e6820a8` | `ab02e97e986f` | manual |
|
| `app/modules/qbittorrent/qbittorrent.py` | `adapters/qbittorrent/client.ts`<br>`adapters/qbittorrent/state.ts` | `e6820a8` | `ab02e97e986f` | manual |
|
||||||
| `app/modules/themoviedb/tmdbapi.py` | `adapters/tmdb/client.ts`<br>`adapters/tmdb/match.ts` | `e6820a8` | `787553a6f57d` | manual |
|
| `app/modules/themoviedb/tmdbapi.py` | `adapters/tmdb/client.ts`<br>`adapters/tmdb/match.ts` | `e6820a8` | `787553a6f57d` | manual |
|
||||||
| `app/schemas/site.py` | `adapters/sites/user-parsers/types.ts` | `e6820a8` | `e7952ed40b7b` | tag |
|
| `app/schemas/site.py` | `adapters/sites/user-parsers/types.ts` | `e6820a8` | `e7952ed40b7b` | tag |
|
||||||
|
| `tests/cases/meta.py` | `packages/domain/test/differential/meta-mp.json`<br>`packages/domain/test/differential/meta-samples.json` | `e6820a8` | `3af375cf2c43` | manual |
|
||||||
|
| `tests/test_rust_accel.py` | `packages/adapters/test/differential/indexer.json`<br>`scripts/mp-sync/gen-differential.py` | `e6820a8` | `0259f245d278` | manual |
|
||||||
<!-- mp-sync:table:end -->
|
<!-- mp-sync:table:end -->
|
||||||
|
|
||||||
## 4. 移植时的统一偏离(每个文件头另有细节)
|
## 4. 移植时的统一偏离(每个文件头另有细节)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr:has(table.torrentname)"
|
"selector": "table.torrents > tr:has(table.torrentname), table.torrents > tbody > tr:has(table.torrentname)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr:has(table.torrentname)"
|
"selector": "table.torrents > tr:has(table.torrentname), table.torrents > tbody > tr:has(table.torrentname)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr, table.torrents > tbody > tr"
|
"selector": "table.torrents > tr:has(td.rowfollow), table.torrents > tbody > tr:has(td.rowfollow)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr, table.torrents > tbody > tr"
|
"selector": "table.torrents > tr:has(td.rowfollow), table.torrents > tbody > tr:has(td.rowfollow)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr, table.torrents > tbody > tr"
|
"selector": "table.torrents > tr:has(td.rowfollow), table.torrents > tbody > tr:has(td.rowfollow)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr:has(table.torrentname)"
|
"selector": "table.torrents > tr:has(table.torrentname), table.torrents > tbody > tr:has(table.torrentname)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr, table.torrents > tbody > tr"
|
"selector": "table.torrents > tr:has(td.rowfollow), table.torrents > tbody > tr:has(td.rowfollow)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents tr"
|
"selector": "table.torrents > tr:has(td.rowfollow), table.torrents > tbody > tr:has(td.rowfollow)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr:has(table.torrentname)"
|
"selector": "table.torrents > tr:has(table.torrentname), table.torrents > tbody > tr:has(table.torrentname)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr, table.torrents > tbody > tr"
|
"selector": "table.torrents > tr:has(td.rowfollow), table.torrents > tbody > tr:has(td.rowfollow)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr:has(table.torrentname)"
|
"selector": "table.torrents > tr:has(table.torrentname), table.torrents > tbody > tr:has(table.torrentname)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr, table.torrents > tbody > tr"
|
"selector": "table.torrents > tr:has(td.rowfollow), table.torrents > tbody > tr:has(td.rowfollow)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr:has(table.torrentname)"
|
"selector": "table.torrents > tr:has(table.torrentname), table.torrents > tbody > tr:has(table.torrentname)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr:has(table.torrentname)"
|
"selector": "table.torrents > tr:has(table.torrentname), table.torrents > tbody > tr:has(table.torrentname)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr, table.torrents > tbody > tr"
|
"selector": "table.torrents > tr:has(td.rowfollow), table.torrents > tbody > tr:has(td.rowfollow)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr, table.torrents > tbody > tr"
|
"selector": "table.torrents > tr:has(td.rowfollow), table.torrents > tbody > tr:has(td.rowfollow)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr:has(table.torrentname)"
|
"selector": "table.torrents > tr:has(table.torrentname), table.torrents > tbody > tr:has(table.torrentname)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
},
|
},
|
||||||
"torrents": {
|
"torrents": {
|
||||||
"list": {
|
"list": {
|
||||||
"selector": "table.torrents > tr:not(:first-child)"
|
"selector": "table.torrents > tr:not(:first-child), table.torrents > tbody > tr:not(:first-child)"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"title_default": {
|
"title_default": {
|
||||||
|
|||||||
@@ -63,6 +63,17 @@ export interface SpiderFields {
|
|||||||
[name: string]: SpiderFieldSelector | undefined;
|
[name: string]: SpiderFieldSelector | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** MP `indexer.subtitles`:字幕搜索页的独立配置(字段额外支持 language_icon/report/language/uploader/torrent_id/subtitle_id/file_name) */
|
||||||
|
export interface SpiderSubtitleConfig {
|
||||||
|
search?: IndexerDefinition["search"];
|
||||||
|
batch?: IndexerDefinition["batch"];
|
||||||
|
browse?: IndexerDefinition["browse"];
|
||||||
|
category?: SpiderCategoryConfig;
|
||||||
|
list?: SpiderListConfig;
|
||||||
|
fields?: SpiderFields;
|
||||||
|
result_num?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** 定义来源(MovieClaw v0.27.0 MIT 转换 / 手写) */
|
/** 定义来源(MovieClaw v0.27.0 MIT 转换 / 手写) */
|
||||||
export interface DefinitionProvenance {
|
export interface DefinitionProvenance {
|
||||||
source: "movieclaw" | "handwritten";
|
source: "movieclaw" | "handwritten";
|
||||||
@@ -104,6 +115,8 @@ export interface IndexerDefinition {
|
|||||||
browse?: { path: string; start?: number; list?: SpiderListConfig; fields?: SpiderFields };
|
browse?: { path: string; start?: number; list?: SpiderListConfig; fields?: SpiderFields };
|
||||||
category?: SpiderCategoryConfig;
|
category?: SpiderCategoryConfig;
|
||||||
torrents: { list: SpiderListConfig; fields: SpiderFields };
|
torrents: { list: SpiderListConfig; fields: SpiderFields };
|
||||||
|
/** 字幕页配置(MP `indexer.subtitles`,search_type=subtitles 时替换 search/batch/browse/category/list/fields) */
|
||||||
|
subtitles?: SpiderSubtitleConfig;
|
||||||
/** 本项目扩展(MP 无对应) */
|
/** 本项目扩展(MP 无对应) */
|
||||||
umt: UmtSiteExtension;
|
umt: UmtSiteExtension;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { createMockNexusPhpSite, type MockNexusPhpSite } from "@umt/testkit";
|
import {
|
||||||
|
CHDBITS_LIST_PAGE,
|
||||||
|
createMockNexusPhpSite,
|
||||||
|
type MockNexusPhpSite,
|
||||||
|
NEXUSPHP_SUBTITLES_CONFIG,
|
||||||
|
NEXUSPHP_SUBTITLES_PAGE,
|
||||||
|
} from "@umt/testkit";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
audiencesDefinition,
|
audiencesDefinition,
|
||||||
|
BUILTIN_SITE_DEFINITIONS,
|
||||||
createSiteRateLimiter,
|
createSiteRateLimiter,
|
||||||
createSpiderSiteAdapter,
|
createSpiderSiteAdapter,
|
||||||
type IndexerDefinition,
|
type IndexerDefinition,
|
||||||
|
parseSizeMp,
|
||||||
type SiteAdapter,
|
type SiteAdapter,
|
||||||
SiteError,
|
SiteError,
|
||||||
SiteSpider,
|
SiteSpider,
|
||||||
@@ -306,3 +314,82 @@ describe("观众适配器(spider + mock NexusPHP)", () => {
|
|||||||
expect(limiter.stats.granted).toBe(4);
|
expect(limiter.stats.granted).toBe(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("SiteSpider 差分补迁(W5-D)", () => {
|
||||||
|
const withTbody = (html: string) =>
|
||||||
|
html.replace(/(<table class="torrents"[^>]*>)([\s\S]*?)(<\/table>\s*<\/body>)/, "$1<tbody>$2</tbody>$3");
|
||||||
|
|
||||||
|
it("列表选择器:显式 <tbody> 的页面也能取到行,表头行不再产出空标题", () => {
|
||||||
|
const chd = BUILTIN_SITE_DEFINITIONS.chdbits as IndexerDefinition;
|
||||||
|
const plain = new SiteSpider(chd, { keyword: "x" }).parse(CHDBITS_LIST_PAGE).torrents;
|
||||||
|
const tbody = new SiteSpider(chd, { keyword: "x" }).parse(withTbody(CHDBITS_LIST_PAGE)).torrents;
|
||||||
|
expect(plain.length).toBeGreaterThan(0);
|
||||||
|
expect(tbody.map((t) => t.title)).toEqual(plain.map((t) => t.title));
|
||||||
|
const hdarea = BUILTIN_SITE_DEFINITIONS.hdarea as IndexerDefinition;
|
||||||
|
const rows = new SiteSpider(hdarea, { keyword: "x" }).parse(CHDBITS_LIST_PAGE).torrents;
|
||||||
|
expect(rows.length).toBe(plain.length);
|
||||||
|
expect(rows.every((t) => Boolean(t.title))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("行级标记 case/hr/labels 包含行自身(PyQuery descendant-or-self),取值字段只查后代", () => {
|
||||||
|
const def: IndexerDefinition = {
|
||||||
|
id: "self",
|
||||||
|
name: "self",
|
||||||
|
domain: "https://self.example/",
|
||||||
|
torrents: {
|
||||||
|
list: { selector: "table.t > tr" },
|
||||||
|
fields: {
|
||||||
|
title: { selector: "a.t" },
|
||||||
|
downloadvolumefactor: { case: { ".free_bg": 0, "*": 1 } },
|
||||||
|
hr: { selector: ".hr_row" },
|
||||||
|
labels: { selector: ".sticky", attribute: "class" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
umt: {} as IndexerDefinition["umt"],
|
||||||
|
};
|
||||||
|
const html =
|
||||||
|
'<table class="t"><tr class="free_bg hr_row sticky"><td><a class="t">A</a></td></tr><tr><td><a class="t">B</a></td></tr></table>';
|
||||||
|
const [a, b] = new SiteSpider(def, { keyword: "x" }).parse(html).torrents;
|
||||||
|
expect(a).toMatchObject({
|
||||||
|
title: "A",
|
||||||
|
downloadvolumefactor: 0,
|
||||||
|
hit_and_run: true,
|
||||||
|
labels: ["free_bg hr_row sticky"],
|
||||||
|
});
|
||||||
|
expect(b).toMatchObject({ title: "B", downloadvolumefactor: 1, hit_and_run: false, labels: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parseSizeMp 与 Python round 一致(银行家舍入)", () => {
|
||||||
|
expect(parseSizeMp("2.5 B")).toBe(2);
|
||||||
|
expect(parseSizeMp("3.5 B")).toBe(4);
|
||||||
|
expect(parseSizeMp("1.5 KB")).toBe(1536);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("字幕分支:get_subtitle_info + __fill_subtitle_ids,缺下载链接的行丢弃", () => {
|
||||||
|
const def: IndexerDefinition = {
|
||||||
|
id: "subs",
|
||||||
|
name: "subs",
|
||||||
|
domain: "https://hhanclub.net/",
|
||||||
|
torrents: { list: { selector: "none" }, fields: {} },
|
||||||
|
subtitles: NEXUSPHP_SUBTITLES_CONFIG,
|
||||||
|
umt: {} as IndexerDefinition["umt"],
|
||||||
|
};
|
||||||
|
const spider = new SiteSpider(def, { keyword: "Example Show", searchType: "subtitles" });
|
||||||
|
const { isError, torrents } = spider.parse(NEXUSPHP_SUBTITLES_PAGE);
|
||||||
|
expect(isError).toBe(false);
|
||||||
|
expect(torrents).toHaveLength(3);
|
||||||
|
expect(torrents[0]).toEqual({
|
||||||
|
title: "Example Show S01E01 1080p WEB-DL CHS",
|
||||||
|
enclosure: "https://hhanclub.net/downloadsubs.php?torrentid=0&subid=1000",
|
||||||
|
size: 114678,
|
||||||
|
pubdate: "2026-04-21 20:54:37",
|
||||||
|
date_elapsed: "1月18天",
|
||||||
|
language_icon: "https://hhanclub.net/pic/flag/china.gif",
|
||||||
|
report_url: "https://hhanclub.net/report.php?subtitle=1000",
|
||||||
|
uploader: "tester0",
|
||||||
|
torrent_id: "0",
|
||||||
|
subtitle_id: "1000",
|
||||||
|
});
|
||||||
|
expect(torrents[2]).toMatchObject({ torrent_id: "2", subtitle_id: "1002", size: 114678 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,16 +10,27 @@
|
|||||||
* → `SiteSpider.getInfo` 与同名私有方法
|
* → `SiteSpider.getInfo` 与同名私有方法
|
||||||
* - `_safe_query / __filter_text / __remove / __attribute_or_text / __index` → 同名方法
|
* - `_safe_query / __filter_text / __remove / __attribute_or_text / __index` → 同名方法
|
||||||
* - `__is_login_or_permission_page` → `isLoginOrPermissionPage`
|
* - `__is_login_or_permission_page` → `isLoginOrPermissionPage`
|
||||||
* - `parse` → `SiteSpider.parse`(不含 Rust 加速分支与字幕分支)
|
* - `parse` → `SiteSpider.parse`(不含 Rust 加速分支);`search_type=subtitles` 分支:`get_subtitle_info /
|
||||||
* - `app/foundation/size.py parse_size` → `parseSizeMp`
|
* __get_language_icon / __get_report_url / __get_subtitle_field / __fill_subtitle_ids` → `getSubtitleInfo` 等同名方法
|
||||||
|
* - `app/foundation/size.py parse_size` → `parseSizeMp`(Python round 为银行家舍入,同样实现)
|
||||||
*
|
*
|
||||||
* 与 MP 的差异(均在 README 与迁移账本登记):
|
* 与 MP 的差异(均在 README 与迁移账本登记):
|
||||||
* 1. 额外输出 `category_id`(原始分类 ID),`category` 仍是 MP 的媒体类型投影(REQ §4.5 原始分类不得丢失)。
|
* 1. 额外输出 `category_id`(原始分类 ID),`category` 仍是 MP 的媒体类型投影(REQ §4.5 原始分类不得丢失)。
|
||||||
* 2. 系数 `selector` 模式用浮点而非 int()(MP 对 0.5 抛异常导致整条丢失,迁移账本 SA4)。
|
* 2. 系数 `selector` 模式用浮点而非 int()(MP 对 0.5 抛异常导致整条丢失,迁移账本 SA4)。
|
||||||
* 3. 不发请求:HTTP/Cookie/UA 由 adapter 注入,本类只负责 URL 构造与 HTML 解析(便于限流与脱敏统一)。
|
* 3. 不发请求:HTTP/Cookie/UA 由 adapter 注入,本类只负责 URL 构造与 HTML 解析(便于限流与脱敏统一)。
|
||||||
|
* 4. 查询范围:PyQuery 在行元素上查询是 descendant-or-self(行自身也会命中),MP Rust 快路径与 cheerio `.find` 只查后代。
|
||||||
|
* 本项目取折中:促销 case / hr / labels 这三类"行级标记"(NexusPHP 把 free_bg、sticky 等 class 挂在 <tr> 上)
|
||||||
|
* 包含行自身,与 Python 一致;取值字段(safeQuery)只查后代,避免 `div:nth-child(3)` 这类选择器命中行本身取到整行文本。
|
||||||
*/
|
*/
|
||||||
import { type CheerioAPI, load } from "cheerio";
|
import { type CheerioAPI, load } from "cheerio";
|
||||||
import type { IndexerDefinition, SpiderFieldSelector, SpiderFields, SpiderFilter, SpiderListConfig } from "./schema.ts";
|
import type {
|
||||||
|
IndexerDefinition,
|
||||||
|
SpiderCategoryConfig,
|
||||||
|
SpiderFieldSelector,
|
||||||
|
SpiderFields,
|
||||||
|
SpiderFilter,
|
||||||
|
SpiderListConfig,
|
||||||
|
} from "./schema.ts";
|
||||||
import { renderTemplate } from "./template.ts";
|
import { renderTemplate } from "./template.ts";
|
||||||
|
|
||||||
type Sel = ReturnType<CheerioAPI>;
|
type Sel = ReturnType<CheerioAPI>;
|
||||||
@@ -65,8 +76,18 @@ export interface SpiderTorrent {
|
|||||||
hit_and_run?: boolean;
|
hit_and_run?: boolean;
|
||||||
category?: MpMediaType;
|
category?: MpMediaType;
|
||||||
category_id?: string;
|
category_id?: string;
|
||||||
|
// ---- 字幕分支(search_type=subtitles)专有字段,键名与 MP 一致 ----
|
||||||
|
language_icon?: string;
|
||||||
|
report_url?: string;
|
||||||
|
language?: string;
|
||||||
|
uploader?: string;
|
||||||
|
torrent_id?: string;
|
||||||
|
subtitle_id?: string;
|
||||||
|
file_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SpiderSearchType = "torrents" | "subtitles";
|
||||||
|
|
||||||
/** MP `select_media_categories` */
|
/** MP `select_media_categories` */
|
||||||
export function selectMediaCategories(category: IndexerDefinition["category"], mtype?: MediaKind) {
|
export function selectMediaCategories(category: IndexerDefinition["category"], mtype?: MediaKind) {
|
||||||
if (!category) return [];
|
if (!category) return [];
|
||||||
@@ -86,7 +107,15 @@ export function resolveCategoryMediaType(value: unknown, category: IndexerDefini
|
|||||||
return matches.length === 1 ? MEDIA_VALUE[matches[0]!] : "未知";
|
return matches.length === 1 ? MEDIA_VALUE[matches[0]!] : "未知";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** MP `size_tools.parse_size`:二进制单位,round。 */
|
/** Python 3 `round()`:恰好 .5 时取偶数(银行家舍入) */
|
||||||
|
function roundHalfEven(v: number): number {
|
||||||
|
const f = Math.floor(v);
|
||||||
|
const diff = v - f;
|
||||||
|
if (diff === 0.5) return f % 2 === 0 ? f : f + 1;
|
||||||
|
return Math.round(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MP `size_tools.parse_size`:二进制单位,round(银行家舍入)。 */
|
||||||
export function parseSizeMp(text: string | number | null | undefined): number {
|
export function parseSizeMp(text: string | number | null | undefined): number {
|
||||||
if (!text) return 0;
|
if (!text) return 0;
|
||||||
const s = String(text);
|
const s = String(text);
|
||||||
@@ -101,7 +130,7 @@ export function parseSizeMp(text: string | number | null | undefined): number {
|
|||||||
else if (normalized.includes("GB") || normalized.includes("GIB")) v *= 1024 ** 3;
|
else if (normalized.includes("GB") || normalized.includes("GIB")) v *= 1024 ** 3;
|
||||||
else if (normalized.includes("MB") || normalized.includes("MIB")) v *= 1024 ** 2;
|
else if (normalized.includes("MB") || normalized.includes("MIB")) v *= 1024 ** 2;
|
||||||
else if (normalized.includes("KB") || normalized.includes("KIB")) v *= 1024;
|
else if (normalized.includes("KB") || normalized.includes("KIB")) v *= 1024;
|
||||||
return Math.round(v);
|
return roundHalfEven(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** strptime 结果(MP dateparse 过滤器返回 datetime) */
|
/** strptime 结果(MP dateparse 过滤器返回 datetime) */
|
||||||
@@ -193,6 +222,8 @@ export interface SiteSpiderQuery {
|
|||||||
cat?: string;
|
cat?: string;
|
||||||
/** 站点内部页码(MP 0 起) */
|
/** 站点内部页码(MP 0 起) */
|
||||||
page?: number;
|
page?: number;
|
||||||
|
/** MP `search_type`:subtitles 时改用 `def.subtitles` 的配置 */
|
||||||
|
searchType?: SpiderSearchType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SiteSpider {
|
export class SiteSpider {
|
||||||
@@ -204,8 +235,13 @@ export class SiteSpider {
|
|||||||
readonly domain: string;
|
readonly domain: string;
|
||||||
readonly resultNum: number;
|
readonly resultNum: number;
|
||||||
readonly siteMediaType: MediaKind | undefined;
|
readonly siteMediaType: MediaKind | undefined;
|
||||||
|
readonly searchType: SpiderSearchType;
|
||||||
private list: SpiderListConfig;
|
private list: SpiderListConfig;
|
||||||
private fields: SpiderFields;
|
private fields: SpiderFields;
|
||||||
|
private searchCfg: IndexerDefinition["search"];
|
||||||
|
private batchCfg: IndexerDefinition["batch"];
|
||||||
|
private browseCfg: IndexerDefinition["browse"];
|
||||||
|
private categoryCfg: SpiderCategoryConfig | undefined;
|
||||||
private requestedResultMediaType: MediaKind | undefined;
|
private requestedResultMediaType: MediaKind | undefined;
|
||||||
private $!: CheerioAPI;
|
private $!: CheerioAPI;
|
||||||
private info: SpiderTorrent = {};
|
private info: SpiderTorrent = {};
|
||||||
@@ -217,26 +253,42 @@ export class SiteSpider {
|
|||||||
this.cat = q.cat;
|
this.cat = q.cat;
|
||||||
this.page = q.page ?? 0;
|
this.page = q.page ?? 0;
|
||||||
this.siteMediaType = fromAgent(def.media_type);
|
this.siteMediaType = fromAgent(def.media_type);
|
||||||
|
this.searchType = q.searchType ?? "torrents";
|
||||||
|
if (this.searchType === "subtitles") {
|
||||||
|
const sc = def.subtitles ?? {};
|
||||||
|
this.searchCfg = sc.search;
|
||||||
|
this.batchCfg = sc.batch;
|
||||||
|
this.browseCfg = sc.browse;
|
||||||
|
this.categoryCfg = sc.category;
|
||||||
|
this.list = sc.list ?? { selector: "" };
|
||||||
|
this.fields = sc.fields ?? {};
|
||||||
|
this.resultNum = Number(sc.result_num ?? def.result_num ?? 100);
|
||||||
|
} else {
|
||||||
|
this.searchCfg = def.search;
|
||||||
|
this.batchCfg = def.batch;
|
||||||
|
this.browseCfg = def.browse;
|
||||||
|
this.categoryCfg = def.category;
|
||||||
this.list = def.torrents.list;
|
this.list = def.torrents.list;
|
||||||
this.fields = def.torrents.fields ?? {};
|
this.fields = def.torrents.fields ?? {};
|
||||||
if (!q.keyword && def.browse) {
|
if (!q.keyword && def.browse) {
|
||||||
this.list = def.browse.list ?? this.list;
|
this.list = def.browse.list ?? this.list;
|
||||||
this.fields = def.browse.fields ?? this.fields;
|
this.fields = def.browse.fields ?? this.fields;
|
||||||
}
|
}
|
||||||
this.domain = def.domain.endsWith("/") ? def.domain : `${def.domain}/`;
|
|
||||||
this.resultNum = Number(def.result_num ?? 100);
|
this.resultNum = Number(def.result_num ?? 100);
|
||||||
}
|
}
|
||||||
|
this.domain = def.domain.endsWith("/") ? def.domain : `${def.domain}/`;
|
||||||
|
}
|
||||||
|
|
||||||
/** MP `__can_search` */
|
/** MP `__can_search` */
|
||||||
canSearch(): boolean {
|
canSearch(): boolean {
|
||||||
if (this.siteMediaType && this.mtype && this.siteMediaType !== this.mtype) return false;
|
if (this.siteMediaType && this.mtype && this.siteMediaType !== this.mtype) return false;
|
||||||
return Boolean(this.def.search && this.domain);
|
return Boolean(this.searchCfg && this.domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** MP `__format_search_word` */
|
/** MP `__format_search_word` */
|
||||||
private formatSearchWord(word: string): string {
|
private formatSearchWord(word: string): string {
|
||||||
if (word && /^tt\d+$/.test(word) && this.def.search?.imdbid_format) {
|
if (word && /^tt\d+$/.test(word) && this.searchCfg?.imdbid_format) {
|
||||||
return this.def.search.imdbid_format
|
return this.searchCfg.imdbid_format
|
||||||
.replace(/\{keyword\}/g, word)
|
.replace(/\{keyword\}/g, word)
|
||||||
.replace(/\{imdbid\}/g, word)
|
.replace(/\{imdbid\}/g, word)
|
||||||
.replace(/\{imdbid_num\}/g, word.slice(2));
|
.replace(/\{imdbid_num\}/g, word.slice(2));
|
||||||
@@ -246,7 +298,7 @@ export class SiteSpider {
|
|||||||
|
|
||||||
/** MP `__get_search_url` */
|
/** MP `__get_search_url` */
|
||||||
getSearchUrl(): string {
|
getSearchUrl(): string {
|
||||||
const search = this.def.search ?? { paths: [] };
|
const search = this.searchCfg ?? { paths: [] };
|
||||||
const paths = search.paths ?? [];
|
const paths = search.paths ?? [];
|
||||||
let torrentspath = "";
|
let torrentspath = "";
|
||||||
let typedPathSelected = false;
|
let typedPathSelected = false;
|
||||||
@@ -272,9 +324,9 @@ export class SiteSpider {
|
|||||||
let searchWord: string;
|
let searchWord: string;
|
||||||
let searchMode: string;
|
let searchMode: string;
|
||||||
if (Array.isArray(this.keyword)) {
|
if (Array.isArray(this.keyword)) {
|
||||||
if (this.def.batch) {
|
if (this.batchCfg) {
|
||||||
const delimiter = this.def.batch.delimiter || " ";
|
const delimiter = this.batchCfg.delimiter || " ";
|
||||||
const spaceReplace = this.def.batch.space_replace || " ";
|
const spaceReplace = this.batchCfg.space_replace || " ";
|
||||||
searchWord = this.keyword.map((k) => String(k).replace(/ /g, spaceReplace)).join(delimiter);
|
searchWord = this.keyword.map((k) => String(k).replace(/ /g, spaceReplace)).join(delimiter);
|
||||||
} else {
|
} else {
|
||||||
searchWord = this.keyword.join(" ");
|
searchWord = this.keyword.join(" ");
|
||||||
@@ -296,7 +348,7 @@ export class SiteSpider {
|
|||||||
notnewword: 1,
|
notnewword: 1,
|
||||||
};
|
};
|
||||||
for (const [k, v] of Object.entries(indexerParams)) params[k] = String(v).replace(/\{keyword\}/g, searchWord);
|
for (const [k, v] of Object.entries(indexerParams)) params[k] = String(v).replace(/\{keyword\}/g, searchWord);
|
||||||
const category = this.def.category;
|
const category = this.categoryCfg;
|
||||||
if (category) {
|
if (category) {
|
||||||
const cats = selectMediaCategories(category, this.mtype);
|
const cats = selectMediaCategories(category, this.mtype);
|
||||||
const allowed = this.cat ? new Set(this.cat.split(",")) : null;
|
const allowed = this.cat ? new Set(this.cat.split(",")) : null;
|
||||||
@@ -333,9 +385,9 @@ export class SiteSpider {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let page = this.page || 0;
|
let page = this.page || 0;
|
||||||
if (this.def.browse && !typedPathSelected) {
|
if (this.browseCfg && !typedPathSelected) {
|
||||||
torrentspath = this.def.browse.path;
|
torrentspath = this.browseCfg.path;
|
||||||
if (this.def.browse.start) page = Number(this.def.browse.start) + Number(this.page || 0);
|
if (this.browseCfg.start) page = Number(this.browseCfg.start) + Number(this.page || 0);
|
||||||
} else if (this.page && !torrentspath.includes("{page}")) {
|
} else if (this.page && !torrentspath.includes("{page}")) {
|
||||||
torrentspath += `${torrentspath.includes("?") ? "&" : "?"}page=${this.page}`;
|
torrentspath += `${torrentspath.includes("?") ? "&" : "?"}page=${this.page}`;
|
||||||
}
|
}
|
||||||
@@ -369,7 +421,7 @@ export class SiteSpider {
|
|||||||
const rows = $(normalizeSelector(this.list.selector ?? ""));
|
const rows = $(normalizeSelector(this.list.selector ?? ""));
|
||||||
rows.each((i, el) => {
|
rows.each((i, el) => {
|
||||||
if (i >= this.resultNum) return false;
|
if (i >= this.resultNum) return false;
|
||||||
const info = this.getInfo($(el));
|
const info = this.searchType === "subtitles" ? this.getSubtitleInfo($(el)) : this.getInfo($(el));
|
||||||
if (Object.keys(info).length > 0) out.push(this.applyRequestedMediaType(info));
|
if (Object.keys(info).length > 0) out.push(this.applyRequestedMediaType(info));
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
@@ -414,6 +466,70 @@ export class SiteSpider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** MP `get_subtitle_info`:缺标题或下载链接的行丢弃;任一字段异常整条返回 {}。 */
|
||||||
|
getSubtitleInfo(row: Sel): SpiderTorrent {
|
||||||
|
this.info = {};
|
||||||
|
try {
|
||||||
|
this.getTitle(row);
|
||||||
|
this.getDescription(row);
|
||||||
|
this.getDetail(row);
|
||||||
|
this.getDownload(row);
|
||||||
|
this.getSize(row);
|
||||||
|
this.getPubdate(row);
|
||||||
|
this.getDateElapsed(row);
|
||||||
|
this.getGrabs(row);
|
||||||
|
this.getLinkField(row, "language_icon", "language_icon");
|
||||||
|
this.getLinkField(row, "report", "report_url");
|
||||||
|
for (const name of ["language", "uploader", "torrent_id", "subtitle_id", "file_name"] as const) {
|
||||||
|
this.getSubtitleField(row, name);
|
||||||
|
}
|
||||||
|
this.fillSubtitleIds();
|
||||||
|
if (!this.info.title || !this.info.enclosure) return {};
|
||||||
|
return { ...this.info };
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MP `__get_language_icon` / `__get_report_url`:相对链接补全为绝对链接 */
|
||||||
|
private getLinkField(row: Sel, name: string, key: "language_icon" | "report_url") {
|
||||||
|
if (!(name in this.fields)) return;
|
||||||
|
const selector = this.fields[name] ?? {};
|
||||||
|
const link = this.filterText(this.safeQuery(row, selector), selector.filters);
|
||||||
|
if (link) this.info[key] = this.normalizeLink(String(link));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MP `__get_subtitle_field` */
|
||||||
|
private getSubtitleField(row: Sel, name: "language" | "uploader" | "torrent_id" | "subtitle_id" | "file_name") {
|
||||||
|
const selector = this.fields[name];
|
||||||
|
if (!selector || Object.keys(selector).length === 0) return;
|
||||||
|
const v = this.filterText(this.safeQuery(row, selector), selector.filters);
|
||||||
|
if (v != null) this.info[name] = v instanceof ParsedDateTime ? v.text : String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MP `__fill_subtitle_ids`:从下载链接 query 补种子 ID(torrentid/torrent_id)与字幕 ID(subid/subtitle) */
|
||||||
|
private fillSubtitleIds() {
|
||||||
|
const enclosure = this.info.enclosure;
|
||||||
|
if (!enclosure) return;
|
||||||
|
const q = enclosure.includes("?") ? enclosure.slice(enclosure.indexOf("?") + 1).split("#")[0]! : "";
|
||||||
|
const params = new URLSearchParams(q);
|
||||||
|
const first = (...keys: string[]) => {
|
||||||
|
for (const k of keys) {
|
||||||
|
const v = params.getAll(k).find((x) => x !== "");
|
||||||
|
if (v !== undefined) return v;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
if (!this.info.torrent_id) {
|
||||||
|
const v = first("torrentid", "torrent_id");
|
||||||
|
if (v !== undefined) this.info.torrent_id = v;
|
||||||
|
}
|
||||||
|
if (!this.info.subtitle_id) {
|
||||||
|
const v = first("subid", "subtitle");
|
||||||
|
if (v !== undefined) this.info.subtitle_id = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 字段 ----
|
// ---- 字段 ----
|
||||||
|
|
||||||
private getTitle(row: Sel) {
|
private getTitle(row: Sel) {
|
||||||
@@ -532,7 +648,7 @@ export class SiteSpider {
|
|||||||
this.info[key] = 1;
|
this.info[key] = 1;
|
||||||
if (selector.case) {
|
if (selector.case) {
|
||||||
for (const [css, value] of Object.entries(selector.case)) {
|
for (const [css, value] of Object.entries(selector.case)) {
|
||||||
if (row.find(normalizeSelector(css)).length > 0) {
|
if (this.rowScope(row, css).length > 0) {
|
||||||
this.info[key] = value;
|
this.info[key] = value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -606,7 +722,7 @@ export class SiteSpider {
|
|||||||
this.info.labels = [];
|
this.info.labels = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const q = row.find(normalizeSelector(selector.selector)).clone();
|
const q = this.rowScope(row, selector.selector).clone();
|
||||||
this.remove(q, selector);
|
this.remove(q, selector);
|
||||||
this.info.labels = this.attributeOrText(q, selector).filter((x): x is string => Boolean(x));
|
this.info.labels = this.attributeOrText(q, selector).filter((x): x is string => Boolean(x));
|
||||||
}
|
}
|
||||||
@@ -614,7 +730,7 @@ export class SiteSpider {
|
|||||||
private getHitAndRun(row: Sel) {
|
private getHitAndRun(row: Sel) {
|
||||||
if (!("hr" in this.fields)) return;
|
if (!("hr" in this.fields)) return;
|
||||||
const selector = this.fields.hr ?? {};
|
const selector = this.fields.hr ?? {};
|
||||||
this.info.hit_and_run = selector.selector ? row.find(normalizeSelector(selector.selector)).length > 0 : false;
|
this.info.hit_and_run = selector.selector ? this.rowScope(row, selector.selector).length > 0 : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCategory(row: Sel) {
|
private getCategory(row: Sel) {
|
||||||
@@ -629,13 +745,21 @@ export class SiteSpider {
|
|||||||
const selector = this.fields.category ?? {};
|
const selector = this.fields.category ?? {};
|
||||||
const value = this.filterText(this.safeQuery(row, selector), selector.filters);
|
const value = this.filterText(this.safeQuery(row, selector), selector.filters);
|
||||||
if (value != null && value !== "") this.info.category_id = String(value);
|
if (value != null && value !== "") this.info.category_id = String(value);
|
||||||
let resolved = resolveCategoryMediaType(value, this.def.category);
|
let resolved = resolveCategoryMediaType(value, this.categoryCfg);
|
||||||
if (resolved === "未知" && this.siteMediaType) resolved = MEDIA_VALUE[this.siteMediaType];
|
if (resolved === "未知" && this.siteMediaType) resolved = MEDIA_VALUE[this.siteMediaType];
|
||||||
this.info.category = resolved;
|
this.info.category = resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 通用查询 ----
|
// ---- 通用查询 ----
|
||||||
|
|
||||||
|
/** 行级标记查询:行自身 + 后代(PyQuery descendant-or-self,见文件头差异 4);`:scope` 选择器只查后代 */
|
||||||
|
private rowScope(row: Sel, selector: string): Sel {
|
||||||
|
const css = normalizeSelector(selector);
|
||||||
|
const desc = row.find(css);
|
||||||
|
if (css.includes(":scope")) return desc;
|
||||||
|
return row.filter(css).add(desc);
|
||||||
|
}
|
||||||
|
|
||||||
/** MP `_safe_query` */
|
/** MP `_safe_query` */
|
||||||
safeQuery(row: Sel, config: SpiderFieldSelector | undefined): string | null | undefined {
|
safeQuery(row: Sel, config: SpiderFieldSelector | undefined): string | null | undefined {
|
||||||
if (!config?.selector) return null;
|
if (!config?.selector) return null;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 站点索引解析差分:MP `SiteSpider.parse`(PyQuery 回退)vs 本项目 `SiteSpider.parse`。
|
* 站点索引解析差分:MP `SiteSpider.parse`(PyQuery 回退)vs 本项目 `SiteSpider.parse`。
|
||||||
* 同一 indexer 配置(本项目 definitions/*.json 或内联配置)+ 同一 HTML;期望由 scripts/mp-sync/gen-differential.py rss|indexer 生成(indexer.json)。
|
* 同一 indexer 配置(本项目 definitions/*.json 或内联配置)+ 同一 HTML;期望由 scripts/mp-sync/gen-differential.py rss|indexer 生成(indexer.json)。
|
||||||
|
* search_type=subtitles 的样本走字幕分支(MP get_subtitle_info)。
|
||||||
*/
|
*/
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { BUILTIN_SITE_DEFINITIONS, type IndexerDefinition, SiteSpider } from "../../src/sites/index.ts";
|
import { BUILTIN_SITE_DEFINITIONS, type IndexerDefinition, SiteSpider } from "../../src/sites/index.ts";
|
||||||
@@ -67,17 +68,12 @@ describe("站点索引差分(MP SiteSpider Python 回退)", () => {
|
|||||||
if (s.mp.status === "error") {
|
if (s.mp.status === "error") {
|
||||||
throw new Error(`${s.id}: MP 运行失败 ${s.mp.error}`);
|
throw new Error(`${s.id}: MP 运行失败 ${s.mp.error}`);
|
||||||
}
|
}
|
||||||
if (s.query.search_type === "subtitles") {
|
const subtitles = s.query.search_type === "subtitles";
|
||||||
// 本项目 SiteSpider 未移植字幕分支(parse_indexer_subtitles)
|
|
||||||
c.mismatches.push({
|
|
||||||
key: `${s.id}.status`,
|
|
||||||
mp: `ok:${s.mp.torrents?.length ?? 0}`,
|
|
||||||
ours: "unsupported",
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const html = s.input.file ? readInput(s.input.file) : (s.input.inline ?? "");
|
const html = s.input.file ? readInput(s.input.file) : (s.input.inline ?? "");
|
||||||
const spider = new SiteSpider(definitionOf(s), s.query.keyword ? { keyword: s.query.keyword } : {});
|
const spider = new SiteSpider(definitionOf(s), {
|
||||||
|
...(s.query.keyword ? { keyword: s.query.keyword } : {}),
|
||||||
|
...(subtitles ? { searchType: "subtitles" as const } : {}),
|
||||||
|
});
|
||||||
const ours = spider.parse(html);
|
const ours = spider.parse(html);
|
||||||
const mpRows = s.mp.torrents ?? [];
|
const mpRows = s.mp.torrents ?? [];
|
||||||
if (Boolean(s.mp.is_error) !== ours.isError) {
|
if (Boolean(s.mp.is_error) !== ours.isError) {
|
||||||
@@ -95,7 +91,9 @@ describe("站点索引差分(MP SiteSpider Python 回退)", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const [i, m, o] of alignRows(mpRows, ours.torrents as Array<Record<string, unknown>>)) {
|
for (const [i, m, o] of alignRows(mpRows, ours.torrents as Array<Record<string, unknown>>)) {
|
||||||
for (const f of doc.fields) c.check(`${s.id}#${i}.${f}`, m[f], o[f]);
|
// 字幕分支输出字段与种子不同(language_icon/report_url/uploader/torrent_id/subtitle_id…),按双方键并集比较
|
||||||
|
const fields = subtitles ? [...new Set([...Object.keys(m), ...Object.keys(o)])].sort() : doc.fields;
|
||||||
|
for (const f of fields) c.check(`${s.id}#${i}.${f}`, m[f], o[f]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* 繁 → 简 简单映射表(只覆盖影视标题常见字,不引第三方库)。
|
* 繁 → 简 简单映射表(只覆盖影视标题常见字,不引第三方库)。
|
||||||
* 格式:每两个字符为一对「繁简」。一对多/语境相关的字(乾/著/瞭 等)刻意不收。
|
* 格式:每两个字符为一对「繁简」。
|
||||||
|
*
|
||||||
|
* 一对多/语境相关的字刻意不收(单字表没有词组上下文,收了会把正确用法转错):
|
||||||
|
* - 乾:乾杯→干杯,但 乾隆/乾坤 不转;
|
||||||
|
* - 瞭:瞭解→了解,但 瞭望 读 liào 不转;
|
||||||
|
* - 髮:頭髮/理髮→发,与 發→发 合并后反查不唯一,召回比较时也不需要(TMDB 中文标题极少出现);
|
||||||
|
* - 著:著作 不转,顯著/著名 的 著 与 着 的分工依语境。
|
||||||
|
* MP 的 zhconv_fast 用 MediaWiki 词组表处理这些字,差分里登记为 missing(见 test/differential/zhconv-deviations.json)。
|
||||||
*/
|
*/
|
||||||
const PAIRS =
|
const PAIRS =
|
||||||
"萬万與与專专業业叢丛東东絲丝兩两嚴严喪丧個个豐丰臨临為为麗丽舉举麼么義义烏乌樂乐喬乔習习鄉乡書书買买亂乱爭争於于虧亏雲云亞亚產产親亲億亿僅仅從从倉仓儀仪們们價价眾众優优會会傘伞偉伟傳传傷伤倫伦偽伪體体餘余傭佣俠侠侶侣偵侦側侧僑侨儂侬倆俩儉俭債债傾倾償偿儲储兒儿兇凶黨党蘭兰關关興兴養养獸兽內内岡冈冊册寫写軍军農农馮冯衝冲決决況况凍冻淨净準准涼凉減减湊凑幾几鳳凤憑凭凱凯擊击劃划劉刘則则剛刚創创刪删別别劑剂劍剑剝剥劇剧勸劝辦办務务動动勵励勁劲勞劳勢势勳勋勝胜區区醫医華华協协單单賣卖盧卢衛卫卻却廠厂廳厅曆历厲厉壓压厭厌廁厕廂厢廈厦廚厨縣县參参雙双發发變变敘叙疊叠葉叶號号嘆叹嚇吓呂吕嗎吗啟启吳吴員员響响啞哑嘩哗喚唤噴喷團团園园圍围國国圖图圓圆聖圣場场壞坏塊块堅坚壇坛壩坝墳坟墜坠壘垒墊垫牆墙壯壮聲声殼壳壺壶處处備备復复夠够頭头誇夸夾夹奪夺奮奋獎奖奧奥妝妆婦妇媽妈嬌娇娛娱嬰婴嬸婶孫孙學学寧宁寶宝實实寵宠審审憲宪宮宫寬宽賓宾寢寝對对尋寻導导壽寿將将爾尔塵尘嘗尝堯尧屍尸盡尽層层屆届屬属嶼屿歲岁豈岂崗岗島岛嶺岭峽峡嶄崭巔巅幣币帥帅師师帳帐簾帘幟帜帶带幫帮並并廣广莊庄慶庆廬庐庫库應应廟庙龐庞廢废開开異异棄弃張张彌弥彎弯彈弹強强歸归當当錄录彥彦徹彻徑径憶忆憂忧懷怀態态憐怜總总戀恋懇恳惡恶惱恼悅悦懸悬驚惊懼惧慘惨懲惩慚惭慣惯憤愤願愿懶懒戲戏戰战戶户紮扎撲扑執执擴扩掃扫揚扬擾扰撫抚拋抛搶抢護护報报擔担擬拟擁拥攔拦擇择掛挂揮挥撈捞損损撿捡換换據据擲掷攬揽攜携攝摄擺摆搖摇攤摊撐撑敵敌斂敛數数齋斋鬥斗斬斩斷断無无舊旧時时曠旷晝昼顯显晉晋曬晒曉晓暈晕暉晖暫暂術术樸朴機机殺杀雜杂權权條条來来楊杨傑杰極极構构槍枪楓枫櫃柜標标棧栈棟栋欄栏樹树棲栖樣样檔档橋桥夢梦檢检樓楼欖榄櫻樱櫥橱歡欢歐欧殲歼殘残毀毁畢毕斃毙氣气匯汇漢汉湯汤溝沟沒没淚泪瀉泻潑泼澤泽潔洁灑洒淺浅漿浆濁浊測测濟济渾浑濃浓濤涛漲涨淵渊漸渐漁渔滲渗溫温遊游灣湾濕湿潰溃濺溅滿满濾滤濫滥濱滨灘滩瀟潇潛潜瀾澜瀕濒滅灭燈灯靈灵災灾燦灿爐炉點点煉炼熾炽爍烁爛烂燭烛煙烟煩烦燒烧熱热煥焕愛爱爺爷牽牵犧牺狀状猶犹狹狭獅狮獄狱獨独獵猎豬猪貓猫獻献瑪玛環环現现瓊琼電电畫画暢畅療疗瘋疯癢痒發发皺皱盞盏鹽盐監监蓋盖盜盗盤盘睜睁瞞瞒矯矫礦矿碼码磚砖確确礙碍禮礼禍祸禪禅離离禿秃種种積积稱称穩稳穫获窮穷竊窃窯窑竄窜窩窝豎竖競竞筆笔築筑篩筛籌筹簽签簡简籃篮籬篱類类糧粮糾纠紀纪約约紅红紋纹納纳紐纽純纯紗纱紡纺紛纷級级紙纸細细紳绅終终組组結结繞绕給给絢绚絡络絕绝統统綁绑絹绢繡绣經经綜综綠绿綴缀網网綱纲緒绪續续緊紧維维綿绵緣缘編编緩缓緯纬練练縫缝縮缩織织繩绳繪绘繳缴纖纤羅罗罰罚罷罢羈羁翹翘聳耸恥耻聶聂聾聋職职聯联聰聪肅肃腸肠膚肤腫肿脅胁膽胆朧胧膠胶脈脉腦脑腳脚脫脱臉脸臘腊騰腾輿舆艦舰艙舱艱艰豔艳藝艺節节蕪芜蘆芦葦苇蒼苍蘇苏蘋苹莖茎薦荐莢荚蕩荡榮荣藥药萊莱蓮莲獲获瑩莹鶯莺蘿萝螢萤營营蕭萧薩萨蔥葱蔣蒋藍蓝薔蔷藹蔼蘊蕴虜虏慮虑虛虚蟲虫雖虽蝦虾蝕蚀蟻蚁螞蚂蠶蚕蠻蛮蝸蜗蠟蜡蠅蝇蟬蝉補补襯衬襖袄襪袜襲袭裝装褲裤見见觀观規规覓觅視视覽览覺觉觸触計计訂订認认討讨讓让訓训議议訊讯記记講讲許许論论設设訪访訣诀證证評评識识詐诈訴诉診诊詞词譯译試试詩诗誠诚話话誕诞詭诡詢询該该詳详誤误語语誘诱說说誦诵請请諸诸諾诺讀读課课誰谁調调諒谅談谈誼谊謀谋謊谎諧谐謂谓謎谜謝谢謠谣謙谦謹谨謬谬譜谱貝贝貞贞負负貢贡財财責责賢贤敗败賬账貨货質质販贩貪贪貧贫購购貫贯貼贴貴贵貸贷貿贸費费賀贺賊贼資资賦赋賭赌贖赎賞赏賜赐賠赔賴赖賺赚賽赛贈赠贊赞贏赢趙赵趕赶趨趋躍跃踐践蹤踪躲躲車车軌轨軍军軒轩轉转輪轮軟软轟轰軸轴輕轻載载較较輔辅輛辆輩辈輝辉輸输轄辖辭辞辯辩邊边遼辽達达遷迁過过邁迈運运還还這这進进遠远違违連连遲迟適适選选遜逊遞递邏逻遺遗遙遥鄧邓郵邮鄰邻鬱郁鄭郑醞酝醬酱釀酿釋释裏里鑒鉴針针釘钉鐘钟鋼钢鑰钥鈴铃鐵铁鉛铅銅铜鋁铝銀银鏈链銷销鎖锁鋒锋鋤锄鍋锅錯错錨锚錘锤錦锦鍵键鋸锯鍛锻鏡镜鐮镰鑲镶長长門门閃闪閉闭問问闖闯閑闲間间悶闷閘闸鬧闹閨闺聞闻閣Line truncated
|
"萬万與与專专業业叢丛東东絲丝兩两嚴严喪丧個个豐丰臨临為为麗丽舉举麼么義义烏乌樂乐喬乔習习鄉乡書书買买亂乱爭争於于虧亏雲云亞亚產产親亲億亿僅仅從从倉仓儀仪們们價价眾众優优會会傘伞偉伟傳传傷伤倫伦偽伪體体餘余傭佣俠侠侶侣偵侦側侧僑侨儂侬倆俩儉俭債债傾倾償偿儲储兒儿兇凶黨党蘭兰關关興兴養养獸兽內内岡冈冊册寫写軍军農农馮冯衝冲決决況况凍冻淨净準准涼凉減减湊凑幾几鳳凤憑凭凱凯擊击劃划劉刘則则剛刚創创刪删別别劑剂劍剑剝剥劇剧勸劝辦办務务動动勵励勁劲勞劳勢势勳勋勝胜區区醫医華华協协單单賣卖盧卢衛卫卻却廠厂廳厅曆历厲厉壓压厭厌廁厕廂厢廈厦廚厨縣县參参雙双發发變变敘叙疊叠葉叶號号嘆叹嚇吓呂吕嗎吗啟启吳吴員员響响啞哑嘩哗喚唤噴喷團团園园圍围國国圖图圓圆聖圣場场壞坏塊块堅坚壇坛壩坝墳坟墜坠壘垒墊垫牆墙壯壮聲声殼壳壺壶處处備备復复夠够頭头誇夸夾夹奪夺奮奋獎奖奧奥妝妆婦妇媽妈嬌娇娛娱嬰婴嬸婶孫孙學学寧宁寶宝實实寵宠審审憲宪宮宫寬宽賓宾寢寝對对尋寻導导壽寿將将爾尔塵尘嘗尝堯尧屍尸盡尽層层屆届屬属嶼屿歲岁豈岂崗岗島岛嶺岭峽峡嶄崭巔巅幣币帥帅師师帳帐簾帘幟帜帶带幫帮並并廣广莊庄慶庆廬庐庫库應应廟庙龐庞廢废開开異异棄弃張张彌弥彎弯彈弹強强歸归當当錄录彥彦徹彻徑径憶忆憂忧懷怀態态憐怜總总戀恋懇恳惡恶惱恼悅悦懸悬驚惊懼惧慘惨懲惩慚惭慣惯憤愤願愿懶懒戲戏戰战戶户紮扎撲扑執执擴扩掃扫揚扬擾扰撫抚拋抛搶抢護护報报擔担擬拟擁拥攔拦擇择掛挂揮挥撈捞損损撿捡換换據据擲掷攬揽攜携攝摄擺摆搖摇攤摊撐撑敵敌斂敛數数齋斋鬥斗斬斩斷断無无舊旧時时曠旷晝昼顯显晉晋曬晒曉晓暈晕暉晖暫暂術术樸朴機机殺杀雜杂權权條条來来楊杨傑杰極极構构槍枪楓枫櫃柜標标棧栈棟栋欄栏樹树棲栖樣样檔档橋桥夢梦檢检樓楼欖榄櫻樱櫥橱歡欢歐欧殲歼殘残毀毁畢毕斃毙氣气匯汇漢汉湯汤溝沟沒没淚泪瀉泻潑泼澤泽潔洁灑洒淺浅漿浆濁浊測测濟济渾浑濃浓濤涛漲涨淵渊漸渐漁渔滲渗溫温遊游灣湾濕湿潰溃濺溅滿满濾滤濫滥濱滨灘滩瀟潇潛潜瀾澜瀕濒滅灭燈灯靈灵災灾燦灿爐炉點点煉炼熾炽爍烁爛烂燭烛煙烟煩烦燒烧熱热煥焕愛爱爺爷牽牵犧牺狀状猶犹狹狭獅狮獄狱獨独獵猎豬猪貓猫獻献瑪玛環环現现瓊琼電电畫画暢畅療疗瘋疯癢痒發发皺皱盞盏鹽盐監监蓋盖盜盗盤盘睜睁瞞瞒矯矫礦矿碼码磚砖確确礙碍禮礼禍祸禪禅離离禿秃種种積积稱称穩稳穫获窮穷竊窃窯窑竄窜窩窝豎竖競竞筆笔築筑篩筛籌筹簽签簡简籃篮籬篱類类糧粮糾纠紀纪約约紅红紋纹納纳紐纽純纯紗纱紡纺紛纷級级紙纸細细紳绅終终組组結结繞绕給给絢绚絡络絕绝統统綁绑絹绢繡绣經经綜综綠绿綴缀網网綱纲緒绪續续緊紧維维綿绵緣缘編编緩缓緯纬練练縫缝縮缩織织繩绳繪绘繳缴纖纤羅罗罰罚罷罢羈羁翹翘聳耸恥耻聶聂聾聋職职聯联聰聪肅肃腸肠膚肤腫肿脅胁膽胆朧胧膠胶脈脉腦脑腳脚脫脱臉脸臘腊騰腾輿舆艦舰艙舱艱艰豔艳藝艺節节蕪芜蘆芦葦苇蒼苍蘇苏蘋苹莖茎薦荐莢荚蕩荡榮荣藥药萊莱蓮莲獲获瑩莹鶯莺蘿萝螢萤營营蕭萧薩萨蔥葱蔣蒋藍蓝薔蔷藹蔼蘊蕴虜虏慮虑虛虚蟲虫雖虽蝦虾蝕蚀蟻蚁螞蚂蠶蚕蠻蛮蝸蜗蠟蜡蠅蝇蟬蝉補补襯衬襖袄襪袜襲袭裝装褲裤見见觀观規规覓觅視视覽览覺觉觸触計计訂订認认討讨讓让訓训議议訊讯記记講讲許许論论設设訪访訣诀證证評评識识詐诈訴诉診诊詞词譯译試试詩诗誠诚話话誕诞詭诡詢询該该詳详誤误語语誘诱說说誦诵請请諸诸諾诺讀读課课誰谁調调諒谅談谈誼谊謀谋謊谎諧谐謂谓謎谜謝谢謠谣謙谦謹谨謬谬譜谱貝贝貞贞負负貢贡財财責责賢贤敗败賬账貨货質质販贩貪贪貧贫購购貫贯貼贴貴贵貸贷貿贸費费賀贺賊贼資资賦赋賭赌贖赎賞赏賜赐賠赔賴赖賺赚賽赛贈赠贊赞贏赢趙赵趕赶趨趋躍跃踐践蹤踪躲躲車车軌轨軍军軒轩轉转輪轮軟软轟轰軸轴輕轻載载較较輔辅輛辆輩辈輝辉輸输轄辖辭辞辯辩邊边遼辽達达遷迁過过邁迈運运還还這这進进遠远違违連连遲迟適适選选遜逊遞递邏逻遺遗遙遥鄧邓郵邮鄰邻鬱郁鄭郑醞酝醬酱釀酿釋释裏里鑒鉴針针釘钉鐘钟鋼钢鑰钥鈴铃鐵铁鉛铅銅铜鋁铝銀银鏈链銷销鎖锁鋒锋鋤锄鍋锅錯错錨锚錘锤錦锦鍵键鋸锯鍛锻鏡镜鐮镰鑲镶長长門门閃闪閉闭問问闖闯閑闲間间悶闷閘闸鬧闹閨闺聞闻閣Line truncated
|
||||||
|
// W5-D 按 MP zhconv 差分补字
|
||||||
|
"迴回滾滚魷鱿瑯琅臥卧駭骇";
|
||||||
|
|
||||||
export const TRAD_TO_SIMP: ReadonlyMap<string, string> = (() => {
|
export const TRAD_TO_SIMP: ReadonlyMap<string, string> = (() => {
|
||||||
const chars = [...PAIRS];
|
const chars = [...PAIRS];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CN_NUM, cnToInt } from "./cn-number.ts";
|
import { CN_NUM, cnToInt } from "./cn-number.ts";
|
||||||
import { evalEpisodeOffset } from "./offset.ts";
|
import { evalEpisodeOffset } from "./offset.ts";
|
||||||
|
import { PLATFORMS } from "./platforms.ts";
|
||||||
import { buildGroupRegex, defaultGroupRegex } from "./release-groups.ts";
|
import { buildGroupRegex, defaultGroupRegex } from "./release-groups.ts";
|
||||||
import {
|
import {
|
||||||
type Confidence,
|
type Confidence,
|
||||||
@@ -38,7 +39,14 @@ export function fileKindOf(name: string): FileKind | undefined {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- 可变草稿
|
// ---------------------------------------------------------------- 可变草稿
|
||||||
|
|
||||||
type Draft = ParsedRelease & { _complete?: boolean; _fin?: boolean; _disc?: boolean; _remux?: boolean };
|
type Draft = ParsedRelease & {
|
||||||
|
_complete?: boolean;
|
||||||
|
_fin?: boolean;
|
||||||
|
_disc?: boolean;
|
||||||
|
_remux?: boolean;
|
||||||
|
/** `{[k=v;…]}` 显式身份块里的季/集/类型,finalize 时覆盖推断值 */
|
||||||
|
_explicit?: { season?: number; seasonEnd?: number; episodes?: EpisodeRange; tv?: boolean };
|
||||||
|
};
|
||||||
|
|
||||||
function emptyDraft(raw: string): Draft {
|
function emptyDraft(raw: string): Draft {
|
||||||
return {
|
return {
|
||||||
@@ -67,6 +75,9 @@ const MARK_HARD = " 〆 "; // 日期/动漫集号被剥离后的占位:之后不
|
|||||||
const GROUP_SUFFIX_EXCLUDE =
|
const GROUP_SUFFIX_EXCLUDE =
|
||||||
/^(?:DL|HD|MA|HR|RIP|RAY|X|ES|\d+|\d+BIT|\d{3,4}[PI]|[XH]\.?26[45]|HEVC|AVC|AAC|FLAC|DTS|DDP?[\d.]*|ATMOS|WEB|BLURAY|REMUX|HDR\d*|DV|HLG|SDR|FIN|END|S\d+|E\d+|EP\d+|CD\d*|PART\d*|\d+CH|MKV|MP4)$/i;
|
/^(?:DL|HD|MA|HR|RIP|RAY|X|ES|\d+|\d+BIT|\d{3,4}[PI]|[XH]\.?26[45]|HEVC|AVC|AAC|FLAC|DTS|DDP?[\d.]*|ATMOS|WEB|BLURAY|REMUX|HDR\d*|DV|HLG|SDR|FIN|END|S\d+|E\d+|EP\d+|CD\d*|PART\d*|\d+CH|MKV|MP4)$/i;
|
||||||
const TECH_IN_BRACKET = /\b(?:\d{3,4}[pi]|x26[45]|hevc|avc|web-?dl|webrip|bluray|bdrip|remux)\b|^\d+$/i;
|
const TECH_IN_BRACKET = /\b(?:\d{3,4}[pi]|x26[45]|hevc|avc|web-?dl|webrip|bluray|bdrip|remux)\b|^\d+$/i;
|
||||||
|
/** 开头方括号里明显是组名的写法:字幕组/汉化组/Raws 等后缀,或 `YTS.MX`、`Hall_of_C` 这种单个点/下划线连接的词元 */
|
||||||
|
const GROUPISH_BRACKET =
|
||||||
|
/(?:字幕[组組社]|[汉漢]化[组組]|[发發][布佈][组組]|[压壓]制[组組]|工作室|搬运[组組]?|Raws?|Subs?|Studio|Fansubs?|Team)$|^(?=.{3,16}$)[A-Za-z0-9]+(?:[._][A-Za-z0-9]+){1,2}$/iu;
|
||||||
|
|
||||||
function extractGroup(s: string, extra: readonly string[]): { s: string; group?: ReleaseGroup; ev?: string } {
|
function extractGroup(s: string, extra: readonly string[]): { s: string; group?: ReleaseGroup; ev?: string } {
|
||||||
const re = extra.length > 0 ? buildGroupRegex(extra) : defaultGroupRegex();
|
const re = extra.length > 0 ? buildGroupRegex(extra) : defaultGroupRegex();
|
||||||
@@ -86,7 +97,23 @@ function extractGroup(s: string, extra: readonly string[]): { s: string; group?:
|
|||||||
if (lead) {
|
if (lead) {
|
||||||
const content = lead[1]!.trim();
|
const content = lead[1]!.trim();
|
||||||
const rest = s.slice(lead[0].length);
|
const rest = s.slice(lead[0].length);
|
||||||
if (!TECH_IN_BRACKET.test(content) && (/[[【]/u.test(rest) || /\s-\s\d{1,3}\b/.test(rest))) {
|
// `[片名][01]…`:开头方括号紧跟集号,它就是片名
|
||||||
|
if (/^\s*[[【]\d{1,3}(?:v\d)?[\]】]/u.test(rest)) return { s };
|
||||||
|
// `[中文片名][English Title]…[Group]`:开头是纯中文片名,组名取末尾方括号
|
||||||
|
if (/^\p{Script=Han}+$/u.test(content) && !GROUPISH_BRACKET.test(content)) {
|
||||||
|
const tail = /[[【]([A-Za-z][A-Za-z0-9-]{1,23})[\]】]\s*$/u.exec(rest);
|
||||||
|
if (tail && !TECH_IN_BRACKET.test(tail[1]!) && !GROUP_SUFFIX_EXCLUDE.test(tail[1]!)) {
|
||||||
|
return {
|
||||||
|
s: s.slice(0, s.length - tail[0].length),
|
||||||
|
group: { name: tail[1]!, source: "bracket" },
|
||||||
|
ev: "group:bracket_tail",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!TECH_IN_BRACKET.test(content) &&
|
||||||
|
(/[[【]/u.test(rest) || /\s-\s\d{1,3}\b/.test(rest) || GROUPISH_BRACKET.test(content))
|
||||||
|
) {
|
||||||
return { s: rest, group: { name: content.replace(/&/g, "@"), source: "bracket" }, ev: "group:bracket" };
|
return { s: rest, group: { name: content.replace(/&/g, "@"), source: "bracket" }, ev: "group:bracket" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,6 +164,8 @@ function extractAirDate(s: string): { s: string; airDate?: string; ev?: string }
|
|||||||
if (!m) continue;
|
if (!m) continue;
|
||||||
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
||||||
if (!validDate(y, mo, d)) continue;
|
if (!validDate(y, mo, d)) continue;
|
||||||
|
// 标题开头的日期(`2022.8.2.Twelve.Monkeys.1995`)是发布/收藏日期,不是播出日期
|
||||||
|
if (/^[\s[【(]*$/u.test(s.slice(0, m.index))) return { s: s.slice(m.index + m[0].length) };
|
||||||
return {
|
return {
|
||||||
s: s.slice(0, m.index) + MARK_HARD + s.slice(m.index + m[0].length),
|
s: s.slice(0, m.index) + MARK_HARD + s.slice(m.index + m[0].length),
|
||||||
airDate: `${y}${pad2(mo)}${pad2(d)}`,
|
airDate: `${y}${pad2(mo)}${pad2(d)}`,
|
||||||
@@ -267,12 +296,13 @@ export function scanCnMarkers(input: string): { s: string } & CnMarkers {
|
|||||||
const AUDIO_CODEC_ALT = "DDP|DD|AAC|TrueHD|DTSHDMA|DTSHDHR|DTSHD|DTSX|DTS|FLAC|LPCM|PCM|AC3|EAC3|Opus|Atmos";
|
const AUDIO_CODEC_ALT = "DDP|DD|AAC|TrueHD|DTSHDMA|DTSHDHR|DTSHD|DTSX|DTS|FLAC|LPCM|PCM|AC3|EAC3|Opus|Atmos";
|
||||||
|
|
||||||
function normalizeCompounds(s: string): string {
|
function normalizeCompounds(s: string): string {
|
||||||
return s
|
return (
|
||||||
|
s
|
||||||
.replace(/'/g, "")
|
.replace(/'/g, "")
|
||||||
.replace(/\bWEB[-. ]?DL\b/gi, " WEBDL ")
|
.replace(/\bWEB[-. ]?DL\b/gi, " WEBDL ")
|
||||||
.replace(/\bWEB[-. ]?Rip\b/gi, " WEBRip ")
|
.replace(/\bWEB[-. ]?Rip\b/gi, " WEBRip ")
|
||||||
.replace(/\bBlu[-. ]?Ray\b/gi, " BluRay ")
|
.replace(/\bBlu[-. ]?Ray\b/gi, " BluRay ")
|
||||||
.replace(/\bDTS[-. ]?HD[-. ]?MA\b/gi, " DTSHDMA ")
|
.replace(/\bDTS[-. ]?HD[-. ]?MA(?![A-Za-z])/gi, " DTSHDMA ")
|
||||||
.replace(/\bDTS[-. ]?HD[-. ]?(?:HRA|HR)\b/gi, " DTSHDHR ")
|
.replace(/\bDTS[-. ]?HD[-. ]?(?:HRA|HR)\b/gi, " DTSHDHR ")
|
||||||
.replace(/\bDTS[-. ]?HD\b/gi, " DTSHD ")
|
.replace(/\bDTS[-. ]?HD\b/gi, " DTSHD ")
|
||||||
.replace(/\bDTS[-. :]?X\b/gi, " DTSX ")
|
.replace(/\bDTS[-. :]?X\b/gi, " DTSX ")
|
||||||
@@ -289,39 +319,18 @@ function normalizeCompounds(s: string): string {
|
|||||||
new RegExp(`\\b(${AUDIO_CODEC_ALT})[ ._]*([1-9])[ .]([0-2])(?![0-9])`, "gi"),
|
new RegExp(`\\b(${AUDIO_CODEC_ALT})[ ._]*([1-9])[ .]([0-2])(?![0-9])`, "gi"),
|
||||||
(_all, codec: string, a: string, b: string) => ` ${codec} CH${a}${b} `,
|
(_all, codec: string, a: string, b: string) => ` ${codec} CH${a}${b} `,
|
||||||
)
|
)
|
||||||
.replace(/(\p{Script=Han})([A-Za-z])/gu, "$1 $2")
|
.replace(/\b(EP?)(\d{1,4})-(?:EP?)?(\d{1,4})\b/gi, " $1$2 $1$3 ")
|
||||||
.replace(/([A-Za-z])(\p{Script=Han})/gu, "$1 $2");
|
.replace(/\bS(\d{1,2})\s+(\d{1,3})-(\d{1,3})(?![\d.])/gi, " S$1 E$2 E$3 ")
|
||||||
|
.replace(/\bB-Global\b/gi, " BGLOBAL ")
|
||||||
|
.replace(/[[【]\s*(4320|2160|1440|1080|720|576|480)\s*[\]】]/gu, " $1p ")
|
||||||
|
.replace(/[[【(]\s*([2-9])\.([0-2])\s*[\]】)]/gu, " CH$1$2 ")
|
||||||
|
// 汉字之间夹单个拉丁字母(哆啦A梦)不拆开
|
||||||
|
.replace(/(\p{Script=Han})([A-Za-z])(?!\p{Script=Han})/gu, "$1 $2")
|
||||||
|
.replace(/(?<!\p{Script=Han})([A-Za-z])(\p{Script=Han})/gu, "$1 $2")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOKEN_SPLIT = /[\s._[\]()【】/~~;&|#「」,,()::"!!??、+\-《》]+/u;
|
const TOKEN_SPLIT = /[\s._[\]()【】/~~;&|#「」,,()::"!!??、+\-《》£]+/u;
|
||||||
|
|
||||||
const PLATFORMS: Record<string, string> = {
|
|
||||||
NF: "Netflix",
|
|
||||||
AMZN: "Amazon",
|
|
||||||
MAX: "Max",
|
|
||||||
HMAX: "HBO Max",
|
|
||||||
DSNP: "Disney+",
|
|
||||||
ATVP: "Apple TV+",
|
|
||||||
IT: "iTunes",
|
|
||||||
HULU: "Hulu",
|
|
||||||
PCOK: "Peacock",
|
|
||||||
PMTP: "Paramount+",
|
|
||||||
CR: "Crunchyroll",
|
|
||||||
IQ: "iQIYI",
|
|
||||||
IQIYI: "iQIYI",
|
|
||||||
TX: "Tencent Video",
|
|
||||||
WETV: "WeTV",
|
|
||||||
YK: "Youku",
|
|
||||||
MGTV: "MGTV",
|
|
||||||
BILI: "Bilibili",
|
|
||||||
BG: "B-Global",
|
|
||||||
VIU: "Viu",
|
|
||||||
KKTV: "KKTV",
|
|
||||||
FRIDAY: "friDay",
|
|
||||||
LINETV: "LINE TV",
|
|
||||||
HBO: "HBO",
|
|
||||||
STAN: "Stan",
|
|
||||||
};
|
|
||||||
|
|
||||||
const AUDIO_TOKENS: Record<string, string> = {
|
const AUDIO_TOKENS: Record<string, string> = {
|
||||||
TRUEHD: "TrueHD",
|
TRUEHD: "TrueHD",
|
||||||
@@ -389,7 +398,14 @@ function walkTokens(tokens: string[], d: Draft): void {
|
|||||||
const ev = d.evidence;
|
const ev = d.evidence;
|
||||||
const en: string[] = [];
|
const en: string[] = [];
|
||||||
const cn: string[] = [];
|
const cn: string[] = [];
|
||||||
|
/** 纯假名词元(日文原名):不混进英文名,英文名为空时才用 */
|
||||||
|
const ja: string[] = [];
|
||||||
let lastTitleList: string[] | undefined;
|
let lastTitleList: string[] | undefined;
|
||||||
|
// 原盘判定:Blu-ray 字样 + AVC/HEVC/VC-1/MPEG-2 且没有 x26x/H.26x(对照 MP 内置规则 BLU)
|
||||||
|
let blurayWord = false;
|
||||||
|
let discCodec = false;
|
||||||
|
let encodeCodec = false;
|
||||||
|
let ripWord = false;
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
let hanClosed = false;
|
let hanClosed = false;
|
||||||
let nonYearStops = 0;
|
let nonYearStops = 0;
|
||||||
@@ -398,9 +414,15 @@ function walkTokens(tokens: string[], d: Draft): void {
|
|||||||
stopped = true;
|
stopped = true;
|
||||||
if (kind !== "year") nonYearStops++;
|
if (kind !== "year") nonYearStops++;
|
||||||
};
|
};
|
||||||
const hasTitle = () => en.length > 0 || cn.length > 0;
|
const hasTitle = () => en.length > 0 || cn.length > 0 || ja.length > 0;
|
||||||
const pushTitle = (t: string) => {
|
const pushTitle = (t: string) => {
|
||||||
const list = /\p{Script=Han}/u.test(t) ? cn : /^\d+$/.test(t) ? (lastTitleList ?? en) : en;
|
const list = /\p{Script=Han}/u.test(t)
|
||||||
|
? cn
|
||||||
|
: /^\d+$/.test(t)
|
||||||
|
? (lastTitleList ?? en)
|
||||||
|
: /^[\p{Script=Hiragana}\p{Script=Katakana}ー・]+$/u.test(t)
|
||||||
|
? ja
|
||||||
|
: en;
|
||||||
list.push(t);
|
list.push(t);
|
||||||
lastTitleList = list;
|
lastTitleList = list;
|
||||||
};
|
};
|
||||||
@@ -472,7 +494,8 @@ function walkTokens(tokens: string[], d: Draft): void {
|
|||||||
stop("season");
|
stop("season");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((u === "SEASON" || u === "EPISODE") && next && /^\d{1,4}$/.test(next)) {
|
// `Season 2017` 是片名 + 年份(The Long Season 2017),季号最多 3 位
|
||||||
|
if ((u === "SEASON" || u === "EPISODE") && next && /^\d{1,3}$/.test(next)) {
|
||||||
if (u === "SEASON") {
|
if (u === "SEASON") {
|
||||||
if (d.season === undefined) d.season = Number(next);
|
if (d.season === undefined) d.season = Number(next);
|
||||||
else if (Number(next) > d.season) d.seasonEnd = Number(next);
|
else if (Number(next) > d.season) d.seasonEnd = Number(next);
|
||||||
@@ -556,8 +579,11 @@ function walkTokens(tokens: string[], d: Draft): void {
|
|||||||
// 来源
|
// 来源
|
||||||
if (gate) {
|
if (gate) {
|
||||||
let src: ReleaseSource | undefined;
|
let src: ReleaseSource | undefined;
|
||||||
if (u === "BLURAY" || u === "BDRIP" || u === "BRRIP" || u === "BD") src = "BluRay";
|
if (u === "BLURAY" || u === "BDRIP" || u === "BRRIP" || u === "BD") {
|
||||||
else if (u === "WEBDL") src = "WEB-DL";
|
src = "BluRay";
|
||||||
|
if (u === "BLURAY") blurayWord = true;
|
||||||
|
else if (u !== "BD") ripWord = true;
|
||||||
|
} else if (u === "WEBDL") src = "WEB-DL";
|
||||||
else if (u === "WEBRIP") src = "WEBRip";
|
else if (u === "WEBRIP") src = "WEBRip";
|
||||||
else if (u === "WEB" && (stopped || (next !== undefined && /^\d{3,4}[pi]$/i.test(next)))) src = "WEB-DL";
|
else if (u === "WEB" && (stopped || (next !== undefined && /^\d{3,4}[pi]$/i.test(next)))) src = "WEB-DL";
|
||||||
else if (u === "HDTV" || u === "UHDTV") src = "HDTV";
|
else if (u === "HDTV" || u === "UHDTV") src = "HDTV";
|
||||||
@@ -609,6 +635,8 @@ function walkTokens(tokens: string[], d: Draft): void {
|
|||||||
if (codec) {
|
if (codec) {
|
||||||
d.videoCodec ??= codec;
|
d.videoCodec ??= codec;
|
||||||
if (u === "X264" || u === "X265") d.encoder ??= u === "X264" ? "x264" : "x265";
|
if (u === "X264" || u === "X265") d.encoder ??= u === "X264" ? "x264" : "x265";
|
||||||
|
if (u === "AVC" || u === "HEVC" || u === "VC1" || u === "MPEG2") discCodec = true;
|
||||||
|
else if (/^[XH]26[45]$/.test(u)) encodeCodec = true;
|
||||||
ev.push("codec");
|
ev.push("codec");
|
||||||
stop("codec");
|
stop("codec");
|
||||||
continue;
|
continue;
|
||||||
@@ -626,9 +654,12 @@ function walkTokens(tokens: string[], d: Draft): void {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
m = /^CH(\d)(\d)$/.exec(u);
|
m = /^CH(\d)(\d)$/.exec(u);
|
||||||
if (m && d.audio.length > 0) {
|
if (m) {
|
||||||
const last = d.audio.length - 1;
|
const last = d.audio.length - 1;
|
||||||
if (!/\d\.\d$/.test(d.audio[last]!)) d.audio[last] = `${d.audio[last]} ${m[1]}.${m[2]}`;
|
// 裸声道 `[5.1]`:没有音频编码时单独记一条
|
||||||
|
if (last < 0) d.audio.push(`${m[1]}.${m[2]}`);
|
||||||
|
else if (!/\d\.\d$/.test(d.audio[last]!)) d.audio[last] = `${d.audio[last]} ${m[1]}.${m[2]}`;
|
||||||
|
stop("audio");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
m = /^(\d)AUDIOS?$/.exec(u);
|
m = /^(\d)AUDIOS?$/.exec(u);
|
||||||
@@ -661,12 +692,14 @@ function walkTokens(tokens: string[], d: Draft): void {
|
|||||||
stop("platform");
|
stop("platform");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (stopped && (u === "CHS" || u === "GB")) {
|
if (u === "CHS" || u === "GB") {
|
||||||
d.subtitles = [...new Set([...(d.subtitles ?? []), "zh-Hans"])];
|
d.subtitles = [...new Set([...(d.subtitles ?? []), "zh-Hans"])];
|
||||||
|
stop("subtitle");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (stopped && (u === "CHT" || u === "BIG5")) {
|
if (u === "CHT" || u === "BIG5") {
|
||||||
d.subtitles = [...new Set([...(d.subtitles ?? []), "zh-Hant"])];
|
d.subtitles = [...new Set([...(d.subtitles ?? []), "zh-Hant"])];
|
||||||
|
stop("subtitle");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (stopped && (u === "ENG" || u === "EN")) {
|
if (stopped && (u === "ENG" || u === "EN")) {
|
||||||
@@ -680,20 +713,26 @@ function walkTokens(tokens: string[], d: Draft): void {
|
|||||||
if (stopped) continue;
|
if (stopped) continue;
|
||||||
if (/\p{Script=Han}/u.test(t)) {
|
if (/\p{Script=Han}/u.test(t)) {
|
||||||
if (!hanClosed) {
|
if (!hanClosed) {
|
||||||
cn.push(t);
|
// 「三部曲合集」「合集」是打包说明,不是片名的一部分
|
||||||
|
const name = t.length > 2 ? t.replace(/合集$/u, "") : t;
|
||||||
|
cn.push(name);
|
||||||
lastTitleList = cn;
|
lastTitleList = cn;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
pushTitle(t);
|
pushTitle(t);
|
||||||
}
|
}
|
||||||
|
if (en.length === 0 && ja.length > 0) en.push(...ja);
|
||||||
const enTitle = en.join(" ").trim();
|
const enTitle = en.join(" ").trim();
|
||||||
const cnTitle = cn.join(" ").trim();
|
const cnTitle = cn.join(" ").trim();
|
||||||
if (enTitle) d.titleEn = enTitle;
|
if (enTitle) d.titleEn = enTitle;
|
||||||
if (cnTitle) d.titleCn = cnTitle;
|
if (cnTitle) d.titleCn = cnTitle;
|
||||||
if (d._remux) d.source = "REMUX";
|
if (d._remux) d.source = "REMUX";
|
||||||
else if (d._disc) d.source = "BluRay-Disc";
|
else if (d._disc) d.source = "BluRay-Disc";
|
||||||
else {
|
else if (blurayWord && discCodec && !encodeCodec && !ripWord && !d.encoder && sources.size === 1) {
|
||||||
|
d.source = "BluRay-Disc";
|
||||||
|
ev.push("source:disc_codec");
|
||||||
|
} else {
|
||||||
for (const s of ["BluRay", "WEB-DL", "WEBRip", "HDTV", "HDRip", "DVDRip", "DVD"] as const) {
|
for (const s of ["BluRay", "WEB-DL", "WEBRip", "HDTV", "HDRip", "DVDRip", "DVD"] as const) {
|
||||||
if (sources.has(s)) {
|
if (sources.has(s)) {
|
||||||
d.source = s;
|
d.source = s;
|
||||||
@@ -703,6 +742,82 @@ function walkTokens(tokens: string[], d: Draft): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 预处理
|
||||||
|
|
||||||
|
const BRACKET_DOT_TITLE =
|
||||||
|
/^\s*\[([A-Za-z0-9][^\s[\]]*?\.(?:19|20)\d{2}\.[^\s[\]]*?\b(?:\d{3,4}p|Blu-?Ray|WEB-?DL|WEBRip|HDTV|REMUX|UHD)\b[^\s[\]]*)\]/i;
|
||||||
|
const ANIME_TAG_CORE =
|
||||||
|
"(?:(?:\\d{2,4}\\s*年\\s*)?(?:\\d{1,2}\\s*月\\s*)?(?:新番|番剧|番)|(?:\\d{2,4}\\s*年\\s*)?[日美国國韩韓港台][漫剧劇])";
|
||||||
|
const ANIME_TAG_BRACKET = new RegExp(`[[【]\\s*★?\\s*${ANIME_TAG_CORE}\\s*★?\\s*[\\]】]`, "gu");
|
||||||
|
const ANIME_TAG_BARE = new RegExp(`★\\s*${ANIME_TAG_CORE}\\s*★?|(?<![\\p{Script=Han}\\d])\\d{1,2}月新番★?`, "gu");
|
||||||
|
/** 开头方括号是分类词(动画/电影/纪录片/Animation/Movie…),MP 同样整块去掉 */
|
||||||
|
const CATEGORY_LEAD =
|
||||||
|
/^\s*[[【](?:[^\]】]*?(?:Animations?|Movies?|Documentar(?:y|ies)|Anime)\b[^\]】]*|[动動漫画畫纪紀录錄片电電影视視连連续續剧劇集日美韩韓中港台海外亚亞洲华華语語大陆陸综綜艺藝原盘盤高清]{2,})[\]】]\s*(?=[[【])/iu;
|
||||||
|
const CN_ALIAS_SLASH =
|
||||||
|
/(?<=^|[\s\]】])[^\s/[\]【】]*\p{Script=Han}[^\s/[\]【】]*\s*\/\s*(?=[^\s/[\]【】]*\p{Script=Han})/u;
|
||||||
|
|
||||||
|
function addExplicit(d: Draft, x: ExplicitId): void {
|
||||||
|
if (x.id === "0" || d.explicitIds.some((e) => e.provider === x.provider)) return;
|
||||||
|
d.explicitIds.push(x);
|
||||||
|
d.evidence.push(`explicit:${x.provider}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLOCK_PROVIDERS: Record<string, ExplicitId["provider"]> = {
|
||||||
|
tmdbid: "tmdb",
|
||||||
|
tmdb: "tmdb",
|
||||||
|
doubanid: "douban",
|
||||||
|
douban: "douban",
|
||||||
|
imdbid: "imdb",
|
||||||
|
imdb: "imdb",
|
||||||
|
tvdbid: "tvdb",
|
||||||
|
tvdb: "tvdb",
|
||||||
|
bangumiid: "bangumi",
|
||||||
|
bangumi: "bangumi",
|
||||||
|
anilistid: "anilist",
|
||||||
|
anilist: "anilist",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** `{[tmdbid=94997;type=tv;s=1;e=3-4;g=…]}`:返回 ID 与季/集/类型;没有任何可识别键时返回 undefined(原样保留) */
|
||||||
|
function parseExplicitBlock(body: string): { ids: ExplicitId[]; meta: NonNullable<Draft["_explicit"]> } | undefined {
|
||||||
|
const ids: ExplicitId[] = [];
|
||||||
|
const meta: NonNullable<Draft["_explicit"]> = {};
|
||||||
|
let hit = false;
|
||||||
|
const range = (v: string) => {
|
||||||
|
const m = /^(\d{1,4})(?:\s*-\s*(\d{1,4}))?$/.exec(v.trim());
|
||||||
|
return m ? { start: Number(m[1]), end: Number(m[2] ?? m[1]) } : undefined;
|
||||||
|
};
|
||||||
|
for (const part of body.split(";")) {
|
||||||
|
const kv = /^\s*([A-Za-z]+)\s*=\s*(.*?)\s*$/.exec(part);
|
||||||
|
if (!kv) continue;
|
||||||
|
const key = kv[1]!.toLowerCase();
|
||||||
|
const val = kv[2]!;
|
||||||
|
const provider = BLOCK_PROVIDERS[key];
|
||||||
|
if (provider) {
|
||||||
|
if (/^[A-Za-z0-9]+$/.test(val)) ids.push({ provider, id: val });
|
||||||
|
hit = true;
|
||||||
|
} else if (key === "type") {
|
||||||
|
if (/^(tv|电视剧|電視劇)$/i.test(val)) meta.tv = true;
|
||||||
|
hit = true;
|
||||||
|
} else if (key === "s") {
|
||||||
|
const r = range(val);
|
||||||
|
if (r) {
|
||||||
|
meta.season = r.start;
|
||||||
|
if (r.end > r.start) meta.seasonEnd = r.end;
|
||||||
|
}
|
||||||
|
hit = true;
|
||||||
|
} else if (key === "e") {
|
||||||
|
const r = range(val);
|
||||||
|
if (r) meta.episodes = r;
|
||||||
|
hit = true;
|
||||||
|
} else if (key === "g") hit = true;
|
||||||
|
}
|
||||||
|
return hit ? { ids, meta } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 常见拼音音节(不含声调);用于判断英文名是否其实是拼音 */
|
||||||
|
const PINYIN_SYLLABLE =
|
||||||
|
/^(?:[bpmfdtnlgkhjqxrzcsyw]|zh|ch|sh)?(?:iang|iong|uang|ueng|ang|eng|ing|ong|ian|iao|uai|uan|ai|ei|ao|ou|an|en|in|un|ia|ie|iu|ua|uo|ui|ue|er|a|o|e|i|u|v)$/i;
|
||||||
|
|
||||||
// ---------------------------------------------------------------- 主入口
|
// ---------------------------------------------------------------- 主入口
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -724,19 +839,31 @@ export function parseRelease(input: string, opts: ParseOptions = {}): ParsedRele
|
|||||||
s = s.slice(0, -(ext.length + 1));
|
s = s.slice(0, -(ext.length + 1));
|
||||||
} else if (opts.isFile) d.isFile = true;
|
} else if (opts.isFile) d.isFile = true;
|
||||||
|
|
||||||
// 2. 显式身份标签
|
// 2. 显式身份标签:先取 `{[k=v;…]}` 块(季/集/类型一并给出),再取单个 `[tmdbid=…]`
|
||||||
|
s = s.replace(/\{\[([^\]{}]*)\]\}/g, (all, body: string) => {
|
||||||
|
const block = parseExplicitBlock(body);
|
||||||
|
if (!block) return all;
|
||||||
|
for (const x of block.ids) addExplicit(d, x);
|
||||||
|
d._explicit = block.meta;
|
||||||
|
ev.push("explicit:block");
|
||||||
|
return " ";
|
||||||
|
});
|
||||||
s = s.replace(
|
s = s.replace(
|
||||||
/[[{]\s*(tmdbid|tmdb|imdbid|imdb|doubanid|douban|tvdbid|tvdb)\s*[=-]\s*([A-Za-z0-9]+)\s*[\]}]/gi,
|
/[[{]\s*(tmdbid|tmdb|imdbid|imdb|doubanid|douban|tvdbid|tvdb|bangumiid|bangumi|anilistid|anilist)\s*[=-]\s*([A-Za-z0-9]+)\s*[\]}]/gi,
|
||||||
(_all, key: string, id: string) => {
|
(_all, key: string, id: string) => {
|
||||||
const k = key.toLowerCase().replace(/id$/, "") as ExplicitId["provider"];
|
addExplicit(d, { provider: key.toLowerCase().replace(/id$/, "") as ExplicitId["provider"], id });
|
||||||
if (id !== "0" && !d.explicitIds.some((x) => x.provider === k)) {
|
|
||||||
d.explicitIds.push({ provider: k, id });
|
|
||||||
ev.push(`explicit:${k}`);
|
|
||||||
}
|
|
||||||
return " ";
|
return " ";
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 2b. 开头方括号里是完整的点分隔发布名(`[Name.2023.2160p.UHD.Blu-ray…-GRP][DIY中字原盘]…`):去括号保留内容
|
||||||
|
s = s.replace(BRACKET_DOT_TITLE, " $1 ");
|
||||||
|
// 2c. 动漫前缀标签(★7月新番★、[国漫]、【22年日剧】、[2022年7月番])与开头的分类方括号([Animations(动画片)])
|
||||||
|
s = s.replace(ANIME_TAG_BRACKET, " ").replace(ANIME_TAG_BARE, " ");
|
||||||
|
s = s.replace(CATEGORY_LEAD, "");
|
||||||
|
// 2d. `中文名A / 中文名B`:同一作品的两个中文译名只留后一个(与 MP 取法一致),避免拼成一个名字
|
||||||
|
s = s.replace(CN_ALIAS_SLASH, "");
|
||||||
|
|
||||||
// 3. 纯数字文件名 → 集号
|
// 3. 纯数字文件名 → 集号
|
||||||
if (d.isFile && /^\s*\d{1,4}\s*$/.test(s)) {
|
if (d.isFile && /^\s*\d{1,4}\s*$/.test(s)) {
|
||||||
const n = Number(s.trim());
|
const n = Number(s.trim());
|
||||||
@@ -765,9 +892,11 @@ export function parseRelease(input: string, opts: ParseOptions = {}): ParsedRele
|
|||||||
for (const [re, cut] of CUT_RULES) {
|
for (const [re, cut] of CUT_RULES) {
|
||||||
s = s.replace(re, () => {
|
s = s.replace(re, () => {
|
||||||
d.cut ??= cut;
|
d.cut ??= cut;
|
||||||
return " ";
|
return "\uE001";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// `Caligula.The.Ultimate.Cut` 去掉剪辑版本后,紧挨着的冠词 The 一并去掉
|
||||||
|
s = s.replace(/\bThe[ .\-_]*\uE001/gi, " ").replace(/\uE001/g, " ");
|
||||||
if (d.cut) ev.push("cut");
|
if (d.cut) ev.push("cut");
|
||||||
|
|
||||||
// 7. 综艺播出日期(在年份识别之前,避免被当成年份或被丢弃)
|
// 7. 综艺播出日期(在年份识别之前,避免被当成年份或被丢弃)
|
||||||
@@ -786,7 +915,26 @@ export function parseRelease(input: string, opts: ParseOptions = {}): ParsedRele
|
|||||||
// 9. 年份区间 2019-2020 只保留前者
|
// 9. 年份区间 2019-2020 只保留前者
|
||||||
s = s.replace(/(?<!\d)((?:19|20)\d{2})\s*[-~]\s*(?:19|20)\d{2}(?!\d)/, "$1");
|
s = s.replace(/(?<!\d)((?:19|20)\d{2})\s*[-~]\s*(?:19|20)\d{2}(?!\d)/, "$1");
|
||||||
|
|
||||||
// 10. 动漫式集号:" - 12 "、"[01-11 Fin]"、"[12]"
|
// 10. 动漫式集号:" - 12 "、"[01-11 Fin]"、"[12]"、"[TV 04]"、"#13"
|
||||||
|
s = s.replace(
|
||||||
|
/[[【]\s*TV\s*(\d{1,4})(?:\s*[-~]\s*(\d{1,4}))?(\s*(?:Fin|END|完))?\s*[\]】]/iu,
|
||||||
|
(all, a: string, b, fin) => {
|
||||||
|
if (d.episodes) return all;
|
||||||
|
d.episodes = { start: Number(a), end: Number(b ?? a) };
|
||||||
|
ev.push("episode:bracket_tv");
|
||||||
|
if (fin) {
|
||||||
|
d._fin = true;
|
||||||
|
ev.push("pack:fin");
|
||||||
|
}
|
||||||
|
return MARK_HARD;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
s = s.replace(/(?<=\S\s+)#(\d{1,4})(?=\s|[[((]|$)/u, (all, n: string) => {
|
||||||
|
if (d.episodes) return all;
|
||||||
|
d.episodes = { start: Number(n), end: Number(n) };
|
||||||
|
ev.push("episode:hash");
|
||||||
|
return MARK_HARD;
|
||||||
|
});
|
||||||
s = s.replace(
|
s = s.replace(
|
||||||
/[[【]\s*(\d{1,4})\s*[-~]\s*(\d{1,4})(\s*(?:Fin|END|完))?\s*[\]】]/iu,
|
/[[【]\s*(\d{1,4})\s*[-~]\s*(\d{1,4})(\s*(?:Fin|END|完))?\s*[\]】]/iu,
|
||||||
(all, a: string, b: string, fin) => {
|
(all, a: string, b: string, fin) => {
|
||||||
@@ -814,13 +962,20 @@ export function parseRelease(input: string, opts: ParseOptions = {}): ParsedRele
|
|||||||
}
|
}
|
||||||
return MARK_HARD;
|
return MARK_HARD;
|
||||||
});
|
});
|
||||||
if (d.releaseGroup?.source === "bracket") {
|
// `[Group][Name][05]`、`Name [01][1080p]`:名称后的方括号纯数字是集号
|
||||||
s = s.replace(/[[【](\d{1,3})(?:v\d)?[\]】]/u, (all, n: string) => {
|
s = s.replace(/(?<=\S\s*)[[【](\d{1,3})(?:v\d)?[\]】]/u, (all, n: string) => {
|
||||||
if (d.episodes) return all;
|
if (d.episodes) return all;
|
||||||
d.episodes = { start: Number(n), end: Number(n) };
|
d.episodes = { start: Number(n), end: Number(n) };
|
||||||
ev.push("episode:anime_bracket");
|
ev.push("episode:anime_bracket");
|
||||||
return MARK_HARD;
|
return MARK_HARD;
|
||||||
});
|
});
|
||||||
|
// 文件名 `西部世界 12`:中文名后空格加 2~3 位数字
|
||||||
|
if (d.isFile && !d.episodes) {
|
||||||
|
s = s.replace(/(?<=\p{Script=Han})\s+(\d{2,3})\s*$/u, (_all, n: string) => {
|
||||||
|
d.episodes = { start: Number(n), end: Number(n) };
|
||||||
|
ev.push("episode:cn_name_number");
|
||||||
|
return MARK_HARD;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. 词元扫描
|
// 11. 词元扫描
|
||||||
@@ -837,7 +992,22 @@ export function parseRelease(input: string, opts: ParseOptions = {}): ParsedRele
|
|||||||
|
|
||||||
// 13. 副标题补缺
|
// 13. 副标题补缺
|
||||||
if (opts.subtitle) {
|
if (opts.subtitle) {
|
||||||
|
// 英文名其实是拼音(Mai Xiang ↔ 麦香):中文名取副标题第一个词
|
||||||
|
if (!d.titleCn && d.titleEn) {
|
||||||
|
const words = d.titleEn.split(/\s+/);
|
||||||
|
const first = /^\s*(\p{Script=Han}{2,12})(?=[\s||/,,]|$)/u.exec(opts.subtitle)?.[1];
|
||||||
|
if (first && words.length === [...first].length && words.every((w) => PINYIN_SYLLABLE.test(w))) {
|
||||||
|
d.titleCn = first;
|
||||||
|
ev.push("subtitle:title_cn_pinyin");
|
||||||
|
}
|
||||||
|
}
|
||||||
const sub = scanCnMarkers(opts.subtitle);
|
const sub = scanCnMarkers(opts.subtitle);
|
||||||
|
// 副标题里的拉丁字幕标记 CHS/CHT/GB/BIG5(`60 GB` 这类体积不算)
|
||||||
|
for (const m of opts.subtitle.matchAll(/(?<![A-Za-z0-9])(?<!\d\s?)(CHS|CHT|GB|BIG5)(?![A-Za-z0-9])/gi)) {
|
||||||
|
const lang = /^(CHS|GB)$/i.test(m[1]!) ? "zh-Hans" : "zh-Hant";
|
||||||
|
sub.subtitles = [...new Set([...(sub.subtitles ?? []), lang])];
|
||||||
|
if (!sub.evidence.includes("subtitle:latin")) sub.evidence.push("subtitle:latin");
|
||||||
|
}
|
||||||
applyCnMarkers(d, sub, "subtitle:");
|
applyCnMarkers(d, sub, "subtitle:");
|
||||||
if (!d.airDate) {
|
if (!d.airDate) {
|
||||||
const sad = extractAirDate(opts.subtitle);
|
const sad = extractAirDate(opts.subtitle);
|
||||||
@@ -900,13 +1070,22 @@ function finalize(d: Draft, opts: ParseOptions): ParsedRelease {
|
|||||||
} else ev.push("episode:offset_rejected");
|
} else ev.push("episode:offset_rejected");
|
||||||
}
|
}
|
||||||
if (d.totalEpisodes !== undefined && d.episodes === undefined && d.isFile) d.totalEpisodes = undefined;
|
if (d.totalEpisodes !== undefined && d.episodes === undefined && d.isFile) d.totalEpisodes = undefined;
|
||||||
|
// 显式身份块的季/集最后覆盖(对照 MP _apply_explicit_metainfo)
|
||||||
|
const x = d._explicit;
|
||||||
|
if (x?.season !== undefined) {
|
||||||
|
d.season = x.season;
|
||||||
|
d.seasonEnd = x.seasonEnd;
|
||||||
|
d.seasonInferred = undefined;
|
||||||
|
}
|
||||||
|
if (x?.episodes) d.episodes = x.episodes;
|
||||||
|
|
||||||
const tvSignals =
|
const tvSignals =
|
||||||
d.season !== undefined ||
|
d.season !== undefined ||
|
||||||
d.episodes !== undefined ||
|
d.episodes !== undefined ||
|
||||||
d.airDate !== undefined ||
|
d.airDate !== undefined ||
|
||||||
d.issue !== undefined ||
|
d.issue !== undefined ||
|
||||||
d.totalEpisodes !== undefined;
|
d.totalEpisodes !== undefined ||
|
||||||
|
d._explicit?.tv === true;
|
||||||
d.mediaTypeHint = tvSignals ? "tv" : "unknown";
|
d.mediaTypeHint = tvSignals ? "tv" : "unknown";
|
||||||
|
|
||||||
// 季号推定:有集号/总集数但无季号,且不是综艺日期/期号
|
// 季号推定:有集号/总集数但无季号,且不是综艺日期/期号
|
||||||
@@ -929,7 +1108,7 @@ function finalize(d: Draft, opts: ParseOptions): ParsedRelease {
|
|||||||
d.title = d.titleCn ?? d.titleEn;
|
d.title = d.titleCn ?? d.titleEn;
|
||||||
d.confidence = confidenceOf(d);
|
d.confidence = confidenceOf(d);
|
||||||
|
|
||||||
const { _complete, _fin, _disc, _remux, ...out } = d;
|
const { _complete, _fin, _disc, _remux, _explicit, ...out } = d;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,14 @@ export function mergeParsed(
|
|||||||
evidence: [...primary.evidence],
|
evidence: [...primary.evidence],
|
||||||
};
|
};
|
||||||
const used: string[] = [];
|
const used: string[] = [];
|
||||||
const fbTv = fallback.mediaTypeHint === "tv";
|
// 目录只有日期(`…Collection.20240501.FRDS` 这类收藏/打包日期)不算剧集信号,不参与类型合并
|
||||||
|
const dateOnly =
|
||||||
|
fallback.airDate !== undefined &&
|
||||||
|
fallback.season === undefined &&
|
||||||
|
fallback.episodes === undefined &&
|
||||||
|
fallback.issue === undefined &&
|
||||||
|
fallback.totalEpisodes === undefined;
|
||||||
|
const fbTv = fallback.mediaTypeHint === "tv" && !dateOnly;
|
||||||
const deviation = primary.mediaTypeHint !== "tv" && fbTv;
|
const deviation = primary.mediaTypeHint !== "tv" && fbTv;
|
||||||
|
|
||||||
if (!primary.title) {
|
if (!primary.title) {
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* 流媒体平台简称 → 展示名(scene 命名惯例,事实数据)。
|
||||||
|
* 条目对照 MoviePilot `streamingplatform.py` 的简称表补齐;只收单词元简称(含 `-` 的写法在 normalizeCompounds 里先合并),
|
||||||
|
* 与英文常用词/其他技术词冲突的简称(FREE、NOW、PLAY、LIFE、MA、AS、SE、TEN…)刻意不收,避免误判。
|
||||||
|
* 词元只在 WEB 来源词前后 2 个位置内才当作平台(见 walkTokens 的 nearWeb)。
|
||||||
|
* 与 MP 的表示差异:HMAX 记 "HBO Max"(MP 记 "Max"),差分登记为 representation。
|
||||||
|
*/
|
||||||
|
export const PLATFORMS: Readonly<Record<string, string>> = {
|
||||||
|
NF: "Netflix",
|
||||||
|
AMZN: "Amazon",
|
||||||
|
MAX: "Max",
|
||||||
|
HMAX: "HBO Max",
|
||||||
|
DSNP: "Disney+",
|
||||||
|
DSNY: "Disney Networks",
|
||||||
|
ATVP: "Apple TV+",
|
||||||
|
IT: "iTunes",
|
||||||
|
HULU: "Hulu",
|
||||||
|
HULUJP: "Hulu",
|
||||||
|
PCOK: "Peacock",
|
||||||
|
PMTP: "Paramount+",
|
||||||
|
PMNT: "Paramount Network",
|
||||||
|
CR: "Crunchyroll",
|
||||||
|
FUNI: "Funimation",
|
||||||
|
HIDI: "HIDIVE",
|
||||||
|
IQ: "iQIYI",
|
||||||
|
IQIYI: "iQIYI",
|
||||||
|
TX: "Tencent Video",
|
||||||
|
WETV: "WeTV",
|
||||||
|
YK: "Youku",
|
||||||
|
MGTV: "MGTV",
|
||||||
|
BILI: "Bilibili",
|
||||||
|
BG: "B-Global",
|
||||||
|
BGLOBAL: "B-Global",
|
||||||
|
BAHA: "Baha",
|
||||||
|
VIU: "Viu",
|
||||||
|
KKTV: "KKTV",
|
||||||
|
FRIDAY: "friDay",
|
||||||
|
LITV: "LiTV",
|
||||||
|
MYVIDEO: "MyVideo",
|
||||||
|
OFIII: "ofiii",
|
||||||
|
HAMI: "Hami Video",
|
||||||
|
HAMIVIDEO: "Hami Video",
|
||||||
|
LINETV: "LINE TV",
|
||||||
|
CATCHPLAY: "CATCHPLAY+",
|
||||||
|
CPP: "CATCHPLAY+",
|
||||||
|
CTHP: "CATCHPLAY+",
|
||||||
|
MYTVS: "MyTVSuper",
|
||||||
|
NOWE: "Now E",
|
||||||
|
NOWPLAYER: "NowPlayer",
|
||||||
|
GAGA: "GagaOOLala",
|
||||||
|
HBO: "HBO",
|
||||||
|
HBOGO: "HBO GO",
|
||||||
|
STAN: "Stan",
|
||||||
|
HS: "Hotstar",
|
||||||
|
APPS: "Disney+ MENA",
|
||||||
|
BCORE: "Bravia Core",
|
||||||
|
SHO: "Showtime",
|
||||||
|
SKST: "SkyShowtime",
|
||||||
|
FXTL: "Foxtel Now",
|
||||||
|
BNGE: "Binge",
|
||||||
|
CRKL: "Crackle",
|
||||||
|
RKTN: "Rakuten TV",
|
||||||
|
VIKI: "Rakuten Viki",
|
||||||
|
ALL4: "Channel 4",
|
||||||
|
MY5: "Channel 5",
|
||||||
|
ITVX: "ITV",
|
||||||
|
IP: "BBC iPlayer",
|
||||||
|
BBC: "BBC",
|
||||||
|
BRITBOX: "BritBox",
|
||||||
|
UKTV: "UKTV",
|
||||||
|
CNLP: "Canal+",
|
||||||
|
CRIT: "Criterion Channel",
|
||||||
|
DSCP: "Discovery+",
|
||||||
|
DISC: "Discovery Channel",
|
||||||
|
MUBI: "Mubi",
|
||||||
|
YT: "YouTube",
|
||||||
|
MW: "meWATCH",
|
||||||
|
ABMA: "Abema",
|
||||||
|
ABEMATV: "Abema",
|
||||||
|
UNXT: "U-NEXT",
|
||||||
|
DMM: "DMM",
|
||||||
|
LEMINO: "Lemino",
|
||||||
|
TVER: "TVer",
|
||||||
|
WAKA: "Wakanim",
|
||||||
|
WAKANIM: "Wakanim",
|
||||||
|
AOD: "Anime on Demand",
|
||||||
|
BCH: "Bandai Channel",
|
||||||
|
VMJ: "VideoMarket",
|
||||||
|
LFTL: "Laftel",
|
||||||
|
LFTLNET: "Laftel",
|
||||||
|
AO: "AnimeOnegai",
|
||||||
|
"9NOW": "9Now",
|
||||||
|
"7PLUS": "7plus",
|
||||||
|
SBS: "SBS",
|
||||||
|
AUBC: "ABC iview",
|
||||||
|
SHAHID: "Shahid",
|
||||||
|
OSN: "OSN+",
|
||||||
|
TOU: "Ici TOU.TV",
|
||||||
|
ROKU: "Roku",
|
||||||
|
KNPY: "Kanopy",
|
||||||
|
SNXT: "Sun NXT",
|
||||||
|
CUR: "Curiosity Stream",
|
||||||
|
AHA: "aha",
|
||||||
|
WOWP: "WOW Presents Plus",
|
||||||
|
JC: "JioCinema",
|
||||||
|
ZEE5: "ZEE5",
|
||||||
|
HOICHOI: "Hoichoi",
|
||||||
|
PLEX: "Plex",
|
||||||
|
SHDR: "Shudder",
|
||||||
|
CRAV: "Crave",
|
||||||
|
CPE: "Cineplex Entertainment",
|
||||||
|
VIAP: "Viaplay",
|
||||||
|
TUBI: "TubiTV",
|
||||||
|
PLUTOTV: "Pluto TV",
|
||||||
|
PBSK: "PBS KIDS",
|
||||||
|
PBS: "PBS",
|
||||||
|
LGP: "Lionsgate Play",
|
||||||
|
STZ: "STARZ",
|
||||||
|
FUBO: "fuboTV",
|
||||||
|
CBC: "CBC Gem",
|
||||||
|
CW: "The CW",
|
||||||
|
CWS: "CW Seed",
|
||||||
|
CMAX: "Cinemax",
|
||||||
|
HLMK: "Hallmark",
|
||||||
|
AMC: "AMC",
|
||||||
|
USAN: "USA Network",
|
||||||
|
BRAV: "BravoTV",
|
||||||
|
TNT: "TNT",
|
||||||
|
TCM: "TCM",
|
||||||
|
TLC: "TLC",
|
||||||
|
HGTV: "HGTV",
|
||||||
|
ANPL: "Animal Planet",
|
||||||
|
TRVL: "Travel Channel",
|
||||||
|
NATG: "National Geographic",
|
||||||
|
STRP: "Star+",
|
||||||
|
CNNP: "CNN+",
|
||||||
|
EPIX: "EPIX MGM+",
|
||||||
|
HIST: "History Channel",
|
||||||
|
NICK: "Nickelodeon",
|
||||||
|
SYFY: "SyFy",
|
||||||
|
DCU: "DC Universe",
|
||||||
|
QIBI: "Quibi",
|
||||||
|
VRV: "VRV",
|
||||||
|
VMEO: "Vimeo",
|
||||||
|
WTCH: "Watcha",
|
||||||
|
WAVVE: "Wavve",
|
||||||
|
TVING: "TVING",
|
||||||
|
ODK: "OnDemandKorea",
|
||||||
|
VIDIO: "Vidio",
|
||||||
|
FPT: "FPT Play",
|
||||||
|
FPTP: "FPT Play",
|
||||||
|
JOYN: "Joyn",
|
||||||
|
RTL: "RTL+",
|
||||||
|
ARTE: "Arte",
|
||||||
|
ZDF: "ZDF",
|
||||||
|
ARD: "ARD",
|
||||||
|
SVT: "Sveriges Television",
|
||||||
|
VIAPLAY: "Viaplay",
|
||||||
|
CMOR: "C More",
|
||||||
|
FTV: "France.tv",
|
||||||
|
GLBO: "Globoplay",
|
||||||
|
A3P: "Atresplayer",
|
||||||
|
FILMIN: "Filmin",
|
||||||
|
OKKO: "Okko",
|
||||||
|
IVI: "Ivi",
|
||||||
|
VOYO: "Voyo",
|
||||||
|
DANT: "DANET",
|
||||||
|
SMNS: "SAMANSA",
|
||||||
|
};
|
||||||
@@ -11,7 +11,8 @@ export const RELEASE_GROUP_SEED: Readonly<Record<string, readonly string[]>> = {
|
|||||||
hdsky: ["HDSky", "HDS", "HDSWEB", "HDSTV", "HDSPad"],
|
hdsky: ["HDSky", "HDS", "HDSWEB", "HDSTV", "HDSPad"],
|
||||||
pthome: ["PTHome", "PTH", "PTHweb"],
|
pthome: ["PTHome", "PTH", "PTHweb"],
|
||||||
cmct: ["CMCT", "CMCTV"],
|
cmct: ["CMCT", "CMCTV"],
|
||||||
frds: ["FRDS"],
|
frds: ["FRDS", "cXcY"],
|
||||||
|
league: ["LeagueWEB", "LeagueTV", "LeagueHD", "LeagueNF", "LeagueCD"],
|
||||||
ttg: ["TTG", "WiKi", "NGB", "DoA"],
|
ttg: ["TTG", "WiKi", "NGB", "DoA"],
|
||||||
hdchina: ["HDChina", "HDC"],
|
hdchina: ["HDChina", "HDC"],
|
||||||
beitai: ["BeiTai"],
|
beitai: ["BeiTai"],
|
||||||
@@ -54,12 +55,12 @@ export const RELEASE_GROUP_SEED: Readonly<Record<string, readonly string[]>> = {
|
|||||||
|
|
||||||
const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
|
||||||
/** 构造发布组匹配正则:前界定符 - @ [ 【 & ,后界定符 结尾 @ . 空白 ] [ 】 & */
|
/** 构造发布组匹配正则:前界定符 - @ [ 【 & £,后界定符 结尾 @ . 空白 ] [ 】 & £(`AC3£cXcY@FRDS` 这类多组写法) */
|
||||||
export function buildGroupRegex(extra: readonly string[] = []): RegExp {
|
export function buildGroupRegex(extra: readonly string[] = []): RegExp {
|
||||||
const all = [...Object.values(RELEASE_GROUP_SEED).flat(), ...extra];
|
const all = [...Object.values(RELEASE_GROUP_SEED).flat(), ...extra];
|
||||||
// 长名优先,避免 CHD 抢先匹配 CHDWEB
|
// 长名优先,避免 CHD 抢先匹配 CHDWEB
|
||||||
const uniq = [...new Set(all)].sort((a, b) => b.length - a.length).map(escapeRe);
|
const uniq = [...new Set(all)].sort((a, b) => b.length - a.length).map(escapeRe);
|
||||||
return new RegExp(`(?<=[-@\\[【&])(?:${uniq.join("|")})(?=$|[@.\\s\\]\\[】&])`, "giu");
|
return new RegExp(`(?<=[-@\\[【&£])(?:${uniq.join("|")})(?=$|[@.\\s\\]\\[】&£])`, "giu");
|
||||||
}
|
}
|
||||||
|
|
||||||
let cached: RegExp | undefined;
|
let cached: RegExp | undefined;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export interface ReleaseGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExplicitId {
|
export interface ExplicitId {
|
||||||
provider: "tmdb" | "imdb" | "douban" | "tvdb";
|
provider: "tmdb" | "imdb" | "douban" | "tvdb" | "bangumi" | "anilist";
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12216,78 +12216,30 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"deviations": {
|
"deviations": {
|
||||||
"R06:t03": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「2160p UHD Blu-ray HEVC」被 parseRelease 判为 BluRay 压制(isEncode=true);MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": 1,
|
|
||||||
"ours": null
|
|
||||||
},
|
|
||||||
"R06:t08": {
|
"R06:t08": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
||||||
"mp": 1,
|
"mp": 1,
|
||||||
"ours": null
|
"ours": null
|
||||||
},
|
},
|
||||||
"R06:t12": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「1080p Blu-ray AVC」(无 x264)被 parseRelease 判为 BluRay 压制;MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": 1,
|
|
||||||
"ours": null
|
|
||||||
},
|
|
||||||
"R07:t03": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「2160p UHD Blu-ray HEVC」被 parseRelease 判为 BluRay 压制(isEncode=true);MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 1
|
|
||||||
},
|
|
||||||
"R07:t08": {
|
"R07:t08": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": 1
|
"ours": 1
|
||||||
},
|
},
|
||||||
"R07:t12": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「1080p Blu-ray AVC」(无 x264)被 parseRelease 判为 BluRay 压制;MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 1
|
|
||||||
},
|
|
||||||
"R11:t03": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「2160p UHD Blu-ray HEVC」被 parseRelease 判为 BluRay 压制(isEncode=true);MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 1
|
|
||||||
},
|
|
||||||
"R11:t08": {
|
"R11:t08": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": 1
|
"ours": 1
|
||||||
},
|
},
|
||||||
"R11:t12": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「1080p Blu-ray AVC」(无 x264)被 parseRelease 判为 BluRay 压制;MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 1
|
|
||||||
},
|
|
||||||
"R13:t17": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "副标题「CHS/ENG」:MP CNSUB 命中 chs/cht 词;parseRelease 字幕扫描只认中文关键词,未识别 CHS/CHT/GB/BIG5",
|
|
||||||
"mp": 1,
|
|
||||||
"ours": null
|
|
||||||
},
|
|
||||||
"R13:t26": {
|
"R13:t26": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"note": "副标题「国语」(音轨语言)被 MP CNSUB 正则当作中文字幕;我们把国语视为音轨,不满足 requiredSubtitles",
|
"note": "副标题「国语」(音轨语言)被 MP CNSUB 正则当作中文字幕;我们把国语视为音轨,不满足 requiredSubtitles",
|
||||||
"mp": 1,
|
"mp": 1,
|
||||||
"ours": null
|
"ours": null
|
||||||
},
|
},
|
||||||
"R14:t17": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "副标题「CHS/ENG」:MP CNSUB 命中 chs/cht 词;parseRelease 字幕扫描只认中文关键词,未识别 CHS/CHT/GB/BIG5",
|
|
||||||
"mp": 1,
|
|
||||||
"ours": null
|
|
||||||
},
|
|
||||||
"R14:t26": {
|
"R14:t26": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"note": "副标题「国语」(音轨语言)被 MP CNSUB 正则当作中文字幕;我们把国语视为音轨,不满足 requiredSubtitles",
|
"note": "副标题「国语」(音轨语言)被 MP CNSUB 正则当作中文字幕;我们把国语视为音轨,不满足 requiredSubtitles",
|
||||||
@@ -12324,54 +12276,18 @@
|
|||||||
"mp": 1,
|
"mp": 1,
|
||||||
"ours": null
|
"ours": null
|
||||||
},
|
},
|
||||||
"R19:t03": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「2160p UHD Blu-ray HEVC」被 parseRelease 判为 BluRay 压制(isEncode=true);MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 1
|
|
||||||
},
|
|
||||||
"R19:t08": {
|
"R19:t08": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": 1
|
"ours": 1
|
||||||
},
|
},
|
||||||
"R19:t12": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「1080p Blu-ray AVC」(无 x264)被 parseRelease 判为 BluRay 压制;MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 1
|
|
||||||
},
|
|
||||||
"R20:t03": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「2160p UHD Blu-ray HEVC」被 parseRelease 判为 BluRay 压制(isEncode=true);MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 1
|
|
||||||
},
|
|
||||||
"R20:t12": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「1080p Blu-ray AVC」(无 x264)被 parseRelease 判为 BluRay 压制;MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 1
|
|
||||||
},
|
|
||||||
"R21:t03": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「2160p UHD Blu-ray HEVC」被 parseRelease 判为 BluRay 压制(isEncode=true);MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 1
|
|
||||||
},
|
|
||||||
"R21:t08": {
|
"R21:t08": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
"note": "MP BLU 正则分支 `(UHD|4K|2160p).*(HEVC|H.265)` 在无来源标记的 2160p HEVC 上误判为原盘;我们来源未知按 !BLU 放行",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": 1
|
"ours": 1
|
||||||
},
|
},
|
||||||
"R21:t12": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"note": "原盘命名「1080p Blu-ray AVC」(无 x264)被 parseRelease 判为 BluRay 压制;MP BLU 判为原盘。应识别为 BluRay-Disc",
|
|
||||||
"mp": null,
|
|
||||||
"ours": 2
|
|
||||||
},
|
|
||||||
"R23:t01": {
|
"R23:t01": {
|
||||||
"kind": "missing",
|
"kind": "missing",
|
||||||
"note": "MP 规则可带 tmdb 条件:媒体 original_language∈{zh,cn} 时 CNSUB 对任何种子直接放行(华语片不要求中字);Slot 无媒体上下文条件",
|
"note": "MP 规则可带 tmdb 条件:媒体 original_language∈{zh,cn} 时 CNSUB 对任何种子直接放行(华语片不要求中字);Slot 无媒体上下文条件",
|
||||||
@@ -12432,12 +12348,6 @@
|
|||||||
"mp": 1,
|
"mp": 1,
|
||||||
"ours": null
|
"ours": null
|
||||||
},
|
},
|
||||||
"R23:t17": {
|
|
||||||
"kind": "missing",
|
|
||||||
"note": "MP 规则可带 tmdb 条件:媒体 original_language∈{zh,cn} 时 CNSUB 对任何种子直接放行(华语片不要求中字);Slot 无媒体上下文条件",
|
|
||||||
"mp": 1,
|
|
||||||
"ours": null
|
|
||||||
},
|
|
||||||
"R23:t18": {
|
"R23:t18": {
|
||||||
"kind": "missing",
|
"kind": "missing",
|
||||||
"note": "MP 规则可带 tmdb 条件:媒体 original_language∈{zh,cn} 时 CNSUB 对任何种子直接放行(华语片不要求中字);Slot 无媒体上下文条件",
|
"note": "MP 规则可带 tmdb 条件:媒体 original_language∈{zh,cn} 时 CNSUB 对任何种子直接放行(华语片不要求中字);Slot 无媒体上下文条件",
|
||||||
@@ -12546,6 +12456,12 @@
|
|||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": 1
|
"ours": 1
|
||||||
},
|
},
|
||||||
|
"R25:t17": {
|
||||||
|
"kind": "not_expressible",
|
||||||
|
"note": "SPECSUB(特效字幕)无对应字段,近似翻译丢失该条件",
|
||||||
|
"mp": null,
|
||||||
|
"ours": 1
|
||||||
|
},
|
||||||
"R25:t21": {
|
"R25:t21": {
|
||||||
"kind": "not_expressible",
|
"kind": "not_expressible",
|
||||||
"note": "SPECSUB(特效字幕)无对应字段,近似翻译丢失该条件",
|
"note": "SPECSUB(特效字幕)无对应字段,近似翻译丢失该条件",
|
||||||
|
|||||||
@@ -317,12 +317,6 @@
|
|||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "grp",
|
"ours": "grp",
|
||||||
"note": "未知发布组兜底(末段 -GROUP / 开头方括号),MP 组表未命中为 None"
|
"note": "未知发布组兜底(末段 -GROUP / 开头方括号),MP 组表未命中为 None"
|
||||||
},
|
|
||||||
"web_source": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "bbciplayer",
|
|
||||||
"ours": null,
|
|
||||||
"note": "流媒体平台表缺 iP(BBC iPlayer)"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"T-14": {
|
"T-14": {
|
||||||
@@ -345,6 +339,12 @@
|
|||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEB-DL",
|
"ours": "WEB-DL",
|
||||||
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
|
},
|
||||||
|
"web_source": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "baha",
|
||||||
|
"note": "MetaAnime(anitopy 路径)不产出流媒体平台;Baha 在 MP 平台表里,MetaVideo 路径同样会产出"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"A-02": {
|
"A-02": {
|
||||||
@@ -359,6 +359,12 @@
|
|||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEB-DL",
|
"ours": "WEB-DL",
|
||||||
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
|
},
|
||||||
|
"web_source": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "baha",
|
||||||
|
"note": "MetaAnime(anitopy 路径)不产出流媒体平台;Baha 在 MP 平台表里,MetaVideo 路径同样会产出"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"A-03": {
|
"A-03": {
|
||||||
@@ -772,36 +778,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-01": {
|
"MP-01": {
|
||||||
"name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "thelongseason",
|
|
||||||
"ours": "thelong",
|
|
||||||
"note": "\"Season 2017\" 被当成第 2017 季、吞掉年份;MP 自带用例期望 en=The Long Season y=2017"
|
|
||||||
},
|
|
||||||
"en_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "thelongseason",
|
|
||||||
"ours": "thelong",
|
|
||||||
"note": "\"Season 2017\" 被当成第 2017 季、吞掉年份;MP 自带用例期望 en=The Long Season y=2017"
|
|
||||||
},
|
|
||||||
"year": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "2017",
|
|
||||||
"ours": null,
|
|
||||||
"note": "\"Season 2017\" 被当成第 2017 季、吞掉年份;MP 自带用例期望 en=The Long Season y=2017"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "unknown",
|
|
||||||
"ours": "tv",
|
|
||||||
"note": "\"Season 2017\" 被当成第 2017 季、吞掉年份;MP 自带用例期望 en=The Long Season y=2017"
|
|
||||||
},
|
|
||||||
"begin_season": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": null,
|
|
||||||
"ours": "2017",
|
|
||||||
"note": "\"Season 2017\" 被当成第 2017 季、吞掉年份;MP 自带用例期望 en=The Long Season y=2017"
|
|
||||||
},
|
|
||||||
"resource_team": {
|
"resource_team": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
@@ -818,18 +794,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-03": {
|
"MP-03": {
|
||||||
"name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "欢迎来到实力至上主义的教室",
|
|
||||||
"ours": "★7月新番欢迎来到实力至上主义的教室",
|
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "欢迎来到实力至上主义的教室",
|
|
||||||
"ours": "★7月新番欢迎来到实力至上主义的教室",
|
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
|
||||||
},
|
|
||||||
"resource_team": {
|
"resource_team": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
@@ -838,57 +802,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-05": {
|
"MP-05": {
|
||||||
|
"name": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": "akibamaidsensou",
|
||||||
|
"ours": "秋叶原冥途战争",
|
||||||
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
|
},
|
||||||
|
"cn_name": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "秋叶原冥途战争",
|
||||||
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
|
},
|
||||||
"begin_season": {
|
"begin_season": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "1",
|
"ours": "1",
|
||||||
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
},
|
},
|
||||||
"resource_pix": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "1080p",
|
|
||||||
"ours": null,
|
|
||||||
"note": "方括号裸数字 [1080] 不识别为分辨率"
|
|
||||||
},
|
|
||||||
"resource_type": {
|
"resource_type": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEB-DL",
|
"ours": "WEB-DL",
|
||||||
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
},
|
|
||||||
"resource_team": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "leagueweb",
|
|
||||||
"ours": "秋叶原冥途战争",
|
|
||||||
"note": "开头方括号是片名而非组名,仍被当 bracket 组;MP 按组表命中 LeagueWEB"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MP-06": {
|
|
||||||
"name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "哆啦a梦大雄的宇宙小战争2021",
|
|
||||||
"ours": "哆啦梦大雄的宇宙小战争2021",
|
|
||||||
"note": "哆啦A梦 的拉丁字母 A 被拆出中文名成为英文名"
|
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "哆啦a梦大雄的宇宙小战争2021",
|
|
||||||
"ours": "哆啦梦大雄的宇宙小战争2021",
|
|
||||||
"note": "哆啦A梦 的拉丁字母 A 被拆出中文名成为英文名"
|
|
||||||
},
|
|
||||||
"en_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": null,
|
|
||||||
"ours": "a",
|
|
||||||
"note": "哆啦A梦 的拉丁字母 A 被拆出中文名成为英文名"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MP-10": {
|
|
||||||
"web_source": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "bglobal",
|
|
||||||
"ours": null,
|
|
||||||
"note": "流媒体平台表缺 B-Global"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-12": {
|
"MP-12": {
|
||||||
@@ -897,12 +833,6 @@
|
|||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEBRip",
|
"ours": "WEBRip",
|
||||||
"note": "MP 未识别方括号内 [WEBRip]"
|
"note": "MP 未识别方括号内 [WEBRip]"
|
||||||
},
|
|
||||||
"audio_encode": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "51",
|
|
||||||
"ours": null,
|
|
||||||
"note": "孤立声道 [5.1] 不识别为音频"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-13": {
|
"MP-13": {
|
||||||
@@ -918,32 +848,20 @@
|
|||||||
"ours": "诛仙",
|
"ours": "诛仙",
|
||||||
"note": "MP 丢中文名(/ 分隔或嵌套方括号)"
|
"note": "MP 丢中文名(/ 分隔或嵌套方括号)"
|
||||||
},
|
},
|
||||||
|
"resource_pix": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "2160p",
|
||||||
|
"note": "MetaAnime 不认方括号裸数字 [2160](同一路径认 [1080]);我们按分辨率识别"
|
||||||
|
},
|
||||||
"resource_type": {
|
"resource_type": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEB-DL",
|
"ours": "WEB-DL",
|
||||||
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
},
|
|
||||||
"resource_team": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "leagueweb",
|
|
||||||
"ours": "animations(动画片)",
|
|
||||||
"note": "开头方括号是片名而非组名,仍被当 bracket 组;MP 按组表命中 LeagueWEB"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-17": {
|
"MP-17": {
|
||||||
"name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "据幸存的六人所说",
|
|
||||||
"ours": "22年日剧据幸存的六人所说",
|
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "据幸存的六人所说",
|
|
||||||
"ours": "22年日剧据幸存的六人所说",
|
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
|
||||||
},
|
|
||||||
"begin_season": {
|
"begin_season": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
@@ -953,16 +871,16 @@
|
|||||||
},
|
},
|
||||||
"MP-18": {
|
"MP-18": {
|
||||||
"name": {
|
"name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "soredemoayumuwayosetekuru",
|
"mp": "soredemoayumuwayosetekuru",
|
||||||
"ours": "★7月新番即使如此依旧步步进逼",
|
"ours": "即使如此依旧步步进逼",
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
},
|
},
|
||||||
"cn_name": {
|
"cn_name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "★7月新番即使如此依旧步步进逼",
|
"ours": "即使如此依旧步步进逼",
|
||||||
"note": "前缀标签混入中文名;MP 丢中文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
},
|
},
|
||||||
"begin_season": {
|
"begin_season": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
@@ -973,31 +891,19 @@
|
|||||||
},
|
},
|
||||||
"MP-20": {
|
"MP-20": {
|
||||||
"name": {
|
"name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "swordquest",
|
"mp": "swordquest",
|
||||||
"ours": "国漫寻剑",
|
"ours": "寻剑",
|
||||||
"note": "动漫标题前缀标签(国漫)未剥离,且 name 取中文"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
},
|
},
|
||||||
"cn_name": {
|
"cn_name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "国漫寻剑",
|
"ours": "寻剑",
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-21": {
|
"MP-21": {
|
||||||
"name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "组长女儿与保姆",
|
|
||||||
"ours": "组长女儿与照料专员组长女儿与保姆",
|
|
||||||
"note": "\"A / B\" 两个中文名被拼接;MP 取后者"
|
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "组长女儿与保姆",
|
|
||||||
"ours": "组长女儿与照料专员组长女儿与保姆",
|
|
||||||
"note": "\"A / B\" 两个中文名被拼接;MP 取后者"
|
|
||||||
},
|
|
||||||
"begin_season": {
|
"begin_season": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
@@ -1022,56 +928,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-24": {
|
"MP-24": {
|
||||||
"name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "galtokyouryuu",
|
|
||||||
"ours": "bdrip",
|
|
||||||
"note": "[Name][02][BDRIP] 取到 BDRIP 作名称"
|
|
||||||
},
|
|
||||||
"en_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "galtokyouryuu",
|
|
||||||
"ours": "bdrip",
|
|
||||||
"note": "[Name][02][BDRIP] 取到 BDRIP 作名称"
|
|
||||||
},
|
|
||||||
"begin_season": {
|
"begin_season": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "1",
|
"ours": "1",
|
||||||
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
|
},
|
||||||
|
"resource_type": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "BluRay",
|
||||||
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
|
},
|
||||||
|
"resource_team": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": "gal to kyouryuu",
|
||||||
|
"ours": null,
|
||||||
|
"note": "MP 把开头的片名方括号 [Gal to Kyouryuu] 同时当成组名;我们判为片名(紧跟集号 [02])"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-25": {
|
"MP-25": {
|
||||||
"name": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "逆境無頼カイジ",
|
|
||||||
"ours": "逆境無頼カイジ13",
|
|
||||||
"note": "#13 集号写法不识别"
|
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "逆境無頼カイジ",
|
|
||||||
"ours": "逆境無頼カイジ13",
|
|
||||||
"note": "#13 集号写法不识别"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "tv",
|
|
||||||
"ours": "unknown",
|
|
||||||
"note": "#13 集号写法不识别"
|
|
||||||
},
|
|
||||||
"begin_season": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "1",
|
|
||||||
"ours": null,
|
|
||||||
"note": "#13 集号写法不识别"
|
|
||||||
},
|
|
||||||
"begin_episode": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "13",
|
|
||||||
"ours": null,
|
|
||||||
"note": "#13 集号写法不识别"
|
|
||||||
},
|
|
||||||
"resource_team": {
|
"resource_team": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
@@ -1080,43 +956,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-27": {
|
"MP-27": {
|
||||||
|
"name": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": "throneofseal",
|
||||||
|
"ours": "神印王座",
|
||||||
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
|
},
|
||||||
|
"cn_name": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "神印王座",
|
||||||
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
|
},
|
||||||
|
"resource_pix": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "2160p",
|
||||||
|
"note": "MetaAnime 不认方括号裸数字 [2160](同一路径认 [1080]);我们按分辨率识别"
|
||||||
|
},
|
||||||
"resource_type": {
|
"resource_type": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEB-DL",
|
"ours": "WEB-DL",
|
||||||
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
},
|
|
||||||
"resource_team": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "leagueweb",
|
|
||||||
"ours": "神印王座",
|
|
||||||
"note": "开头方括号是片名而非组名,仍被当 bracket 组;MP 按组表命中 LeagueWEB"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-29": {
|
"MP-29": {
|
||||||
"name": {
|
"begin_season": {
|
||||||
"kind": "missing",
|
"kind": "intentional",
|
||||||
"mp": "西部世界",
|
"mp": null,
|
||||||
"ours": "西部世界12",
|
"ours": "1",
|
||||||
"note": "\"中文名 12.mkv\" 文件名尾部裸数字不识别为集号"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "西部世界",
|
|
||||||
"ours": "西部世界12",
|
|
||||||
"note": "\"中文名 12.mkv\" 文件名尾部裸数字不识别为集号"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "tv",
|
|
||||||
"ours": "unknown",
|
|
||||||
"note": "\"中文名 12.mkv\" 文件名尾部裸数字不识别为集号"
|
|
||||||
},
|
|
||||||
"begin_episode": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "12",
|
|
||||||
"ours": null,
|
|
||||||
"note": "\"中文名 12.mkv\" 文件名尾部裸数字不识别为集号"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-30": {
|
"MP-30": {
|
||||||
@@ -1125,6 +995,12 @@
|
|||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEB-DL",
|
"ours": "WEB-DL",
|
||||||
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
|
},
|
||||||
|
"web_source": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "baha",
|
||||||
|
"note": "MetaAnime(anitopy 路径)不产出流媒体平台;Baha 在 MP 平台表里,MetaVideo 路径同样会产出"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-31": {
|
"MP-31": {
|
||||||
@@ -1143,16 +1019,16 @@
|
|||||||
},
|
},
|
||||||
"MP-32": {
|
"MP-32": {
|
||||||
"name": {
|
"name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "fightsbreakspherev",
|
"mp": "fightsbreakspherev",
|
||||||
"ours": "国漫斗破苍穹",
|
"ours": "斗破苍穹",
|
||||||
"note": "动漫标题前缀标签(国漫)未剥离,且 name 取中文"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
},
|
},
|
||||||
"cn_name": {
|
"cn_name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "国漫斗破苍穹",
|
"ours": "斗破苍穹",
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-34": {
|
"MP-34": {
|
||||||
@@ -1215,6 +1091,12 @@
|
|||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEB-DL",
|
"ours": "WEB-DL",
|
||||||
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
|
},
|
||||||
|
"web_source": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "baha",
|
||||||
|
"note": "MetaAnime(anitopy 路径)不产出流媒体平台;Baha 在 MP 平台表里,MetaVideo 路径同样会产出"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-41": {
|
"MP-41": {
|
||||||
@@ -1227,10 +1109,10 @@
|
|||||||
},
|
},
|
||||||
"MP-44": {
|
"MP-44": {
|
||||||
"name": {
|
"name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "isekaiojisan",
|
"mp": "isekaiojisan",
|
||||||
"ours": "异世界归来的舅舅",
|
"ours": "异世界归来的舅舅",
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
},
|
},
|
||||||
"cn_name": {
|
"cn_name": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
@@ -1238,55 +1120,31 @@
|
|||||||
"ours": "异世界归来的舅舅",
|
"ours": "异世界归来的舅舅",
|
||||||
"note": "MP 丢中文名"
|
"note": "MP 丢中文名"
|
||||||
},
|
},
|
||||||
"en_name": {
|
"begin_season": {
|
||||||
"kind": "our_bug",
|
"kind": "intentional",
|
||||||
"mp": "isekaiojisan",
|
"mp": null,
|
||||||
"ours": "isekaiojisan01",
|
"ours": "1",
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "tv",
|
|
||||||
"ours": "unknown",
|
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
|
||||||
},
|
|
||||||
"begin_episode": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "1",
|
|
||||||
"ours": null,
|
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-45": {
|
"MP-45": {
|
||||||
"name": {
|
"name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "summertimerendering",
|
"mp": "summertimerendering",
|
||||||
"ours": "★04月新番★夏日重現",
|
"ours": "夏日重現",
|
||||||
"note": "前缀标签混入中文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
},
|
},
|
||||||
"cn_name": {
|
"cn_name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "★04月新番★夏日重現",
|
"ours": "夏日重現",
|
||||||
"note": "前缀标签混入中文名;MP 丢中文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
},
|
},
|
||||||
"en_name": {
|
"begin_season": {
|
||||||
"kind": "our_bug",
|
"kind": "intentional",
|
||||||
"mp": "summertimerendering",
|
"mp": null,
|
||||||
"ours": "summertimerendering15",
|
"ours": "1",
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "tv",
|
|
||||||
"ours": "unknown",
|
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
|
||||||
},
|
|
||||||
"begin_episode": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "15",
|
|
||||||
"ours": null,
|
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-46": {
|
"MP-46": {
|
||||||
@@ -1305,56 +1163,12 @@
|
|||||||
"note": "未知发布组兜底(末段 -GROUP / 开头方括号),MP 组表未命中为 None"
|
"note": "未知发布组兜底(末段 -GROUP / 开头方括号),MP 组表未命中为 None"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-48": {
|
|
||||||
"audio_encode": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "DTSHDMA51",
|
|
||||||
"ours": "DTSHD",
|
|
||||||
"note": "DTS-HD MA5.1(MA 与声道粘连)只识别到 DTSHD"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MP-49": {
|
"MP-49": {
|
||||||
"audio_encode": {
|
"audio_encode": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "AC3",
|
"ours": "AC3",
|
||||||
"note": "AC3£cXcY:MP 未切开 £ 分隔,音频丢失"
|
"note": "AC3£cXcY:MP 未切开 £ 分隔,音频丢失"
|
||||||
},
|
|
||||||
"resource_team": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "cxcy@frds",
|
|
||||||
"ours": "frds",
|
|
||||||
"note": "£ 分隔的多组 cXcY@FRDS 只取到 FRDS"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MP-50": {
|
|
||||||
"begin_episode": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "13",
|
|
||||||
"ours": null,
|
|
||||||
"note": "S01 13-14 裸数字集区间不识别"
|
|
||||||
},
|
|
||||||
"end_episode": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "14",
|
|
||||||
"ours": null,
|
|
||||||
"note": "S01 13-14 裸数字集区间不识别"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MP-51": {
|
|
||||||
"end_episode": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "14",
|
|
||||||
"ours": null,
|
|
||||||
"note": "E13-14 只取到起始集"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MP-52": {
|
|
||||||
"type": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "unknown",
|
|
||||||
"ours": "tv",
|
|
||||||
"note": "开头下载日期 2022.8.2 被当成 airDate → 误判剧集"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-54": {
|
"MP-54": {
|
||||||
@@ -1363,12 +1177,6 @@
|
|||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "AC3",
|
"ours": "AC3",
|
||||||
"note": "AC3£cXcY:MP 未切开 £ 分隔,音频丢失"
|
"note": "AC3£cXcY:MP 未切开 £ 分隔,音频丢失"
|
||||||
},
|
|
||||||
"resource_team": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "cxcy@frds",
|
|
||||||
"ours": "frds",
|
|
||||||
"note": "£ 分隔的多组 cXcY@FRDS 只取到 FRDS"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-55": {
|
"MP-55": {
|
||||||
@@ -1385,10 +1193,10 @@
|
|||||||
"note": "MP 丢中文名(/ 分隔或嵌套方括号)"
|
"note": "MP 丢中文名(/ 分隔或嵌套方括号)"
|
||||||
},
|
},
|
||||||
"en_name": {
|
"en_name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "うたわれるもの",
|
"mp": "うたわれるもの",
|
||||||
"ours": "utawarerumonoうたわれるもの",
|
"ours": "utawarerumono",
|
||||||
"note": "三段 / 名称(中/罗马字/日文)未拆开"
|
"note": "MP 把 `/` 最后一段的日文假名名当英文名;我们取罗马字名,假名名只在没有英文名时兜底"
|
||||||
},
|
},
|
||||||
"begin_season": {
|
"begin_season": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
@@ -1415,44 +1223,36 @@
|
|||||||
"note": "flac_ac3 我们记两种音轨,MP 只记 FLAC"
|
"note": "flac_ac3 我们记两种音轨,MP 只记 FLAC"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-56": {
|
"MP-57": {
|
||||||
"name": {
|
"name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "欢迎来到实力至上主义的教室",
|
"mp": "jadedynasty",
|
||||||
"ours": "7月新番欢迎来到实力至上主义的教室",
|
"ours": "诛仙",
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
},
|
},
|
||||||
"cn_name": {
|
"cn_name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "欢迎来到实力至上主义的教室",
|
"mp": null,
|
||||||
"ours": "7月新番欢迎来到实力至上主义的教室",
|
"ours": "诛仙",
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"MP-57": {
|
"begin_season": {
|
||||||
"type": {
|
"kind": "intentional",
|
||||||
"kind": "missing",
|
"mp": null,
|
||||||
"mp": "tv",
|
"ours": "1",
|
||||||
"ours": "unknown",
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
"note": "[TV 04] 集号写法不识别"
|
|
||||||
},
|
},
|
||||||
"begin_episode": {
|
"resource_pix": {
|
||||||
"kind": "missing",
|
"kind": "mp_bug",
|
||||||
"mp": "4",
|
"mp": null,
|
||||||
"ours": null,
|
"ours": "2160p",
|
||||||
"note": "[TV 04] 集号写法不识别"
|
"note": "MetaAnime 不认方括号裸数字 [2160](同一路径认 [1080]);我们按分辨率识别"
|
||||||
},
|
},
|
||||||
"resource_type": {
|
"resource_type": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEB-DL",
|
"ours": "WEB-DL",
|
||||||
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
},
|
|
||||||
"resource_team": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "leagueweb",
|
|
||||||
"ours": "诛仙",
|
|
||||||
"note": "开头方括号是片名而非组名,仍被当 bracket 组;MP 按组表命中 LeagueWEB"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-58": {
|
"MP-58": {
|
||||||
@@ -1472,38 +1272,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-60": {
|
"MP-60": {
|
||||||
"en_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "zhuxianjadedynasty",
|
|
||||||
"ours": "hallofczhuxianjadedynasty",
|
|
||||||
"note": "开头 [Hall_of_C] 组名混入英文名"
|
|
||||||
},
|
|
||||||
"begin_season": {
|
|
||||||
"kind": "intentional",
|
|
||||||
"mp": null,
|
|
||||||
"ours": "1",
|
|
||||||
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MP-63": {
|
|
||||||
"name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "金属口红",
|
|
||||||
"ours": "★1月新番金属口红",
|
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "金属口红",
|
|
||||||
"ours": "★1月新番金属口红",
|
|
||||||
"note": "动漫标题前缀标签(★N月新番/N年日剧/国漫)未剥离,混入中文名"
|
|
||||||
},
|
|
||||||
"en_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "metallicrouge",
|
|
||||||
"ours": "metallicrougegb",
|
|
||||||
"note": "语言标记 GB 未剥离,进英文名"
|
|
||||||
},
|
|
||||||
"begin_season": {
|
"begin_season": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
@@ -1511,33 +1279,21 @@
|
|||||||
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
},
|
},
|
||||||
"resource_team": {
|
"resource_team": {
|
||||||
"kind": "our_bug",
|
"kind": "intentional",
|
||||||
"mp": "极影字幕社",
|
"mp": null,
|
||||||
"ours": null,
|
"ours": "hall_of_c",
|
||||||
"note": "【极影字幕社】全角方括号开头组未取到"
|
"note": "未知发布组兜底(末段 -GROUP / 开头方括号),MP 组表未命中为 None"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-64": {
|
"MP-63": {
|
||||||
"name": {
|
"begin_season": {
|
||||||
"kind": "missing",
|
"kind": "intentional",
|
||||||
"mp": "麦香",
|
"mp": null,
|
||||||
"ours": "maixiang",
|
"ours": "1",
|
||||||
"note": "英文名为拼音时从副标题取中文名(MP metavideo 规则)未迁"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "麦香",
|
|
||||||
"ours": null,
|
|
||||||
"note": "英文名为拼音时从副标题取中文名(MP metavideo 规则)未迁"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-73": {
|
"MP-73": {
|
||||||
"type": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "unknown",
|
|
||||||
"ours": "tv",
|
|
||||||
"note": "父目录 Collection.20240501 的日期被当 airDate,合并后误判剧集"
|
|
||||||
},
|
|
||||||
"audio_encode": {
|
"audio_encode": {
|
||||||
"kind": "representation",
|
"kind": "representation",
|
||||||
"mp": "2AUDIO",
|
"mp": "2AUDIO",
|
||||||
@@ -1547,66 +1303,24 @@
|
|||||||
},
|
},
|
||||||
"MP-74": {
|
"MP-74": {
|
||||||
"name": {
|
"name": {
|
||||||
"kind": "our_bug",
|
"kind": "intentional",
|
||||||
"mp": "caligulatheultimatecut",
|
"mp": "caligulatheultimatecut",
|
||||||
"ours": "罗马帝国艳情史最终剪辑版澳大利亚版",
|
"ours": "caligula",
|
||||||
"note": "[点分隔发布名][..] 形式:方括号内是完整发布名(MP BRACKET_DOT_TITLE_RE),未迁"
|
"note": "剪辑版本(Ultimate Cut 等)单独进 cut 字段,不留在名称里(REQ §6.1)"
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": null,
|
|
||||||
"ours": "罗马帝国艳情史最终剪辑版澳大利亚版",
|
|
||||||
"note": "[点分隔发布名][..] 形式:方括号内是完整发布名(MP BRACKET_DOT_TITLE_RE),未迁"
|
|
||||||
},
|
},
|
||||||
"en_name": {
|
"en_name": {
|
||||||
"kind": "our_bug",
|
"kind": "intentional",
|
||||||
"mp": "caligulatheultimatecut",
|
"mp": "caligulatheultimatecut",
|
||||||
"ours": "diy",
|
"ours": "caligula",
|
||||||
"note": "[点分隔发布名][..] 形式:方括号内是完整发布名(MP BRACKET_DOT_TITLE_RE),未迁"
|
"note": "剪辑版本(Ultimate Cut 等)单独进 cut 字段,不留在名称里(REQ §6.1)"
|
||||||
},
|
|
||||||
"year": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "2023",
|
|
||||||
"ours": null,
|
|
||||||
"note": "[点分隔发布名][..] 形式:方括号内是完整发布名(MP BRACKET_DOT_TITLE_RE),未迁"
|
|
||||||
},
|
|
||||||
"resource_pix": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "2160p",
|
|
||||||
"ours": null,
|
|
||||||
"note": "[点分隔发布名][..] 形式:方括号内是完整发布名(MP BRACKET_DOT_TITLE_RE),未迁"
|
|
||||||
},
|
|
||||||
"resource_type": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "UHD BluRay",
|
|
||||||
"ours": "UHD ?",
|
|
||||||
"note": "[点分隔发布名][..] 形式:方括号内是完整发布名(MP BRACKET_DOT_TITLE_RE),未迁"
|
|
||||||
},
|
|
||||||
"video_encode": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "H.265",
|
|
||||||
"ours": null,
|
|
||||||
"note": "[点分隔发布名][..] 形式:方括号内是完整发布名(MP BRACKET_DOT_TITLE_RE),未迁"
|
|
||||||
},
|
|
||||||
"audio_encode": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "DTSHDMA51",
|
|
||||||
"ours": null,
|
|
||||||
"note": "[点分隔发布名][..] 形式:方括号内是完整发布名(MP BRACKET_DOT_TITLE_RE),未迁"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MP-76": {
|
"MP-76": {
|
||||||
"name": {
|
"resource_team": {
|
||||||
"kind": "our_bug",
|
"kind": "intentional",
|
||||||
"mp": "theshawshankredemption",
|
"mp": null,
|
||||||
"ours": "ytsmxtheshawshankredemption",
|
"ours": "yts.mx",
|
||||||
"note": "[YTS.MX] 前缀未当作组名剥离"
|
"note": "未知发布组兜底(末段 -GROUP / 开头方括号),MP 组表未命中为 None"
|
||||||
},
|
|
||||||
"en_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "theshawshankredemption",
|
|
||||||
"ours": "ytsmxtheshawshankredemption",
|
|
||||||
"note": "[YTS.MX] 前缀未当作组名剥离"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Z-02": {
|
"Z-02": {
|
||||||
@@ -1663,6 +1377,12 @@
|
|||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "WEB-DL",
|
"ours": "WEB-DL",
|
||||||
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
"note": "MetaAnime(anitopy 路径)不产出来源/编码,我们产出"
|
||||||
|
},
|
||||||
|
"web_source": {
|
||||||
|
"kind": "mp_bug",
|
||||||
|
"mp": null,
|
||||||
|
"ours": "baha",
|
||||||
|
"note": "MetaAnime(anitopy 路径)不产出流媒体平台;Baha 在 MP 平台表里,MetaVideo 路径同样会产出"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Z-06": {
|
"Z-06": {
|
||||||
@@ -1675,10 +1395,10 @@
|
|||||||
},
|
},
|
||||||
"Z-07": {
|
"Z-07": {
|
||||||
"name": {
|
"name": {
|
||||||
"kind": "our_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "dungeonmeshi",
|
"mp": "dungeonmeshi",
|
||||||
"ours": "迷宫饭",
|
"ours": "迷宫饭",
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
"note": "MP 丢中文名(MetaAnime 取 `/` 后段,或把中英混排/纯中文片名块删掉),name 随之取英文名;我们保留中文名"
|
||||||
},
|
},
|
||||||
"cn_name": {
|
"cn_name": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
@@ -1686,23 +1406,11 @@
|
|||||||
"ours": "迷宫饭",
|
"ours": "迷宫饭",
|
||||||
"note": "MP 丢中文名"
|
"note": "MP 丢中文名"
|
||||||
},
|
},
|
||||||
"en_name": {
|
"begin_season": {
|
||||||
"kind": "our_bug",
|
"kind": "intentional",
|
||||||
"mp": "dungeonmeshi",
|
"mp": null,
|
||||||
"ours": "dungeonmeshi13",
|
"ours": "1",
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "tv",
|
|
||||||
"ours": "unknown",
|
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
|
||||||
},
|
|
||||||
"begin_episode": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "13",
|
|
||||||
"ours": null,
|
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Z-08": {
|
"Z-08": {
|
||||||
@@ -1730,18 +1438,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Z-12": {
|
"Z-12": {
|
||||||
"name": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "指环王三部曲",
|
|
||||||
"ours": "指环王三部曲合集",
|
|
||||||
"note": "\"合集\" 干扰词未剥离"
|
|
||||||
},
|
|
||||||
"cn_name": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "指环王三部曲",
|
|
||||||
"ours": "指环王三部曲合集",
|
|
||||||
"note": "\"合集\" 干扰词未剥离"
|
|
||||||
},
|
|
||||||
"resource_team": {
|
"resource_team": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
@@ -1814,29 +1510,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Z-27": {
|
"Z-27": {
|
||||||
"name": {
|
"begin_season": {
|
||||||
"kind": "our_bug",
|
"kind": "intentional",
|
||||||
"mp": "shingekinokyojin",
|
"mp": null,
|
||||||
"ours": "shingekinokyojin05ma10p",
|
"ours": "1",
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
},
|
|
||||||
"en_name": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "shingekinokyojin",
|
|
||||||
"ours": "shingekinokyojin05ma10p",
|
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "tv",
|
|
||||||
"ours": "unknown",
|
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
|
||||||
},
|
|
||||||
"begin_episode": {
|
|
||||||
"kind": "our_bug",
|
|
||||||
"mp": "5",
|
|
||||||
"ours": null,
|
|
||||||
"note": "名称后的方括号集号 [01] 未识别,拼进英文名"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Z-28": {
|
"Z-28": {
|
||||||
@@ -1855,62 +1533,12 @@
|
|||||||
"note": "name 选择:我们有中文名就取中文,MP 仅中文名全为汉字时取中文(cn/en 一致)"
|
"note": "name 选择:我们有中文名就取中文,MP 仅中文名全为汉字时取中文(cn/en 一致)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Z-31": {
|
|
||||||
"name": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "houseofthedragon",
|
|
||||||
"ours": "{tmdbid=94997type=tvs=1e=34}houseofthedragon",
|
|
||||||
"note": "{[tmdbid=..;type=tv;s=1;e=3-4]} 显式身份块未迁(find_metainfo)"
|
|
||||||
},
|
|
||||||
"en_name": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "houseofthedragon",
|
|
||||||
"ours": "{tmdbid=94997type=tvs=1e=34}houseofthedragon",
|
|
||||||
"note": "{[tmdbid=..;type=tv;s=1;e=3-4]} 显式身份块未迁(find_metainfo)"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "tv",
|
|
||||||
"ours": "unknown",
|
|
||||||
"note": "{[tmdbid=..;type=tv;s=1;e=3-4]} 显式身份块未迁(find_metainfo)"
|
|
||||||
},
|
|
||||||
"begin_season": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "1",
|
|
||||||
"ours": null,
|
|
||||||
"note": "{[tmdbid=..;type=tv;s=1;e=3-4]} 显式身份块未迁(find_metainfo)"
|
|
||||||
},
|
|
||||||
"begin_episode": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "3",
|
|
||||||
"ours": null,
|
|
||||||
"note": "{[tmdbid=..;type=tv;s=1;e=3-4]} 显式身份块未迁(find_metainfo)"
|
|
||||||
},
|
|
||||||
"end_episode": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "4",
|
|
||||||
"ours": null,
|
|
||||||
"note": "{[tmdbid=..;type=tv;s=1;e=3-4]} 显式身份块未迁(find_metainfo)"
|
|
||||||
},
|
|
||||||
"media_id": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "themoviedb:94997",
|
|
||||||
"ours": null,
|
|
||||||
"note": "{[tmdbid=..;type=tv;s=1;e=3-4]} 显式身份块未迁(find_metainfo)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Z-32": {
|
"Z-32": {
|
||||||
"begin_season": {
|
"begin_season": {
|
||||||
"kind": "intentional",
|
"kind": "intentional",
|
||||||
"mp": null,
|
"mp": null,
|
||||||
"ours": "1",
|
"ours": "1",
|
||||||
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
"note": "有集号无季号推定第 1 季(seasonInferred)"
|
||||||
},
|
|
||||||
"media_id": {
|
|
||||||
"kind": "missing",
|
|
||||||
"mp": "bangumi:400602",
|
|
||||||
"ours": null,
|
|
||||||
"note": "[bangumiid=..] 未迁(我们只认 tmdb/imdb/douban/tvdb)"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Z-33": {
|
"Z-33": {
|
||||||
|
|||||||
@@ -183,7 +183,14 @@ export function canonMp(m: MpMeta): Canon {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROVIDER: Record<string, string> = { tmdb: "themoviedb", douban: "douban", imdb: "imdb", tvdb: "tvdb" };
|
const PROVIDER: Record<string, string> = {
|
||||||
|
tmdb: "themoviedb",
|
||||||
|
douban: "douban",
|
||||||
|
imdb: "imdb",
|
||||||
|
tvdb: "tvdb",
|
||||||
|
bangumi: "bangumi",
|
||||||
|
anilist: "anilist",
|
||||||
|
};
|
||||||
|
|
||||||
export function oursParse(s: MetaSample): ParsedRelease {
|
export function oursParse(s: MetaSample): ParsedRelease {
|
||||||
return s.kind === "path"
|
return s.kind === "path"
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
{
|
{
|
||||||
"note": "繁→简差分已知差异:key=输入。kind 同 meta-deviations.json。",
|
"note": "繁→简差分已知差异:key=输入。kind 同 meta-deviations.json。",
|
||||||
"deviations": {
|
"deviations": {
|
||||||
"咒術迴戰": { "kind": "missing", "mp": "咒术回战", "ours": "咒术迴战", "note": "字表缺 迴→回" },
|
|
||||||
"孤獨搖滾": { "kind": "missing", "mp": "孤独摇滚", "ours": "孤独摇滾", "note": "字表缺 滾→滚" },
|
|
||||||
"魷魚遊戲": { "kind": "missing", "mp": "鱿鱼游戏", "ours": "魷鱼游戏", "note": "字表缺 魷→鱿" },
|
|
||||||
"瑯琊榜": { "kind": "missing", "mp": "琅琊榜", "ours": "瑯琊榜", "note": "字表缺 瑯→琅(异体)" },
|
|
||||||
"慶餘年": {
|
"慶餘年": {
|
||||||
"kind": "mp_bug",
|
"kind": "mp_bug",
|
||||||
"mp": "庆馀年",
|
"mp": "庆馀年",
|
||||||
"ours": "庆余年",
|
"ours": "庆余年",
|
||||||
"note": "MediaWiki 词表把 餘 转成 馀,官方简体片名为 庆余年"
|
"note": "MediaWiki 词表把 餘 转成 馀,官方简体片名为 庆余年"
|
||||||
},
|
},
|
||||||
"臥虎藏龍": { "kind": "missing", "mp": "卧虎藏龙", "ours": "臥虎藏龙", "note": "字表缺 臥→卧" },
|
"乾杯": {
|
||||||
"駭客任務": { "kind": "missing", "mp": "骇客任务", "ours": "駭客任务", "note": "字表缺 駭→骇" },
|
"kind": "intentional",
|
||||||
"乾杯": { "kind": "missing", "mp": "干杯", "ours": "乾杯", "note": "一对多字 乾 刻意不收;MP 有词组表" },
|
"mp": "干杯",
|
||||||
"瞭解": { "kind": "missing", "mp": "了解", "ours": "瞭解", "note": "一对多字 瞭 刻意不收;MP 有词组表" },
|
"ours": "乾杯",
|
||||||
"頭髮": { "kind": "missing", "mp": "头发", "ours": "头髮", "note": "髮→发 未收(发/發 合并字)" },
|
"note": "一对多字 乾 刻意不收;MP 有词组表(见 identity/zh-map.ts 注释)"
|
||||||
"理髮": { "kind": "missing", "mp": "理发", "ours": "理髮", "note": "髮→发 未收" }
|
},
|
||||||
|
"瞭解": {
|
||||||
|
"kind": "intentional",
|
||||||
|
"mp": "了解",
|
||||||
|
"ours": "瞭解",
|
||||||
|
"note": "一对多字 瞭 刻意不收;MP 有词组表(见 identity/zh-map.ts 注释)"
|
||||||
|
},
|
||||||
|
"頭髮": {
|
||||||
|
"kind": "intentional",
|
||||||
|
"mp": "头发",
|
||||||
|
"ours": "头髮",
|
||||||
|
"note": "一对多字 髮 刻意不收;MP 有词组表(见 identity/zh-map.ts 注释)"
|
||||||
|
},
|
||||||
|
"理髮": {
|
||||||
|
"kind": "intentional",
|
||||||
|
"mp": "理发",
|
||||||
|
"ours": "理髮",
|
||||||
|
"note": "一对多字 髮 刻意不收;MP 有词组表(见 identity/zh-map.ts 注释)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,8 +56,9 @@ const TITLES: Sample[] = [
|
|||||||
title: "奥本海默",
|
title: "奥本海默",
|
||||||
titleCn: "奥本海默",
|
titleCn: "奥本海默",
|
||||||
titleEn: "Oppenheimer",
|
titleEn: "Oppenheimer",
|
||||||
source: "BluRay",
|
// Blu-ray + HEVC 且没有 x265/H.265 → 原盘(对照 MP 内置规则 BLU)
|
||||||
isEncode: true,
|
source: "BluRay-Disc",
|
||||||
|
isEncode: false,
|
||||||
uhd: true,
|
uhd: true,
|
||||||
hdr: ["DV"],
|
hdr: ["DV"],
|
||||||
group: "MTeam",
|
group: "MTeam",
|
||||||
@@ -446,6 +447,180 @@ const TITLES: Sample[] = [
|
|||||||
{ id: "V-18", input: "某剧 第1-10集 1080p", expect: { episodes: { start: 1, end: 10 } } },
|
{ id: "V-18", input: "某剧 第1-10集 1080p", expect: { episodes: { start: 1, end: 10 } } },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// W5-D:按 MP 差分报告(docs/porting/mp-rust-logic-parity.md §2)补迁的写法,编号对应报告 B/M 条目
|
||||||
|
const MP_PARITY: Sample[] = [
|
||||||
|
{
|
||||||
|
id: "B1",
|
||||||
|
input:
|
||||||
|
"【爪爪字幕组】★7月新番[欢迎来到实力至上主义的教室 第二季/Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e S2][11][1080p][HEVC][GB][MP4]",
|
||||||
|
expect: { titleCn: "欢迎来到实力至上主义的教室", season: 2, episodes: { start: 11, end: 11 }, group: "爪爪字幕组" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B1b",
|
||||||
|
input: "[GM-Team][国漫][寻剑 第1季][Sword Quest Season 1][2002][02][AVC][GB][1080P]",
|
||||||
|
expect: { titleCn: "寻剑" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B1c",
|
||||||
|
input: "【幻月字幕组】【22年日剧】【据幸存的六人所说】【04】【1080P】",
|
||||||
|
expect: { titleCn: "据幸存的六人所说" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B2",
|
||||||
|
input: "[桜都字幕组] 异世界归来的舅舅 / Isekai Ojisan [01][1080p][简体内嵌]",
|
||||||
|
expect: { titleEn: "Isekai Ojisan", episodes: { start: 1, end: 1 }, mediaTypeHint: "tv" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B3",
|
||||||
|
input: "[秋叶原冥途战争][Akiba Maid Sensou][2022][WEB-DL][1080][TV Series][第01话][LeagueWEB]",
|
||||||
|
expect: { titleCn: "秋叶原冥途战争", resolution: "1080p", group: "LeagueWEB" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B3b",
|
||||||
|
input: "[某动画][Some Anime][2022][WEB-DL][1080p][TV 03][NewSub]",
|
||||||
|
expect: { titleCn: "某动画", episodes: { start: 3, end: 3 }, group: "NewSub", groupSource: "bracket" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B4",
|
||||||
|
input:
|
||||||
|
"[Caligula.The.Ultimate.Cut.2023.2160p.UHD.Blu-ray.HEVC.DTS-HD.MA.5.1-BHYS@OurBits][DIY中字原盘] [罗马帝国艳情史][91.86GB].iso",
|
||||||
|
expect: { titleEn: "Caligula", year: 2023, resolution: "2160p", source: "BluRay-Disc", cut: "Ultimate" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B5",
|
||||||
|
input: "The Long Season 2017 2160p WEB-DL H265 120FPS AAC-XXX",
|
||||||
|
expect: { titleEn: "The Long Season", year: 2017, season: undefined },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B6",
|
||||||
|
input: "2022.8.2.Twelve.Monkeys.1995.GBR.4K.REMASTERED.BluRay.1080p.X264.DTS",
|
||||||
|
expect: { titleEn: "Twelve Monkeys", year: 1995, airDate: undefined, mediaTypeHint: "unknown" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B7",
|
||||||
|
input: "[YTS.MX] The Shawshank Redemption 1994 1080p BluRay x264",
|
||||||
|
expect: { titleEn: "The Shawshank Redemption", group: "YTS.MX" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B8",
|
||||||
|
input: "[猎户不鸽发布组] 组长女儿与照料专员 / 组长女儿与保姆 Kumichou Musume to Sewagakari [09] [1080p+]",
|
||||||
|
expect: { titleCn: "组长女儿与保姆", group: "猎户不鸽发布组" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B8b",
|
||||||
|
input: "[xyx98]传颂之物/Utawarerumono/うたわれるもの[BDrip][1920x1080][TV 01-26 Fin]",
|
||||||
|
expect: { titleCn: "传颂之物", titleEn: "Utawarerumono", episodes: { start: 1, end: 26 } },
|
||||||
|
},
|
||||||
|
{ id: "B9", input: "哆啦A梦:大雄的宇宙小战争 2021 (2022) - 1080p.mp4", expect: { titleEn: undefined, year: 2022 } },
|
||||||
|
{
|
||||||
|
id: "B10",
|
||||||
|
input: "[Gal to Kyouryuu][02][BDRIP][1080P][H264_FLAC].mkv",
|
||||||
|
expect: { titleEn: "Gal to Kyouryuu", episodes: { start: 2, end: 2 }, releaseGroup: undefined },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B11",
|
||||||
|
input: "The 355 2022 BluRay 1080p DTS-HD MA5.1 X265.10bit-BeiTai",
|
||||||
|
expect: { audio: ["DTS-HD MA 5.1"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B11b",
|
||||||
|
input: "The Heart of Genius E13-14 2022 1080p WEB-DL H264 AAC",
|
||||||
|
expect: { episodes: { start: 13, end: 14 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "B11c",
|
||||||
|
input: "【极影字幕社】★1月新番 Metallic Rouge/金属口红 第13话 GB 1080P MP4",
|
||||||
|
expect: { titleEn: "Metallic Rouge", titleCn: "金属口红", group: "极影字幕社", subtitles: ["zh-Hans"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "M1",
|
||||||
|
input: "{[tmdbid=94997;type=tv;s=1;e=3-4]}House.of.the.Dragon.1080p.mkv",
|
||||||
|
expect: {
|
||||||
|
titleEn: "House of the Dragon",
|
||||||
|
explicitIds: [{ provider: "tmdb", id: "94997" }],
|
||||||
|
season: 1,
|
||||||
|
episodes: { start: 3, end: 4 },
|
||||||
|
mediaTypeHint: "tv",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "M2",
|
||||||
|
input: "Sousou no Frieren - 05 [1080p] [bangumiid=400602].mkv",
|
||||||
|
expect: { explicitIds: [{ provider: "bangumi", id: "400602" }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "M3a",
|
||||||
|
input: "[AI-Raws] 逆境無頼カイジ #13 (BD HEVC 1920x1080 FLAC)[7CFEE642].mkv",
|
||||||
|
expect: { titleCn: "逆境無頼カイジ", episodes: { start: 13, end: 13 } },
|
||||||
|
},
|
||||||
|
{ id: "M3b", input: "西部世界 12.mkv", expect: { titleCn: "西部世界", episodes: { start: 12, end: 12 } } },
|
||||||
|
{
|
||||||
|
id: "M3c",
|
||||||
|
input: "The Heart of Genius S01 13-14 2022 1080p WEB-DL H264 AAC",
|
||||||
|
expect: { season: 1, episodes: { start: 13, end: 14 } },
|
||||||
|
},
|
||||||
|
{ id: "M3d", input: "流浪地球 2.mkv", expect: { episodes: undefined } },
|
||||||
|
{
|
||||||
|
id: "M4",
|
||||||
|
input: "Mai Xiang S01 2019 2160p WEB-DL H.265 DDP2.0-HHWEB",
|
||||||
|
subtitle: "麦香 | 全36集 | 4K",
|
||||||
|
expect: { titleCn: "麦香", title: "麦香" },
|
||||||
|
},
|
||||||
|
{ id: "M4b", input: "Blade Runner 2017 1080p WEB-DL", subtitle: "银翼杀手 | 科幻", expect: { titleCn: undefined } },
|
||||||
|
{ id: "M5", input: "Thor Love and Thunder (2022) [1080p] [WEBRip] [5.1]", expect: { audio: ["5.1"] } },
|
||||||
|
{
|
||||||
|
id: "M6",
|
||||||
|
input: "Planet.Earth.III.S01.2160p.iP.WEB-DL.AAC2.0.HLG.H.265-GRP",
|
||||||
|
expect: { webSource: "BBC iPlayer" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "M6b",
|
||||||
|
input: "Some Anime S01E02 2022 1080p B-Global WEB-DL X264 AAC-AnimeS@ADWeb[2022年10月新番]",
|
||||||
|
expect: { webSource: "B-Global", titleEn: "Some Anime" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "M7",
|
||||||
|
input: "Sense8 S2E1 2015-2017 1080P WEB-DL X265 AC3£cXcY@FRDS",
|
||||||
|
expect: { group: "cXcY@FRDS", audio: ["AC3"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "M7b",
|
||||||
|
input: "指环王三部曲合集 The Lord of the Rings Trilogy 2001-2003 Extended 2160p UHD BluRay REMUX",
|
||||||
|
expect: { titleCn: "指环王三部曲" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "DISC",
|
||||||
|
input: "Some.Movie.2021.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-GRP",
|
||||||
|
expect: { source: "BluRay-Disc", isEncode: false },
|
||||||
|
},
|
||||||
|
{ id: "DISC-x264", input: "Some.Movie.2021.1080p.BluRay.AVC.x264-GRP", expect: { source: "BluRay", isEncode: true } },
|
||||||
|
{ id: "DISC-rip", input: "Some.Movie.2021.1080p.BDRip.AVC-GRP", expect: { source: "BluRay", isEncode: true } },
|
||||||
|
{
|
||||||
|
id: "SUB-latin",
|
||||||
|
input: "Oppenheimer.2023.2160p.BluRay.x265-GRP",
|
||||||
|
subtitle: "奥本海默 CHS/ENG",
|
||||||
|
expect: { subtitles: ["zh-Hans"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SUB-size",
|
||||||
|
input: "Oppenheimer.2023.2160p.BluRay.x265-GRP",
|
||||||
|
subtitle: "奥本海默 60 GB",
|
||||||
|
expect: { subtitles: undefined },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("MP 差分补迁写法", () => {
|
||||||
|
for (const s of MP_PARITY) {
|
||||||
|
it(`${s.id} ${s.input}`, () => check(parseRelease(s.input, s.subtitle ? { subtitle: s.subtitle } : {}), s.expect));
|
||||||
|
}
|
||||||
|
it("父目录只有日期不把电影合并成剧集", () => {
|
||||||
|
const r = parseReleasePath(
|
||||||
|
"/movies/Mixed.Collection.20240501.FRDS/12.Years.a.Slave.2013.BluRay.1080p.x265-FRDS/12.Years.a.Slave.2013.BluRay.1080p.x265-FRDS.mkv",
|
||||||
|
);
|
||||||
|
expect(r.mediaTypeHint).toBe("unknown");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("parseRelease 样本表", () => {
|
describe("parseRelease 样本表", () => {
|
||||||
it("样本数量 ≥ 60", () => {
|
it("样本数量 ≥ 60", () => {
|
||||||
expect(TITLES.length).toBeGreaterThanOrEqual(60);
|
expect(TITLES.length).toBeGreaterThanOrEqual(60);
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./chdbits.ts";
|
export * from "./chdbits.ts";
|
||||||
export * from "./hhanclub.ts";
|
export * from "./hhanclub.ts";
|
||||||
|
export * from "./subtitles.ts";
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* NexusPHP 字幕页 fixture(hhanclub 风格 div 网格,自行编写,**未对照真实页面**)。
|
||||||
|
* 与 `packages/adapters/test/differential` 的 `hhanclub-subtitles@inline` 样本同构,供 SiteSpider 字幕分支测试。
|
||||||
|
*/
|
||||||
|
const row = (i: number) =>
|
||||||
|
`<div class="grid"><div><img src="pic/flag/china.gif"></div><div>
|
||||||
|
<a href="downloadsubs.php?torrentid=${i}&subid=${1000 + i}">Example Show S01E0${i + 1} 1080p WEB-DL CHS</a>
|
||||||
|
<a href="https://hhanclub.net/userdetails.php?id=${i}"><b>tester${i}</b></a></div>
|
||||||
|
<div><div>111.99 KB</div></div><div><span title="2026-04-21 20:54:37">1月18天</span></div>
|
||||||
|
<div><a href="report.php?subtitle=${1000 + i}">举报</a></div></div>`;
|
||||||
|
|
||||||
|
export const NEXUSPHP_SUBTITLES_PAGE = `<!DOCTYPE html><html><head><title>字幕</title></head><body>
|
||||||
|
<div id="subtitles-table">${[0, 1, 2].map(row).join("")}<div class="grid"><div></div><div>没有下载链接的行</div></div></div>
|
||||||
|
</body></html>`;
|
||||||
|
|
||||||
|
/** 与上面 fixture 配套的字幕配置(MP `indexer.subtitles` 结构) */
|
||||||
|
export const NEXUSPHP_SUBTITLES_CONFIG = {
|
||||||
|
list: { selector: "#subtitles-table > div" },
|
||||||
|
fields: {
|
||||||
|
language_icon: { selector: "div:nth-child(1) img", attribute: "src" },
|
||||||
|
title: { selector: 'div:nth-child(2) a[href*="downloadsubs.php"]' },
|
||||||
|
download: { selector: 'div:nth-child(2) a[href*="downloadsubs.php"]', attribute: "href" },
|
||||||
|
size: { selector: "div:nth-child(3)" },
|
||||||
|
date_added: { selector: "div:nth-child(4) span", attribute: "title" },
|
||||||
|
date_elapsed: { selector: "div:nth-child(4) span" },
|
||||||
|
uploader: { selector: 'div:nth-child(2) a[href*="userdetails.php"]' },
|
||||||
|
report: { selector: 'div:nth-child(5) a[href*="report.php"]', attribute: "href" },
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -380,6 +380,20 @@
|
|||||||
"sha256": "e7952ed40b7b5a66d95fe1b07e1d3915601b47c2936ff10ec6880c8a030d0b8f",
|
"sha256": "e7952ed40b7b5a66d95fe1b07e1d3915601b47c2936ff10ec6880c8a030d0b8f",
|
||||||
"ours": ["packages/adapters/src/sites/user-parsers/types.ts"],
|
"ours": ["packages/adapters/src/sites/user-parsers/types.ts"],
|
||||||
"source": "tag"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -59,6 +59,16 @@
|
|||||||
},
|
},
|
||||||
{ "upstream": "app/modules/filemanager/transhandler.py", "ours": ["packages/adapters/src/fs/move.ts"] },
|
{ "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/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 差分样本"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -238,7 +238,7 @@ function convertNexus(y, file) {
|
|||||||
batch: { delimiter: " ", space_replace: "_" },
|
batch: { delimiter: " ", space_replace: "_" },
|
||||||
browse: { path: `${s.torrent_list_path}?page={page}`, start: Number(s.page_offset) === -1 ? 0 : 1 },
|
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") },
|
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: {
|
umt: {
|
||||||
version: `${id}@mc-${REF}`,
|
version: `${id}@mc-${REF}`,
|
||||||
kind: "nexusphp",
|
kind: "nexusphp",
|
||||||
@@ -343,6 +343,32 @@ function convertApi(y, file) {
|
|||||||
function isObj(v) {
|
function isObj(v) {
|
||||||
return v && typeof v === "object" && !Array.isArray(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 = "") {
|
function deepMerge(base, over, path = "") {
|
||||||
if (!isObj(base) || !isObj(over)) return over;
|
if (!isObj(base) || !isObj(over)) return over;
|
||||||
const out = { ...base };
|
const out = { ...base };
|
||||||
|
|||||||
Reference in new issue
Block a user