Files
UHD_Media_Tool/packages/domain/src/path/conflicts.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

57 lines
1.8 KiB
TypeScript

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;
}