+165
-4
@@ -781,12 +781,173 @@ export async function unapproveShot(shotId: string) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
revalidatePath(`/projects/${shot.projectId}`);
|
||||
revalidatePath(`/projects/${shot.projectId}/shots/${shotId}`);
|
||||
revalidatePath(`/shot-status`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ── EDL / Pull CSV Import ─────────────────────────────────────────────────────
|
||||
|
||||
export interface EdlImportRow {
|
||||
outputName: string;
|
||||
sourceClip: string;
|
||||
timecodeStart: string;
|
||||
timecodeEnd: string;
|
||||
clipDuration: string;
|
||||
// Derived
|
||||
shotCode: string;
|
||||
exrOutput: string;
|
||||
// Action decided during preview
|
||||
action: "create" | "update" | "skip";
|
||||
existingShotId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: handle client requesting changes on a shot.
|
||||
* Resets shotApprovalStatus = PENDING, sharedWithClient = false.
|
||||
* Recalculates shot status (will become REVISIONS if tasks set to CHANGES).
|
||||
* Parse a VFX pull CSV into import rows.
|
||||
* Output Name format: {showId}_{episode}_{scene}_{shot}_BG01_TT_v001
|
||||
* shotCode = first 4 underscore-separated segments
|
||||
* exrOutput = {shotCode}_{sourceClip}
|
||||
*/
|
||||
export async function clientRequestShotChanges(shotId: string, taskId?: string) {
|
||||
export function parseEdlCsv(csvText: string): { rows: EdlImportRow[]; errors: string[] } {
|
||||
const lines = csvText.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
||||
if (lines.length < 2) return { rows: [], errors: ["CSV must have a header row and at least one data row"] };
|
||||
|
||||
const unquote = (s: string) => s.replace(/^"|"$/g, "").trim();
|
||||
|
||||
const rawHeaders = lines[0].split(",").map(unquote).map((h) => h.toLowerCase());
|
||||
const col = (name: string) => rawHeaders.indexOf(name);
|
||||
|
||||
const outNameIdx = col("output name");
|
||||
const srcClipIdx = col("source clip");
|
||||
const tcStartIdx = col("timecode start");
|
||||
const tcEndIdx = col("timecode end");
|
||||
const durIdx = col("duration");
|
||||
|
||||
if (outNameIdx === -1 || srcClipIdx === -1) {
|
||||
return { rows: [], errors: ['CSV must contain "Output Name" and "Source Clip" columns'] };
|
||||
}
|
||||
|
||||
const rows: EdlImportRow[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cells = lines[i].match(/("(?:[^"]|"")*"|[^,]*)/g)?.map(unquote) ?? [];
|
||||
const get = (idx: number) => (idx !== -1 ? cells[idx] ?? "" : "");
|
||||
|
||||
const outputName = get(outNameIdx);
|
||||
const sourceClip = get(srcClipIdx);
|
||||
if (!outputName) { errors.push(`Row ${i + 1}: empty Output Name — skipped`); continue; }
|
||||
|
||||
const parts = outputName.split("_");
|
||||
if (parts.length < 4) {
|
||||
errors.push(`Row ${i + 1}: "${outputName}" — cannot parse shot code (need at least 4 segments)`);
|
||||
continue;
|
||||
}
|
||||
const shotCode = parts.slice(0, 4).join("_");
|
||||
const exrOutput = sourceClip ? `${shotCode}_${sourceClip}` : shotCode;
|
||||
|
||||
rows.push({
|
||||
outputName,
|
||||
sourceClip,
|
||||
timecodeStart: get(tcStartIdx),
|
||||
timecodeEnd: get(tcEndIdx),
|
||||
clipDuration: get(durIdx),
|
||||
shotCode,
|
||||
exrOutput,
|
||||
action: "create",
|
||||
});
|
||||
}
|
||||
|
||||
return { rows, errors };
|
||||
}
|
||||
|
||||
export async function importShotsFromEdl(
|
||||
projectId: string,
|
||||
rows: EdlImportRow[]
|
||||
): Promise<{ created: string[]; updated: string[]; skipped: string[]; errors: string[] }> {
|
||||
const session = await auth();
|
||||
if (!session?.user) throw new Error("Unauthorized");
|
||||
if (!["ADMIN", "PRODUCER", "SUPERVISOR"].includes(session.user.role)) {
|
||||
throw new Error("Insufficient permissions");
|
||||
}
|
||||
|
||||
const created: string[] = [];
|
||||
const updated: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.action === "skip") { skipped.push(row.shotCode); continue; }
|
||||
|
||||
try {
|
||||
if (row.action === "update" && row.existingShotId) {
|
||||
await db.shot.update({
|
||||
where: { id: row.existingShotId },
|
||||
data: {
|
||||
sourceClip: row.sourceClip || null,
|
||||
timecodeStart: row.timecodeStart || null,
|
||||
timecodeEnd: row.timecodeEnd || null,
|
||||
clipDuration: row.clipDuration || null,
|
||||
exrOutput: row.exrOutput || null,
|
||||
},
|
||||
});
|
||||
updated.push(row.shotCode);
|
||||
} else {
|
||||
const existing = await db.shot.findFirst({
|
||||
where: { projectId, shotCode: row.shotCode },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await db.shot.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
sourceClip: row.sourceClip || null,
|
||||
timecodeStart: row.timecodeStart || null,
|
||||
timecodeEnd: row.timecodeEnd || null,
|
||||
clipDuration: row.clipDuration || null,
|
||||
exrOutput: row.exrOutput || null,
|
||||
},
|
||||
});
|
||||
updated.push(row.shotCode);
|
||||
} else {
|
||||
const parts = row.shotCode.split("_");
|
||||
const scene = parts[2] ?? parts[1] ?? "000";
|
||||
const episode = parts[1] ?? null;
|
||||
|
||||
const maxNum = await db.shot.findFirst({
|
||||
where: { projectId, scene, episode },
|
||||
orderBy: { shotNumber: "desc" },
|
||||
select: { shotNumber: true },
|
||||
});
|
||||
const shotNumber = (maxNum?.shotNumber ?? 0) + 10;
|
||||
|
||||
await db.shot.create({
|
||||
data: {
|
||||
projectId,
|
||||
shotCode: row.shotCode,
|
||||
scene,
|
||||
episode,
|
||||
shotNumber,
|
||||
sourceClip: row.sourceClip || null,
|
||||
timecodeStart: row.timecodeStart || null,
|
||||
timecodeEnd: row.timecodeEnd || null,
|
||||
clipDuration: row.clipDuration || null,
|
||||
exrOutput: row.exrOutput || null,
|
||||
},
|
||||
});
|
||||
created.push(row.shotCode);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push(`${row.shotCode}: ${e instanceof Error ? e.message : "Unknown error"}`);
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath(`/projects/${projectId}`);
|
||||
return { created, updated, skipped, errors };
|
||||
}
|
||||
const session = await auth();
|
||||
if (!session?.user) throw new Error("Unauthorized");
|
||||
if (!["ADMIN", "PRODUCER", "SUPERVISOR", "CLIENT"].includes(session.user.role)) {
|
||||
|
||||
Reference in New Issue
Block a user