176 lines
5.1 KiB
TypeScript
176 lines
5.1 KiB
TypeScript
import { z } from "zod";
|
|
import { db } from "@/lib/db";
|
|
import { Prisma } from "@prisma/client";
|
|
|
|
const exportManifestSchema = z.object({
|
|
shotCode: z.string().min(1),
|
|
projectCode: z.string().min(1),
|
|
aepPath: z.string().min(1),
|
|
compName: z.string().min(1),
|
|
rendererType: z.string().default("aerender"),
|
|
outputDir: z.string().optional(),
|
|
outputPattern: z.string().optional(),
|
|
frameStart: z.number().int().positive(),
|
|
frameEnd: z.number().int().positive(),
|
|
fps: z.number().positive(),
|
|
width: z.number().int().positive().optional(),
|
|
height: z.number().int().positive().optional(),
|
|
colorspace: z.string().optional(),
|
|
});
|
|
|
|
export function validateExportManifest(input: unknown) {
|
|
return exportManifestSchema.parse(input);
|
|
}
|
|
|
|
export function buildVersionString(versionNumber: number) {
|
|
return `v${versionNumber.toString().padStart(3, "0")}`;
|
|
}
|
|
|
|
export function deriveOutputBaseName(outputPattern: string) {
|
|
const match = outputPattern.match(/^(.*)\.(?:\[#+\]|\{#+\})\.([A-Za-z0-9]+)$/);
|
|
return match ? match[1] : outputPattern;
|
|
}
|
|
|
|
export async function createRenderExport(input: {
|
|
shotId: string;
|
|
projectId: string;
|
|
manifest: unknown;
|
|
submittedById?: string | null;
|
|
submittedByName?: string | null;
|
|
taskId?: string | null;
|
|
}) {
|
|
const manifest = validateExportManifest(input.manifest);
|
|
const shot = await db.shot.findUnique({
|
|
where: { id: input.shotId },
|
|
select: { id: true, shotVersion: true, exrOutput: true, projectId: true },
|
|
});
|
|
|
|
if (!shot) {
|
|
throw new Error("Shot not found");
|
|
}
|
|
|
|
if (shot.projectId !== input.projectId) {
|
|
throw new Error("Shot does not belong to the supplied project");
|
|
}
|
|
|
|
const versionNumber = parseInt((shot.shotVersion ?? "v001").replace(/^v/i, ""), 10) + 1;
|
|
const versionString = buildVersionString(versionNumber);
|
|
|
|
const outputPattern = manifest.outputPattern ?? `${manifest.compName}_TT_${versionString}.[####].exr`;
|
|
const outputDir = manifest.outputDir ?? `${shot.exrOutput ?? manifest.compName}_${versionString}`;
|
|
|
|
const result = await db.$transaction(async (tx) => {
|
|
const activeExports = await tx.export.findMany({
|
|
where: {
|
|
shotId: input.shotId,
|
|
status: { in: ["QUEUED", "RENDERING", "VALIDATING", "GENERATING_PREVIEW", "READY_FOR_QC", "READY_FOR_DELIVERY"] },
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
const exportRecord = await tx.export.create({
|
|
data: {
|
|
shotId: input.shotId,
|
|
projectId: input.projectId,
|
|
taskId: input.taskId ?? undefined,
|
|
versionNumber,
|
|
versionString,
|
|
status: "QUEUED",
|
|
aepPath: manifest.aepPath,
|
|
compName: manifest.compName,
|
|
rendererType: manifest.rendererType,
|
|
outputDir,
|
|
outputPattern,
|
|
frameStart: manifest.frameStart,
|
|
frameEnd: manifest.frameEnd,
|
|
fps: manifest.fps,
|
|
width: manifest.width ?? 1920,
|
|
height: manifest.height ?? 1080,
|
|
colorspace: manifest.colorspace,
|
|
submittedById: input.submittedById ?? undefined,
|
|
submittedByName: input.submittedByName ?? undefined,
|
|
},
|
|
});
|
|
|
|
await tx.exportEvent.createMany({
|
|
data: [
|
|
{
|
|
exportId: exportRecord.id,
|
|
toStatus: "QUEUED",
|
|
actorType: "USER",
|
|
actorId: input.submittedById ?? undefined,
|
|
note: "Queued from panel",
|
|
},
|
|
],
|
|
});
|
|
|
|
await tx.renderJob.create({
|
|
data: {
|
|
type: "AE_RENDER",
|
|
exportId: exportRecord.id,
|
|
attempt: 1,
|
|
maxAttempts: 3,
|
|
status: "QUEUED",
|
|
manifest: manifest as Prisma.JsonObject,
|
|
priority: 50,
|
|
},
|
|
});
|
|
|
|
await tx.shot.update({
|
|
where: { id: input.shotId },
|
|
data: {
|
|
shotVersion: versionString,
|
|
exrOutput: deriveOutputBaseName(outputPattern),
|
|
},
|
|
});
|
|
|
|
if (activeExports.length > 0) {
|
|
await tx.export.updateMany({
|
|
where: { id: { in: activeExports.map((item) => item.id) } },
|
|
data: {
|
|
status: "SUPERSEDED",
|
|
supersededById: exportRecord.id,
|
|
statusChangedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
await tx.exportEvent.createMany({
|
|
data: activeExports.map((item) => ({
|
|
exportId: item.id,
|
|
fromStatus: "QUEUED",
|
|
toStatus: "SUPERSEDED",
|
|
actorType: "SYSTEM",
|
|
note: "Superseded by newer export",
|
|
})),
|
|
});
|
|
}
|
|
|
|
return { exportRecord, versionString };
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
export async function listExportsForQueue() {
|
|
return db.export.findMany({
|
|
where: { status: { notIn: ["SUPERSEDED", "ARCHIVED", "CANCELLED", "DELIVERED"] } },
|
|
orderBy: [{ createdAt: "desc" }],
|
|
include: {
|
|
shot: { select: { shotCode: true, shotVersion: true, project: { select: { code: true } } } },
|
|
renderJobs: { orderBy: [{ createdAt: "desc" }], take: 1 },
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getLatestExportForShot(shotId: string) {
|
|
return db.export.findFirst({
|
|
where: { shotId },
|
|
orderBy: [{ createdAt: "desc" }],
|
|
include: {
|
|
shot: { select: { shotCode: true, project: { select: { code: true } } } },
|
|
renderJobs: { orderBy: [{ createdAt: "desc" }], take: 1 },
|
|
validations: true,
|
|
},
|
|
});
|
|
}
|