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,56 @@
|
||||
export interface PlannedPath {
|
||||
src: string;
|
||||
dst: string;
|
||||
}
|
||||
|
||||
export interface ExistingPath {
|
||||
path: string;
|
||||
size?: number;
|
||||
sha256?: string;
|
||||
}
|
||||
|
||||
export interface PathConflict {
|
||||
dst: string;
|
||||
kind: "duplicate_in_plan" | "exists" | "case_collision";
|
||||
srcs: string[];
|
||||
existing?: ExistingPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 冲突检测:计划内多个源写同一目标、目标已存在、(大小写不敏感文件系统上)仅大小写不同。
|
||||
* 只输出结构,是否覆盖由整理策略与授权决定。
|
||||
*/
|
||||
export function detectPathConflicts(
|
||||
planned: readonly PlannedPath[],
|
||||
existing: readonly ExistingPath[],
|
||||
opts: { caseInsensitive?: boolean } = {},
|
||||
): PathConflict[] {
|
||||
const out: PathConflict[] = [];
|
||||
const byDst = new Map<string, string[]>();
|
||||
for (const p of planned) byDst.set(p.dst, [...(byDst.get(p.dst) ?? []), p.src]);
|
||||
for (const [dst, srcs] of byDst) if (srcs.length > 1) out.push({ dst, kind: "duplicate_in_plan", srcs });
|
||||
const exact = new Map(existing.map((e) => [e.path, e]));
|
||||
const folded = new Map(existing.map((e) => [e.path.toLowerCase(), e]));
|
||||
for (const [dst, srcs] of byDst) {
|
||||
const hit = exact.get(dst);
|
||||
if (hit) {
|
||||
out.push({ dst, kind: "exists", srcs, existing: hit });
|
||||
continue;
|
||||
}
|
||||
if (opts.caseInsensitive) {
|
||||
const f = folded.get(dst.toLowerCase());
|
||||
if (f) out.push({ dst, kind: "case_collision", srcs, existing: f });
|
||||
}
|
||||
}
|
||||
if (opts.caseInsensitive) {
|
||||
const seen = new Map<string, string>();
|
||||
for (const dst of byDst.keys()) {
|
||||
const k = dst.toLowerCase();
|
||||
const prev = seen.get(k);
|
||||
if (prev !== undefined && prev !== dst)
|
||||
out.push({ dst, kind: "case_collision", srcs: [...(byDst.get(prev) ?? []), ...(byDst.get(dst) ?? [])] });
|
||||
seen.set(k, dst);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user