feat(organize,web): sidecars follow selected videos; /console/organize?ids= prefill (FE-4 follow-ups)
- planImport (sourcePaths mode): when a selected source is a video file, same-directory subtitles / external audio whose stem (language tags stripped) matches it join the source list, so the organize wizard, which expands a directory into individual videos, no longer drops them. Unrelated sidecars stay out; pathGuard applies to the added files. - Organize records page honours ?ids= (wizard multi-group redirect): shows just those records via /files/records, refreshes while running, clear chip. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
363f85b1a1
commit
2ae780d2a1
5 files changed
+310
-7
No files matched your search
@@ -22,6 +22,7 @@ import {
|
||||
type PathBinding as DomainBinding,
|
||||
type ExistingPolicy,
|
||||
type ExistingTarget,
|
||||
fileKindOf,
|
||||
findOtherVersions,
|
||||
type NewBinding,
|
||||
type OrganizePlan,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
type QualityFacts,
|
||||
resolveTargetDirectory,
|
||||
type SourceFile,
|
||||
sidecarStem,
|
||||
} from "@umt/domain";
|
||||
import { organizationStatusFromFiles, organizePlanToFileResults, toDomainPathBinding } from "@umt/mapping";
|
||||
import { withAttemptToken } from "@umt/tasks";
|
||||
@@ -192,6 +194,48 @@ async function walk(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中的是单个视频文件时,把同目录里主干名(剥掉 .chs/.eng 等语言标签后)相同的字幕 / 外挂音轨补进源文件列表;
|
||||
* 配对规则与 planOrganize 的附属文件一致(同目录、主干唯一匹配),这里只负责把它们带进来。
|
||||
* 目录源已由 walk 递归带上附属文件;已在列表里的不重复加。
|
||||
*/
|
||||
async function addSidecarSiblings(
|
||||
sources: string[],
|
||||
out: string[],
|
||||
limit: number,
|
||||
guard?: ((p: string) => Promise<string>) | undefined,
|
||||
): Promise<void> {
|
||||
const have = new Set(out);
|
||||
const byDir = new Map<string, Set<string>>();
|
||||
for (const src of sources) {
|
||||
if (fileKindOf(src) !== "video" || !have.has(src)) continue;
|
||||
const dir = dirname(src);
|
||||
const stems = byDir.get(dir) ?? new Set<string>();
|
||||
stems.add(
|
||||
basename(src)
|
||||
.replace(/\.[^.]+$/, "")
|
||||
.toLowerCase(),
|
||||
);
|
||||
byDir.set(dir, stems);
|
||||
}
|
||||
for (const [dir, stems] of byDir) {
|
||||
const names = (await readdir(dir).catch(() => [] as string[])).sort();
|
||||
for (const n of names) {
|
||||
if (out.length >= limit) return;
|
||||
if (n.startsWith(".")) continue;
|
||||
const kind = fileKindOf(n);
|
||||
if (kind !== "subtitle" && kind !== "audio") continue;
|
||||
const p = join(dir, n);
|
||||
if (have.has(p) || !stems.has(sidecarStem(n).stem)) continue;
|
||||
const st = await statSafe(p);
|
||||
if (st.kind !== "ok" || !st.facts.isFile) continue;
|
||||
if (guard) await guard(p);
|
||||
out.push(p);
|
||||
have.add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function libraryKinds(kind: MediaEntityRow["kind"]): string[] {
|
||||
if (kind === "movie") return ["movie", "mixed"];
|
||||
if (kind === "variety") return ["variety", "series", "mixed"];
|
||||
@@ -302,6 +346,8 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise<PlanRes
|
||||
try {
|
||||
// 多走一个用来判断是否超限
|
||||
for (const p of req.sourcePaths) await walk(p, paths, max + 1, req.pathGuard);
|
||||
// 直接选中的视频文件(整理向导把目录展开成逐个视频):同目录同主干的字幕 / 外挂音轨随行进计划
|
||||
await addSidecarSiblings(req.sourcePaths, paths, max + 1, req.pathGuard);
|
||||
} catch (err) {
|
||||
if (err instanceof PathGuardError) throw err;
|
||||
return fail("source_missing", { message: errMessage(err) });
|
||||
|
||||
Reference in new issue
Block a user