186 lines
5.7 KiB
TypeScript
186 lines
5.7 KiB
TypeScript
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 });
|
||
}
|