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]>
This commit is contained in:
1 parent
a6d049db81
commit
7ba712ea55
40 files changed
+5885
-2
No files matched your search
@@ -0,0 +1,82 @@
|
||||
/** 附属文件(字幕/外挂音轨)的语言标签,按主干名剥离后与主视频配对 */
|
||||
const LANG_TAG: Record<string, "zh-Hans" | "zh-Hant" | "en" | "ja" | "ko" | "flag"> = {
|
||||
chs: "zh-Hans",
|
||||
sc: "zh-Hans",
|
||||
gb: "zh-Hans",
|
||||
chi: "zh-Hans",
|
||||
zho: "zh-Hans",
|
||||
zh: "zh-Hans",
|
||||
"zh-cn": "zh-Hans",
|
||||
"zh-hans": "zh-Hans",
|
||||
"zh-sg": "zh-Hans",
|
||||
简体: "zh-Hans",
|
||||
简中: "zh-Hans",
|
||||
简: "zh-Hans",
|
||||
cht: "zh-Hant",
|
||||
tc: "zh-Hant",
|
||||
big5: "zh-Hant",
|
||||
"zh-tw": "zh-Hant",
|
||||
"zh-hk": "zh-Hant",
|
||||
"zh-hant": "zh-Hant",
|
||||
繁体: "zh-Hant",
|
||||
繁體: "zh-Hant",
|
||||
繁中: "zh-Hant",
|
||||
繁: "zh-Hant",
|
||||
eng: "en",
|
||||
en: "en",
|
||||
english: "en",
|
||||
jpn: "ja",
|
||||
ja: "ja",
|
||||
jp: "ja",
|
||||
kor: "ko",
|
||||
ko: "ko",
|
||||
default: "flag",
|
||||
forced: "flag",
|
||||
sdh: "flag",
|
||||
cc: "flag",
|
||||
简英: "zh-Hans",
|
||||
繁英: "zh-Hant",
|
||||
中英: "zh-Hans",
|
||||
双语: "zh-Hans",
|
||||
简繁: "zh-Hans",
|
||||
};
|
||||
|
||||
export const basename = (p: string) => p.replace(/\\/g, "/").split("/").pop() ?? p;
|
||||
export const dirname = (p: string) => {
|
||||
const s = p.replace(/\\/g, "/");
|
||||
const i = s.lastIndexOf("/");
|
||||
return i <= 0 ? (i === 0 ? "/" : "") : s.slice(0, i);
|
||||
};
|
||||
export const splitExt = (name: string): [string, string] => {
|
||||
const i = name.lastIndexOf(".");
|
||||
return i > 0 ? [name.slice(0, i), name.slice(i + 1)] : [name, ""];
|
||||
};
|
||||
|
||||
/** 字幕主干名:去扩展名后逐级剥掉 .chs/.eng/.default 等语言标签,返回小写主干与剥下的标签 */
|
||||
export function sidecarStem(name: string): { stem: string; tags: string[] } {
|
||||
let [stem] = splitExt(basename(name));
|
||||
const tags: string[] = [];
|
||||
for (;;) {
|
||||
const i = stem.lastIndexOf(".");
|
||||
if (i <= 0) break;
|
||||
const tag = stem.slice(i + 1).toLowerCase();
|
||||
if (!(tag in LANG_TAG)) break;
|
||||
tags.unshift(tag);
|
||||
stem = stem.slice(0, i);
|
||||
}
|
||||
return { stem: stem.toLowerCase(), tags };
|
||||
}
|
||||
|
||||
/** 目标字幕后缀(对照 MP:繁体优先判定 → .zh-tw;简体 → .chi.zh-cn;英 → .eng;日 → .ja) */
|
||||
export function sidecarLanguageSuffix(tags: readonly string[]): string {
|
||||
const langs = tags.map((t) => LANG_TAG[t]);
|
||||
let s = "";
|
||||
if (langs.includes("zh-Hant")) s = ".zh-tw";
|
||||
else if (langs.includes("zh-Hans")) s = ".chi.zh-cn";
|
||||
else if (langs.includes("en")) s = ".eng";
|
||||
else if (langs.includes("ja")) s = ".ja";
|
||||
else if (langs.includes("ko")) s = ".ko";
|
||||
if (tags.includes("default")) s += ".default";
|
||||
if (tags.includes("forced")) s += ".forced";
|
||||
return s;
|
||||
}
|
||||
Reference in new issue
Block a user