Files
vfxreview/app/api/batch-upload/preview/route.ts
T
twotalesanimation 637b141c87
Deploy / deploy (push) Successful in 2m38s
Batch Video Uploads
2026-07-09 12:46:50 +02:00

186 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import {
parseFilename,
stripVersionFromTitle,
findMatchingShotCode,
} from "@/lib/batch-upload-parser";
export type PreviewItemStatus =
| "new-version" // task found, same version prefix → just add a version
| "rename-and-upload" // task found but different version → rename task + add version
| "create-task" // no matching task → use/rename fallback "Comp" task or create new
| "update-highres" // .mov file → replace the shot's high-res file
| "no-shot" // shot code not found in this project
| "unsupported"; // file extension is not .mp4 or .mov
export interface PreviewItem {
fileName: string;
status: PreviewItemStatus;
shotId: string | null;
shotCode: string | null;
/** For .mp4: ID of the directly matched task, if found */
taskId: string | null;
/** For .mp4: current title of the matched / fallback task */
currentTaskTitle: string | null;
/** For .mp4: desired task title after upload (filename without extension) */
newTaskTitle: string;
/** For .mov: filename of the existing high-res file that will be replaced */
currentHighResFilename: string | null;
/** For create-task: ID of a "Comp" task to rename & use */
fallbackTaskId: string | null;
fallbackTaskTitle: string | null;
}
/**
* POST /api/batch-upload/preview
* Body: { projectId: string; fileNames: string[] }
*
* Returns a preview of what action will be taken for each file, without
* actually uploading anything.
*/
export async function POST(req: NextRequest) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const { projectId, fileNames } = body as {
projectId?: string;
fileNames?: unknown;
};
if (!projectId || !Array.isArray(fileNames) || fileNames.length === 0) {
return NextResponse.json(
{ error: "projectId and fileNames[] are required" },
{ status: 400 }
);
}
// Load all shots for this project with their tasks
const shots = await db.shot.findMany({
where: { projectId },
select: {
id: true,
shotCode: true,
highResFilename: true,
tasks: {
select: { id: true, title: true, type: true },
orderBy: { sortOrder: "asc" },
},
},
});
const shotCodes = shots.map((s) => s.shotCode);
const items: PreviewItem[] = fileNames.map((fileName: string) => {
const parsed = parseFilename(fileName);
if (parsed.type === "other") {
return {
fileName,
status: "unsupported" as const,
shotId: null,
shotCode: null,
taskId: null,
currentTaskTitle: null,
newTaskTitle: "",
currentHighResFilename: null,
fallbackTaskId: null,
fallbackTaskTitle: null,
};
}
// Find the shot whose code is a prefix of this file's task name base
const matchedCode = findMatchingShotCode(parsed.taskNameBase, shotCodes);
const matchedShot = matchedCode
? shots.find((s) => s.shotCode === matchedCode) ?? null
: null;
if (!matchedShot) {
return {
fileName,
status: "no-shot" as const,
shotId: null,
shotCode: null,
taskId: null,
currentTaskTitle: null,
newTaskTitle: "",
currentHighResFilename: null,
fallbackTaskId: null,
fallbackTaskTitle: null,
};
}
// ── .mov → update-highres ─────────────────────────────────────────────
if (parsed.type === "mov") {
return {
fileName,
status: "update-highres" as const,
shotId: matchedShot.id,
shotCode: matchedShot.shotCode,
taskId: null,
currentTaskTitle: null,
newTaskTitle: "",
currentHighResFilename: matchedShot.highResFilename ?? null,
fallbackTaskId: null,
fallbackTaskTitle: null,
};
}
// ── .mp4 → find matching task ─────────────────────────────────────────
const desiredTitle = parsed.taskNameWithVersion; // full name without ext
// Match by stripping version from existing task titles
const matchedTask = matchedShot.tasks.find(
(t) => stripVersionFromTitle(t.title) === parsed.taskNameBase
);
if (matchedTask) {
const sameTitle = matchedTask.title === desiredTitle;
return {
fileName,
status: (sameTitle ? "new-version" : "rename-and-upload") as PreviewItemStatus,
shotId: matchedShot.id,
shotCode: matchedShot.shotCode,
taskId: matchedTask.id,
currentTaskTitle: matchedTask.title,
newTaskTitle: desiredTitle,
currentHighResFilename: null,
fallbackTaskId: null,
fallbackTaskTitle: null,
};
}
// No direct match look for a generic "Comp" or "comp" task, or any COMP type
const compTask = matchedShot.tasks.find(
(t) =>
t.title.toLowerCase() === "comp" ||
t.type === "COMP"
);
return {
fileName,
status: "create-task" as const,
shotId: matchedShot.id,
shotCode: matchedShot.shotCode,
taskId: null,
currentTaskTitle: null,
newTaskTitle: desiredTitle,
currentHighResFilename: null,
fallbackTaskId: compTask?.id ?? null,
fallbackTaskTitle: compTask?.title ?? null,
};
});
return NextResponse.json({ items });
}