@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildVersionString, deriveOutputBaseName, validateExportManifest } from "./exports";
|
||||
|
||||
describe("render export helpers", () => {
|
||||
it("formats version strings with zero-padding", () => {
|
||||
expect(buildVersionString(4)).toBe("v004");
|
||||
expect(buildVersionString(42)).toBe("v042");
|
||||
});
|
||||
|
||||
it("derives the output base name from a frame pattern", () => {
|
||||
expect(deriveOutputBaseName("UNG_106_010_020_cmp_TT_v004.[####].exr")).toBe(
|
||||
"UNG_106_010_020_cmp_TT_v004"
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid manifests", () => {
|
||||
expect(() =>
|
||||
validateExportManifest({
|
||||
shotCode: "UNG_106_010_020",
|
||||
projectCode: "UNG_S1",
|
||||
aepPath: "",
|
||||
compName: "",
|
||||
frameStart: 1001,
|
||||
frameEnd: 1000,
|
||||
fps: 24,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user