From 6abfa8d0b5380dc1be98a3e1d834e9c52ccb43fb Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:08:10 +0200 Subject: [PATCH] csv take 3 --- actions/shots.ts | 74 +------------------ .../[id]/import-edl/EdlImportClient.tsx | 5 +- lib/edl-utils.ts | 72 ++++++++++++++++++ 3 files changed, 77 insertions(+), 74 deletions(-) create mode 100644 lib/edl-utils.ts diff --git a/actions/shots.ts b/actions/shots.ts index 733c06c..2efc24e 100644 --- a/actions/shots.ts +++ b/actions/shots.ts @@ -783,78 +783,8 @@ export async function unapproveShot(shotId: string) { // ── 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; -} - -/** - * 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 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 }; -} +import type { EdlImportRow } from "@/lib/edl-utils"; +export type { EdlImportRow }; export async function importShotsFromEdl( projectId: string, diff --git a/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx b/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx index 0c3e3b0..7fdcf7f 100644 --- a/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx +++ b/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx @@ -18,8 +18,9 @@ import { Pencil, Film, } from "lucide-react"; -import { parseEdlCsv, importShotsFromEdl } from "@/actions/shots"; -import type { EdlImportRow } from "@/actions/shots"; +import { parseEdlCsv } from "@/lib/edl-utils"; +import type { EdlImportRow } from "@/lib/edl-utils"; +import { importShotsFromEdl } from "@/actions/shots"; interface EdlImportClientProps { projectId: string; diff --git a/lib/edl-utils.ts b/lib/edl-utils.ts new file mode 100644 index 0000000..2ae67f1 --- /dev/null +++ b/lib/edl-utils.ts @@ -0,0 +1,72 @@ +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; +} + +/** + * 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 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 }; +}