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 }; }