@@ -97,3 +97,90 @@ export function parseEdlCsv(csvText: string): { rows: EdlImportRow[]; errors: st
|
||||
|
||||
return { rows, errors };
|
||||
}
|
||||
|
||||
// ── Picture Tracker CSV Parser ────────────────────────────────────────────────
|
||||
|
||||
export interface PictureTrackerRow {
|
||||
shotCode: string;
|
||||
seqTimecodeStart: string;
|
||||
seqTimecodeEnd: string;
|
||||
/** true when shotCode matched an existing shot in the project */
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* TC values exported by some tools contain invalid values like "aN:aN:aN:aN"
|
||||
* or "#ERROR!". Reject anything that doesn't look like HH:MM:SS:FF.
|
||||
*/
|
||||
function isValidTimecode(tc: string): boolean {
|
||||
return /^\d{2}:\d{2}:\d{2}[:;]\d{2}$/.test(tc.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a picture tracker CSV.
|
||||
*
|
||||
* Expected header (col indices may vary, but we find by name):
|
||||
* #, Thumbnail, Shot Name, TC In, TC Out, Duration, ...
|
||||
*
|
||||
* Only the FIRST TC In/Out for each unique Shot Name is used
|
||||
* (subsequent rows for the same shot are ignored).
|
||||
*
|
||||
* @param csvText raw CSV text
|
||||
* @param existingCodes set of shotCodes already in the project
|
||||
*/
|
||||
export function parsePictureTrackerCsv(
|
||||
csvText: string,
|
||||
existingCodes: Set<string>
|
||||
): { rows: PictureTrackerRow[]; 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 rawHeaders = parseCsvLine(lines[0]).map((h) => h.toLowerCase().trim());
|
||||
const col = (name: string) => rawHeaders.indexOf(name);
|
||||
|
||||
const shotNameIdx = col("shot name");
|
||||
const tcInIdx = col("tc in");
|
||||
const tcOutIdx = col("tc out");
|
||||
|
||||
if (shotNameIdx === -1) {
|
||||
return { rows: [], errors: ['CSV must contain a "Shot Name" column'] };
|
||||
}
|
||||
if (tcInIdx === -1 || tcOutIdx === -1) {
|
||||
return { rows: [], errors: ['CSV must contain "TC In" and "TC Out" columns'] };
|
||||
}
|
||||
|
||||
const rows: PictureTrackerRow[] = [];
|
||||
const errors: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cells = parseCsvLine(lines[i]);
|
||||
const get = (idx: number) => (idx < cells.length ? cells[idx] ?? "" : "").trim();
|
||||
|
||||
const shotCode = get(shotNameIdx);
|
||||
if (!shotCode) continue;
|
||||
|
||||
// Only use the first occurrence per shot
|
||||
if (seen.has(shotCode)) continue;
|
||||
|
||||
const tcIn = get(tcInIdx);
|
||||
const tcOut = get(tcOutIdx);
|
||||
|
||||
if (!isValidTimecode(tcIn) || !isValidTimecode(tcOut)) {
|
||||
errors.push(`Row ${i + 1} (${shotCode}): invalid timecode "${tcIn}" / "${tcOut}" — skipped`);
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(shotCode);
|
||||
rows.push({
|
||||
shotCode,
|
||||
seqTimecodeStart: tcIn,
|
||||
seqTimecodeEnd: tcOut,
|
||||
exists: existingCodes.has(shotCode),
|
||||
});
|
||||
}
|
||||
|
||||
return { rows, errors };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user