Files
UHD_Media_Tool/packages/domain/test/release-parser.test.ts
T
rootandClaude Fable 5.1 7ba712ea55 feat(domain): pure business logic for parsing, identity, slots, acquisition, paths, organize, download state (W1-2)
- release-parser: title/file/dir parsing with MP parity + deviations (airDate/issue,
  cut, release group suffix fallback, dir+file merge, safe episode offset)
- identity: title normalization (NFKC, trad->simp map, season suffix) and
  evidence-level candidate ranking (year tolerance ±1, no probabilities)
- slot: hard/soft/upgrade evaluation (missing vs unmet fields), state machine,
  movie/unit completion (season pack must be covered by actual file list)
- acquisition: new_title/catch_up/upgrade modes, priority order, budget,
  cache volume hard threshold with resume watermark (no double counting)
- path: immutable path bindings, restricted naming templates, sanitize,
  conflict detection, downloader<->host path mapping
- organize: planOrganize with sidecar pairing, sample exclusion, existing
  target policy, crossDevice marking, H&R -> conflict for move
- download-state: qB state mapping, completion, no-progress evidence

230 tests, ~97% statement coverage.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 18:11:29 +08:00

651 lines
25 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
cnToInt,
evalEpisodeOffset,
expandEpisodes,
fileKindOf,
OffsetExpressionError,
type ParsedRelease,
parseRelease,
parseReleasePath,
splitReleasePath,
} from "../src/release-parser";
type Expect = Partial<ParsedRelease> & { group?: string; groupSource?: string };
interface Sample {
id: string;
input: string;
subtitle?: string;
expect: Expect;
/** 与 MoviePilot 行为不同的地方,写明原因 */
deviation?: string;
}
function check(r: ParsedRelease, e: Expect) {
const { group, groupSource, ...rest } = e;
for (const [k, v] of Object.entries(rest)) {
expect(r[k as keyof ParsedRelease], `${r.raw} → ${k}`).toEqual(v);
}
if (group !== undefined) expect(r.releaseGroup?.name, `${r.raw} → group`).toBe(group);
if (groupSource !== undefined) expect(r.releaseGroup?.source).toBe(groupSource);
}
// M-xx 编号对应 docs/porting/moviepilot-metadata-recognition.md §9.1
const TITLES: Sample[] = [
{
id: "M-01",
input: "Oppenheimer.2023.2160p.UHD.BluRay.REMUX.DV.HDR.HEVC.TrueHD.7.1.Atmos-FraMeSToR",
expect: {
titleEn: "Oppenheimer",
year: 2023,
resolution: "2160p",
source: "REMUX",
isEncode: false,
videoCodec: "H.265",
hdr: ["DV", "HDR10"],
audio: ["TrueHD 7.1", "Atmos"],
group: "FraMeSToR",
confidence: "rule_confirmed",
},
deviation: "MP 发布组表无 FraMeSToR 时 grp=None;本项目种子表收录",
},
{
id: "M-02",
input: "奥本海默 Oppenheimer 2023 2160p UHD Blu-ray HEVC DV TrueHD 7.1-MTeam",
expect: {
title: "奥本海默",
titleCn: "奥本海默",
titleEn: "Oppenheimer",
source: "BluRay",
isEncode: true,
uhd: true,
hdr: ["DV"],
group: "MTeam",
groupSource: "known",
},
},
{
id: "M-03",
input: "The.Last.of.Us.S01.2023.1080p.WEB-DL.H264.DDP5.1.Atmos-ADWeb",
expect: {
titleEn: "The Last of Us",
season: 1,
episodes: undefined,
year: 2023,
audio: ["DDP 5.1", "Atmos"],
group: "ADWeb",
isSeasonPack: true,
packHint: "season_pack",
},
},
{
id: "M-04",
input: "The.Last.of.Us.S01E03.Long.Long.Time.2160p.MAX.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX",
expect: {
titleEn: "The Last of Us",
season: 1,
episodes: { start: 3, end: 3 },
webSource: "Max",
videoCodec: "H.265",
hdr: ["DV", "HDR10"],
packHint: "single",
},
},
{
id: "M-05",
input: "Breaking.Bad.S01-S05.Complete.1080p.BluRay.x264-ROVERS",
expect: { season: 1, seasonEnd: 5, packHint: "multi_season_pack", isSeasonPack: true, encoder: "x264" },
},
{
id: "M-06",
input: "庆余年 第二季 Joy of Life S02 2024 2160p WEB-DL H265 60fps DDP5.1-HHWEB",
expect: { titleCn: "庆余年", titleEn: "Joy of Life", season: 2, fps: 60, group: "HHWEB" },
},
{
id: "M-07",
input: "庆余年.Joy.of.Life.S02E01-E06.2024.2160p.WEB-DL.H265.AAC-ADWeb",
expect: { season: 2, episodes: { start: 1, end: 6 }, packHint: "multi_episode", isSeasonPack: false },
},
{
id: "M-08",
input:
"The.Lord.of.the.Rings.The.Fellowship.of.the.Ring.2001.Extended.Edition.2160p.UHD.BluRay.x265.10bit.HDR.DTS-HD.MA.6.1-CHD",
expect: {
titleEn: "The Lord of the Rings The Fellowship of the Ring",
cut: "Extended",
bitDepth: 10,
audio: ["DTS-HD MA 6.1"],
group: "CHD",
},
deviation: "MP 把 Extended.Edition 当干扰词删除;本项目保留为 cut",
},
{
id: "M-09",
input: "Kill.Bill.Vol.1.2003.Part.1.1080p.BluRay.x264-CHD",
expect: { titleEn: "Kill Bill Vol 1", year: 2003, part: "Part1" },
},
{
id: "M-10",
input: "Avatar.The.Way.of.Water.2022.2160p.WEB-DL.DDP5.1.Atmos.HDR10Plus.HEVC-CMCT",
expect: { hdr: ["HDR10+"], group: "CMCT", source: "WEB-DL" },
},
{
id: "M-11",
input: "奔跑吧 第十二季 20240426 第1期 1080p WEB-DL H264 AAC-HHWEB",
expect: {
titleCn: "奔跑吧",
season: 12,
issue: 1,
airDate: "20240426",
episodes: undefined,
packHint: "single",
mediaTypeHint: "tv",
},
deviation: "MP 把第1期当 episode、忽略 20240426;本项目 issue/airDate 分开保存",
},
{
id: "M-12",
input: "奔跑吧.Keep.Running.S12.2024.04.26.1080p.WEB-DL.H264.AAC-HHWEB",
expect: { season: 12, airDate: "20240426", year: undefined, isSeasonPack: false, packHint: "single" },
deviation: "MP 删除日期后判成整季包;本项目有日期即单期",
},
{
id: "M-13",
input: "向往的生活 第八季 全12期 2024 1080p WEB-DL",
expect: { titleCn: "向往的生活", season: 8, totalEpisodes: 12, year: 2024, isSeasonPack: true },
deviation: "MP 负向前瞻导致季号丢失",
},
{
id: "M-14",
input: "[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 12 [WebRip 1080p HEVC-10bit AAC][简繁日内封字幕]",
expect: {
titleCn: "葬送的芙莉莲",
titleEn: "Sousou no Frieren",
episodes: { start: 12, end: 12 },
season: 1,
seasonInferred: true,
group: "喵萌奶茶屋@LoliHouse",
bitDepth: 10,
source: "WEBRip",
subtitles: ["zh-Hans", "zh-Hant", "ja"],
confidence: "multi_evidence",
},
deviation: "MP 季号为空;本项目推定 1 并标 seasonInferred",
},
{
id: "M-15",
input: "Frieren.S01E12.1080p.NF.WEB-DL.DDP2.0.H.264-VARYG",
expect: { webSource: "Netflix", audio: ["DDP 2.0"], group: "VARYG", videoCodec: "H.264" },
},
{
id: "M-16",
input: "黑暗荣耀 The Glory 2022 S01 Complete 1080p NF WEB-DL x264 DDP5.1-HHWEB",
subtitle: "黑暗荣耀 第一季 全8集 宋慧乔",
expect: { season: 1, totalEpisodes: 8, isSeasonPack: true, confidence: "multi_evidence" },
},
{
id: "M-17",
input: "Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX.HDR.HEVC.Atmos-MTeam",
expect: { titleEn: "Dune Part Two", part: undefined, audio: ["Atmos"], source: "REMUX" },
},
{
id: "M-18",
input: "Some.Show.S02E05E06.1080p.WEB-DL-GRP",
expect: { season: 2, episodes: { start: 5, end: 6 }, group: "GRP", groupSource: "suffix" },
},
{
id: "M-19",
input: "狂飙 The Knockout 2023 全39集 1080p WEB-DL H264 AAC-OurTV",
expect: { season: 1, seasonInferred: true, totalEpisodes: 39, packHint: "season_pack" },
},
{
id: "M-20",
input: "Some.Show.2019.1080p.WEB-DL-GRP",
subtitle: "第1-10集 全10集",
expect: { episodes: { start: 1, end: 10 }, totalEpisodes: 10 },
},
{
id: "M-21",
input: "[Nekomoe kissaten][Oshi no Ko][01-11 Fin][1080p]",
expect: { titleEn: "Oshi no Ko", episodes: { start: 1, end: 11 }, season: 1, group: "Nekomoe kissaten" },
},
{ id: "M-22", input: "Show.Name.2019-2020.1080p.WEB-DL", expect: { titleEn: "Show Name", year: 2019 } },
// ---- 国际/观众/馒头风格电影
{
id: "X-01",
input: "Blade.Runner.2049.2017.1080p.BluRay.x264-SPARKS",
expect: { titleEn: "Blade Runner 2049", year: 2017, isEncode: true },
},
{
id: "X-02",
input: "Dune.Part.Two.2024.1080p.BluRay.x264.DTS-HD.MA.7.1-ADE",
expect: { group: "ADE", audio: ["DTS-HD MA 7.1"], isEncode: true },
},
{
id: "X-03",
input: "流浪地球2 The Wandering Earth II 2023 2160p UHD BluRay REMUX HEVC DV TrueHD 7.1 Atmos-MTeam",
expect: { titleCn: "流浪地球2", titleEn: "The Wandering Earth II", source: "REMUX" },
},
{
id: "X-04",
input: "Interstellar.2014.IMAX.1080p.BluRay.x264.DTS-HD.MA.5.1-HDS",
expect: { cut: "IMAX", group: "HDS", titleEn: "Interstellar" },
},
{
id: "X-05",
input: "Blade.Runner.1982.The.Final.Cut.2160p.UHD.BluRay.x265.10bit.HDR.DTS-HD.MA.5.1-CiNEPHiLES",
expect: { cut: "Final Cut", titleEn: "Blade Runner", year: 1982 },
},
{
id: "X-06",
input: "Apocalypse.Now.1979.Theatrical.Cut.1080p.BluRay.x264-AMIABLE",
expect: { cut: "Theatrical", group: "AMIABLE", groupSource: "suffix" },
deviation: "未知组 MP 为 None;本项目末段兜底",
},
{
id: "X-07",
input: "Oppenheimer.2023.1080p.BluRay.DD+5.1.x264-EbP",
expect: { audio: ["DDP 5.1"], group: "EbP" },
},
{
id: "X-08",
input: "Oppenheimer.2023.COMPLETE.UHD.BLURAY-SURCODE",
expect: { source: "BluRay-Disc", isEncode: false, group: "SURCODE" },
},
{
id: "X-09",
input: "Some.Movie.2021.BD50.1080p.AVC.DTS-HD.MA.5.1",
expect: { source: "BluRay-Disc", isEncode: false, videoCodec: "H.264" },
},
{ id: "X-10", input: "Some.Movie.2021.1080p.HDTV.x264-GRP", expect: { source: "HDTV" } },
{
id: "X-11",
input: "Some.Movie.2021.720p.WEBRip.x264.AAC-YTS",
expect: { source: "WEBRip", resolution: "720p", audio: ["AAC"] },
},
{ id: "X-12", input: "Some.Movie.2021.DVDRip.XviD-GRP", expect: { source: "DVDRip", videoCodec: "XviD" } },
{ id: "X-13", input: "Movie.2020.3840x2160.WEB-DL.HEVC", expect: { resolution: "2160p", titleEn: "Movie" } },
{ id: "X-14", input: "Movie.2020.4K.WEB-DL.HEVC-GRP", expect: { resolution: "2160p" } },
{
id: "X-15",
input: "Mission.Impossible.Dead.Reckoning.Part.One.2023.2160p.UHD.BluRay.x265.10bit.HDR.TrueHD.7.1.Atmos-MTeam",
expect: { titleEn: "Mission Impossible Dead Reckoning Part One", part: undefined, audio: ["TrueHD 7.1", "Atmos"] },
},
{
id: "X-16",
input: "Harry.Potter.and.the.Deathly.Hallows.Part.2.2011.1080p.BluRay.x264-SPARKS",
expect: { titleEn: "Harry Potter and the Deathly Hallows Part 2", year: 2011 },
},
{
id: "X-17",
input: "Movie.Title.2021.1080p.BluRay.Remux.AVC.DTS-HD.MA.5.1-FGT",
expect: { source: "REMUX", isEncode: false },
},
{ id: "X-18", input: "The.Matrix.1999.Remastered.1080p.BluRay.x264-GRP", expect: { cut: "Remastered" } },
{ id: "X-19", input: "Movie.2020.Unrated.1080p.BluRay.x264-GRP", expect: { cut: "Unrated" } },
{
id: "X-20",
input: "Movie.Name.2019.Directors.Cut.1080p.BluRay.x264-FOO",
expect: { cut: "Director's Cut", titleEn: "Movie Name" },
},
{
id: "X-21",
input: "银翼杀手 导演剪辑版 1982 1080p BluRay x264",
expect: { titleCn: "银翼杀手", cut: "Director's Cut", year: 1982 },
},
{
id: "X-22",
input: "让子弹飞.Let.the.Bullets.Fly.2010.1080p.BluRay.x264.DTS-CMCT",
expect: { titleCn: "让子弹飞", titleEn: "Let the Bullets Fly", audio: ["DTS"], group: "CMCT" },
},
{
id: "X-23",
input: "长安三万里.Chang.An.2023.2160p.WEB-DL.H265.HDR.DDP5.1-OurTV",
expect: { titleCn: "长安三万里", hdr: ["HDR10"], group: "OurTV" },
},
{
id: "X-24",
input: "Movie.2020.1080p.BluRay.x264-GRP 12.5GB",
expect: { sizeBytes: Math.round(12.5 * 1024 ** 3), group: "GRP" },
},
{ id: "X-25", input: "Movie 2020 1080p WEB-DL 中英字幕", expect: { subtitles: ["zh", "en"] } },
{ id: "X-26", input: "Movie 2020 1080p 国语中字 WEB-DL", expect: { subtitles: ["zh"], titleEn: "Movie" } },
{
id: "X-27",
input: "[简繁英字幕] Movie 2020 1080p WEB-DL",
expect: { subtitles: ["zh-Hans", "zh-Hant", "en"], releaseGroup: undefined },
},
{ id: "X-28", input: "The Movie 2021", expect: { titleEn: "The Movie", year: 2021, confidence: "rule_confirmed" } },
{ id: "X-29", input: "The Movie 1080p WEB-DL", expect: { year: undefined, confidence: "needs_confirm" } },
// ---- 剧集
{
id: "T-01",
input: "三体 Three-Body S01E05 2023 1080p WEB-DL H264 AAC-OurTV",
expect: { titleCn: "三体", titleEn: "Three Body", season: 1, episodes: { start: 5, end: 5 } },
},
{
id: "T-02",
input: "Loki.S02E01.2160p.DSNP.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX",
expect: { webSource: "Disney+", hdr: ["DV", "HDR10"] },
},
{
id: "T-03",
input: "The.Bear.S03.1080p.DSNP.WEB-DL.DDP5.1.H.264-MWeb",
expect: { season: 3, isSeasonPack: true, group: "MWeb" },
},
{
id: "T-04",
input: "Beyond.the.Clouds.2024.S01.1080p.WEB-DL.H264.AAC-ADWeb",
expect: { titleEn: "Beyond the Clouds", year: 2024, season: 1 },
},
{
id: "T-05",
input: "The.Office.US.S05E13-E14.720p.HDTV.x264",
expect: { titleEn: "The Office US", episodes: { start: 13, end: 14 }, source: "HDTV" },
},
{ id: "T-06", input: "Friends.1x05.720p", expect: { season: 1, episodes: { start: 5, end: 5 } } },
{ id: "T-07", input: "Show.Season.2.1080p.BluRay.x264-GRP", expect: { season: 2, isSeasonPack: true } },
{
id: "T-08",
input: "Show.S01E01.Pilot.1080p.WEB-DL.mkv",
expect: { isFile: true, fileKind: "video", extension: "mkv", titleEn: "Show", packHint: "single" },
},
{ id: "T-09", input: "Show.S01E01.chs.srt", expect: { fileKind: "subtitle", subtitles: ["zh-Hans"] } },
{ id: "T-10", input: "Show.S01E01E02E03.1080p.WEB-DL", expect: { episodes: { start: 1, end: 3 } } },
{ id: "T-11", input: "Show.Name.S01.E01.1080p", expect: { season: 1, episodes: { start: 1, end: 1 } } },
{ id: "T-12", input: "Show.S01D02.1080p.BluRay", expect: { season: 1, part: "Disc2" } },
{
id: "T-13",
input: "Planet.Earth.III.S01.2160p.iP.WEB-DL.AAC2.0.HLG.H.265-GRP",
expect: { titleEn: "Planet Earth III", hdr: ["HLG"], audio: ["AAC 2.0"] },
},
{ id: "T-14", input: "天下长河 第1集 1080p WEB-DL", expect: { titleCn: "天下长河", segment: undefined } },
{
id: "T-15",
input: "庆余年 第二季 第1集 范闲入京 1080p WEB-DL",
expect: { titleCn: "庆余年", season: 2, episodes: { start: 1, end: 1 } },
},
{
id: "T-16",
input: "[tmdbid=94997] House.of.the.Dragon.S01E03.1080p.mkv",
expect: { explicitIds: [{ provider: "tmdb", id: "94997" }], season: 1, titleEn: "House of the Dragon" },
},
{ id: "T-17", input: "[tmdbid=0]Foo.2020", expect: { explicitIds: [], titleEn: "Foo" } },
// ---- 动漫
{
id: "A-01",
input: "[ANi] 葬送的芙莉蓮 - 13 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4",
expect: { group: "ANi", episodes: { start: 13, end: 13 }, subtitles: ["zh-Hant"], titleCn: "葬送的芙莉蓮" },
},
{
id: "A-02",
input: "[Lilith-Raws] Sousou no Frieren - 01 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]",
expect: { group: "Lilith-Raws", titleEn: "Sousou no Frieren", episodes: { start: 1, end: 1 } },
},
{
id: "A-03",
input: "[UnknownSub][Some Anime][05][1080p]",
expect: { group: "UnknownSub", groupSource: "bracket", episodes: { start: 5, end: 5 }, titleEn: "Some Anime" },
},
{
id: "A-04",
input: "[Sub] Show - 26 Fin [1080p]",
expect: { episodes: { start: 26, end: 26 } },
},
{ id: "A-05", input: "Show 01-26 Fin 1080p", expect: { episodes: { start: 1, end: 26 }, isSeasonPack: true } },
// ---- 综艺
{
id: "V-01",
input: "快乐大本营 20210903 1080p WEB-DL H264 AAC-HHWEB",
expect: { airDate: "20210903", season: undefined, confidence: "needs_confirm", packHint: "single" },
},
{
id: "V-02",
input: "乘风2024 第5期 下 20240524 1080p WEB-DL",
expect: { titleCn: "乘风2024", issue: 5, segment: "lower", airDate: "20240524" },
},
{
id: "V-03",
input: "披荆斩棘 第三季 第12期 加更版 1080p WEB-DL-HHWEB",
expect: { season: 3, issue: 12, contentType: "extra", titleCn: "披荆斩棘" },
},
{
id: "V-04",
input: "歌手2024 第1期 会员版 1080p",
expect: { variantTags: ["会员版"], confidence: "needs_confirm", issue: 1 },
},
{
id: "V-05",
input: "声生不息 纯享版 20220501 1080p WEB-DL",
expect: { contentType: "performance_only", airDate: "20220501" },
},
{
id: "V-06",
input: "The.Voice.2024.05.03.1080p.WEB.h264-GRP",
expect: { titleEn: "The Voice", airDate: "20240503", source: "WEB-DL", year: undefined },
},
{
id: "V-07",
input: "Late.Night.2024.05.03.Guest.Name.1080p.WEB.h264-GRP",
expect: { titleEn: "Late Night", airDate: "20240503" },
},
{
id: "V-08",
input: "极限挑战 第十季 20240505 第2期 上 1080p WEB-DL",
expect: { season: 10, issue: 2, segment: "upper" },
},
{ id: "V-09", input: "声生不息 第2期(上) 1080p", expect: { issue: 2, segment: "upper" } },
{ id: "V-10", input: "奔跑吧 第十二季 第3期 下 1080p", expect: { issue: 3, segment: "lower", season: 12 } },
{ id: "V-11", input: "某综艺 2024年5月3日 完整版 1080p", expect: { airDate: "20240503", segment: "full" } },
{ id: "V-12", input: "某综艺 第1期 花絮", expect: { contentType: "behind_the_scenes" } },
{ id: "V-13", input: "某综艺 先导片", expect: { contentType: "preview" } },
{ id: "V-14", input: "某综艺 第3期 陪看", expect: { contentType: "reaction" } },
{ id: "V-15", input: "某综艺 第3期 正片", expect: { contentType: "main" } },
{ id: "V-16", input: "某剧 全3季 1080p", expect: { season: 1, seasonEnd: 3, packHint: "multi_season_pack" } },
{ id: "V-17", input: "某剧 第1-3季 1080p", expect: { season: 1, seasonEnd: 3 } },
{ id: "V-18", input: "某剧 第1-10集 1080p", expect: { episodes: { start: 1, end: 10 } } },
];
describe("parseRelease 样本表", () => {
it("样本数量 ≥ 60", () => {
expect(TITLES.length).toBeGreaterThanOrEqual(60);
});
for (const s of TITLES) {
it(`${s.id} ${s.input}${s.deviation ? ` [deviation: ${s.deviation}]` : ""}`, () => {
const r = parseRelease(s.input, s.subtitle ? { subtitle: s.subtitle } : {});
check(r, s.expect);
expect(r.parserVersion).toBe(1);
expect(r.raw).toBe(s.input);
});
}
});
// P-xx 对应迁移文档 §9.2
const PATHS: Sample[] = [
{
id: "P-01",
input: "/downloads/The.Last.of.Us.S01.2023.1080p.WEB-DL-ADWeb/The.Last.of.Us.S01E03.1080p.WEB-DL-ADWeb.mkv",
expect: { season: 1, episodes: { start: 3, end: 3 }, year: 2023, confidence: "multi_evidence" },
},
{
id: "P-02",
input: "/downloads/庆余年 第二季 Joy of Life S02 2024 2160p WEB-DL H265 DDP5.1-HHWEB/01.mp4",
expect: { title: "庆余年", season: 2, year: 2024, episodes: { start: 1, end: 1 }, seasonInferred: undefined },
},
{
id: "P-03",
input: "/downloads/庆余年 第二季 Joy of Life S02 2024 2160p WEB-DL H265 DDP5.1-HHWEB/第02集.mkv",
expect: { episodes: { start: 2, end: 2 }, season: 2, group: "HHWEB", audio: ["DDP 5.1"] },
},
{
id: "P-04",
input: "/downloads/Breaking.Bad.S01-S05.1080p.BluRay.x264-ROVERS/S03/07.mkv",
expect: { title: "Breaking Bad", season: 3, episodes: { start: 7, end: 7 }, seasonEnd: undefined },
},
{
id: "P-05",
input: "/d/Oppenheimer.2023.2160p.UHD.BluRay.REMUX-FraMeSToR/Featurettes/Making.Of.mkv",
expect: { title: "Oppenheimer", contentType: "extra", confidence: "needs_confirm" },
deviation: "MP 把附加内容识别成名为 Making Of 的电影",
},
{
id: "P-07",
input: "/downloads/The Glory 2022 S01 1080p NF WEB-DL-HHWEB/简体.srt",
expect: { auxiliaryOnly: true, title: "The Glory", confidence: "needs_confirm", fileKind: "subtitle" },
deviation: "MP 取到根目录名 Downloads",
},
{
id: "P-08",
input: "/downloads/奔跑吧 第十二季 1080p WEB-DL-HHWEB/奔跑吧.20240426.第1期.mp4",
expect: { season: 12, issue: 1, airDate: "20240426", episodes: undefined },
},
{
id: "P-09",
input: "/downloads/奔跑吧 第十二季 1080p WEB-DL-HHWEB/奔跑吧.2024.05.03.mp4",
expect: { mediaTypeHint: "tv", season: 12, airDate: "20240503", year: undefined, confidence: "multi_evidence" },
deviation: "MP 文件无类型、不与 TV 目录合并",
},
{
id: "P-10",
input: "/downloads/奔跑吧 第十二季 1080p WEB-DL-HHWEB/奔跑吧.20240503.加更版.mp4",
expect: { contentType: "extra", airDate: "20240503", season: 12 },
},
{
id: "P-11",
input: "/downloads/Kill.Bill.2003.1080p.BluRay.x264-CHD/CD1.mkv",
expect: { part: "CD1", title: "Kill Bill", year: 2003 },
deviation: "MP 因 Part 门槛丢失 CD1",
},
{
id: "P-12",
input: "/media/tv/庆余年 (2019)/Season 2/庆余年 - S02E03 - 第 3 集.mkv",
expect: { season: 2, episodes: { start: 3, end: 3 }, year: 2019 },
},
{
id: "P-13",
input:
"/dl/[LoliHouse] Sousou no Frieren [01-28][WebRip 1080p]/[LoliHouse] Sousou no Frieren - 13 [WebRip 1080p].mkv",
expect: { episodes: { start: 13, end: 13 }, titleEn: "Sousou no Frieren" },
},
{
id: "P-14",
input: "/dl/Some.Show.S01.1080p/Making.Of.Special.mkv",
expect: { mediaTypeHint: "tv", season: 1, confidence: "needs_confirm" },
deviation: "文件无剧集信号、目录是剧集:合并但降级为 needs_confirm",
},
{
id: "P-15",
input: "/dl/Collection.1080p/The.Matrix.Reloaded.2003.1080p.BluRay.x264.mkv",
expect: { year: 2003, confidence: "rule_confirmed" },
},
{
id: "P-16",
input: "/dl/The.Matrix.1999.1080p/The.Matrix.Reloaded.2003.1080p.mkv",
expect: { year: 2003, confidence: "needs_confirm" },
},
{
id: "P-17",
input: "/dl/Show.S01.1080p.WEB-DL-GRP/Sample/show.s01e01.sample.mkv",
expect: { season: 1 },
},
];
describe("parseReleasePath 目录 + 文件名合并", () => {
for (const s of PATHS) {
it(`${s.id} ${s.input}${s.deviation ? ` [deviation: ${s.deviation}]` : ""}`, () => {
check(parseReleasePath(s.input), s.expect);
});
}
it("接受已拆分的路径段", () => {
const r = parseReleasePath({ file: "E05.mkv", parent: "Show.S02.1080p.WEB-DL-GRP" });
expect(r.season).toBe(2);
expect(r.episodes).toEqual({ start: 5, end: 5 });
expect(r.title).toBe("Show");
});
it("sample 目录打证据", () => {
const r = parseReleasePath("/dl/Show.S01/Sample/x.mkv");
expect(r.evidence).toContain("dir:sample");
});
it("splitReleasePath 跳过附加内容目录", () => {
expect(splitReleasePath("/a/b/Extras/c.mkv")).toEqual({
file: "c.mkv",
parent: "b",
grandParent: "a",
extrasDir: "Extras",
});
expect(splitReleasePath("c.mkv")).toEqual({ file: "c.mkv" });
});
});
describe("集号偏移(安全求值)", () => {
it("W-03: 按表达式偏移集号", () => {
const r = parseRelease("[Sub] Kimetsu no Yaiba - 45 [1080p].mkv", { episodeOffset: "EP-44" });
expect(r.episodes).toEqual({ start: 1, end: 1 });
expect(r.evidence).toContain("episode:offset");
expect(r.confidence).toBe("multi_evidence");
});
it("负结果被拒绝,保留原集号", () => {
const r = parseRelease("Show.S01E03.1080p", { episodeOffset: "-10" });
expect(r.episodes).toEqual({ start: 3, end: 3 });
expect(r.evidence).toContain("episode:offset_rejected");
});
it("各运算符", () => {
expect(evalEpisodeOffset("EP-12", 13)).toBe(1);
expect(evalEpisodeOffset("+1", 1)).toBe(2);
expect(evalEpisodeOffset("*2", 3)).toBe(6);
expect(evalEpisodeOffset("5", 3)).toBe(8);
expect(evalEpisodeOffset("(EP+1)//2", 5)).toBe(3);
expect(evalEpisodeOffset("EP%4", 10)).toBe(2);
expect(evalEpisodeOffset("EP/2", 5)).toBe(2);
expect(evalEpisodeOffset("2**3+ep", 1)).toBe(9);
expect(evalEpisodeOffset("-EP+20", 5)).toBe(15);
expect(evalEpisodeOffset("+(EP)", 5)).toBe(5);
});
it("拒绝非白名单输入", () => {
for (const bad of ["", "EP; process.exit()", "EP+", "(EP+1", "EP/0", "EP 1", "a".repeat(80), "EP)", "2**2000"]) {
expect(() => evalEpisodeOffset(bad, 1), bad).toThrow(OffsetExpressionError);
}
});
});
describe("辅助函数", () => {
it("cnToInt", () => {
expect(cnToInt("十二")).toBe(12);
expect(cnToInt("二十")).toBe(20);
expect(cnToInt("二十三")).toBe(23);
expect(cnToInt("一百零五")).toBe(105);
expect(cnToInt("两")).toBe(2);
expect(cnToInt("08")).toBe(8);
expect(cnToInt("abc")).toBeUndefined();
expect(cnToInt("")).toBeUndefined();
});
it("fileKindOf", () => {
expect(fileKindOf("a.MKV")).toBe("video");
expect(fileKindOf("a.ass")).toBe("subtitle");
expect(fileKindOf("a.mka")).toBe("audio");
expect(fileKindOf("a.nfo")).toBe("nfo");
expect(fileKindOf("a.jpg")).toBe("image");
expect(fileKindOf("a.txt")).toBe("other");
expect(fileKindOf("a")).toBeUndefined();
});
it("expandEpisodes", () => {
expect(expandEpisodes({ start: 2, end: 4 })).toEqual([2, 3, 4]);
expect(expandEpisodes(undefined)).toEqual([]);
});
it("自定义发布组扩展", () => {
const r = parseRelease("Movie.2020.1080p.WEB-DL-MyGroup", { releaseGroups: ["MyGroup"] });
expect(r.releaseGroup).toEqual({ name: "MyGroup", source: "known" });
});
it("纯数字/Exx 文件名", () => {
expect(parseRelease("05.mkv").episodes).toEqual({ start: 5, end: 5 });
expect(parseRelease("E07.mkv").episodes).toEqual({ start: 7, end: 7 });
expect(parseRelease("EP07.mp4").episodes).toEqual({ start: 7, end: 7 });
expect(parseRelease("S03").season).toBe(3);
expect(parseRelease("Season 2").season).toBe(2);
});
it("isFile 选项", () => {
expect(parseRelease("12", { isFile: true }).episodes).toEqual({ start: 12, end: 12 });
expect(parseRelease("12").episodes).toBeUndefined();
});
});