Batch Video Uploads
Deploy / deploy (push) Successful in 2m38s

This commit is contained in:
twotalesanimation
2026-07-09 12:46:50 +02:00
parent 77cfcbc9e9
commit 637b141c87
6 changed files with 1105 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { uploadFile, uploadToHetzner, deleteFromHetzner } from "@/lib/storage";
import { recalcShotStatus } from "@/lib/shot-status";
export const maxDuration = 120;
/**
* POST /api/batch-upload/upload
*
* Handles a single file upload from the batch upload flow.
*
* FormData fields:
* file the binary file
* action "new-version" | "rename-and-upload" | "create-task" | "update-highres"
* shotId shot DB id (required for all actions)
* projectId project DB id
* taskId required for "new-version" and "rename-and-upload"
* fallbackTaskId optional: the "Comp" task id to rename for "create-task"
* newTaskTitle desired task title (filename without extension)
*/
export async function POST(
req: NextRequest
): Promise<NextResponse> {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const formData = await req.formData();
const file = formData.get("file") as File | null;
const action = formData.get("action") as string | null;
const shotId = formData.get("shotId") as string | null;
const projectId = formData.get("projectId") as string | null;
const taskId = formData.get("taskId") as string | null;
const fallbackTaskId = formData.get("fallbackTaskId") as string | null;
const newTaskTitle = (formData.get("newTaskTitle") as string | null) ?? "";
if (!file || !action || !shotId || !projectId) {
return NextResponse.json(
{ error: "Missing required fields: file, action, shotId, projectId" },
{ status: 400 }
);
}
const buffer = Buffer.from(await file.arrayBuffer());
// ── High-res (.mov) ───────────────────────────────────────────────────────
if (action === "update-highres") {
const shot = await db.shot.findUnique({
where: { id: shotId },
select: { id: true, highResKey: true },
});
if (!shot) {
return NextResponse.json({ error: "Shot not found" }, { status: 404 });
}
// Remove the previous high-res file if one exists
if (shot.highResKey) {
await deleteFromHetzner(shot.highResKey).catch(() => {});
}
const { key } = await uploadToHetzner(buffer, file.name, file.type, "highres");
await db.shot.update({
where: { id: shotId },
data: { highResKey: key, highResFilename: file.name },
});
return NextResponse.json({
success: true,
action: "update-highres",
fileName: file.name,
});
}
// ── Version upload (.mp4) ─────────────────────────────────────────────────
let resolvedTaskId: string;
if (action === "new-version") {
if (!taskId) {
return NextResponse.json(
{ error: "taskId is required for new-version action" },
{ status: 400 }
);
}
resolvedTaskId = taskId;
} else if (action === "rename-and-upload") {
if (!taskId) {
return NextResponse.json(
{ error: "taskId is required for rename-and-upload action" },
{ status: 400 }
);
}
// Rename the task to reflect the new version number
await db.task.update({
where: { id: taskId },
data: { title: newTaskTitle },
});
resolvedTaskId = taskId;
} else if (action === "create-task") {
if (fallbackTaskId) {
// Rename the existing "Comp" (or similar) task and reuse it
await db.task.update({
where: { id: fallbackTaskId },
data: { title: newTaskTitle },
});
resolvedTaskId = fallbackTaskId;
} else {
// Create a brand-new task under the shot
const lastTask = await db.task.findFirst({
where: { shotId },
orderBy: { sortOrder: "desc" },
select: { sortOrder: true },
});
const task = await db.task.create({
data: {
title: newTaskTitle,
type: "COMP",
shotId,
projectId,
createdById: session.user.id,
sortOrder: (lastTask?.sortOrder ?? -1) + 1,
},
});
resolvedTaskId = task.id;
}
} else {
return NextResponse.json(
{ error: `Unknown action: ${action}` },
{ status: 400 }
);
}
// Upload the video to the configured storage backend
const result = await uploadFile(buffer, file.name, file.type, "videos");
// Mark all existing versions for this task as no longer latest
await db.version.updateMany({
where: { taskId: resolvedTaskId },
data: { isLatest: false },
});
// Determine next sequential version number
const lastVersion = await db.version.findFirst({
where: { taskId: resolvedTaskId },
orderBy: { versionNumber: "desc" },
select: { versionNumber: true },
});
const versionNumber = (lastVersion?.versionNumber ?? 0) + 1;
// Create the version record
const version = await db.version.create({
data: {
versionNumber,
taskId: resolvedTaskId,
artistId: session.user.id,
fileUrl: result.url,
fileName: file.name,
fileSize: BigInt(file.size),
mimeType: file.type,
isLatest: true,
},
});
// Move the task to INTERNAL_REVIEW
await db.task.update({
where: { id: resolvedTaskId },
data: { status: "INTERNAL_REVIEW" },
});
// Recalculate the parent shot's status
await recalcShotStatus(shotId).catch(() => {});
return NextResponse.json({
success: true,
action,
versionId: version.id,
versionNumber,
taskId: resolvedTaskId,
});
}