/** * Utilities for parsing VFX filenames into shot / task metadata. * * Expected naming convention (any separator count is fine): * {SHOW}_{EP}_{SCENE}_{CUT}_{TASK}_{ARTIST}_v{NNN}.{ext} * e.g. UNG_106_035_010_cmp_TT_v002.mp4 * * The shot code is matched against the database – the parser itself * does NOT hard-code how many underscore segments belong to the shot. */ export interface ParsedFilename { originalName: string; /** Filename without the extension */ nameWithoutExt: string; /** Lowercased extension without the leading dot (e.g. "mp4", "mov") */ ext: string; /** * Task name base – the full name without the version suffix. * e.g. "UNG_106_035_010_cmp_TT" */ taskNameBase: string; /** * Full task title including the version suffix (no extension). * This is what the task should be named after upload. * e.g. "UNG_106_035_010_cmp_TT_v002" */ taskNameWithVersion: string; /** * Version string extracted from the filename, e.g. "v002". * Empty string if no version pattern was found. */ version: string; type: "mp4" | "mov" | "other"; } /** * Parse a VFX filename into its constituent parts. */ export function parseFilename(filename: string): ParsedFilename { const lastDot = filename.lastIndexOf("."); const ext = lastDot >= 0 ? filename.slice(lastDot + 1).toLowerCase() : ""; const nameWithoutExt = lastDot >= 0 ? filename.slice(0, lastDot) : filename; // Version pattern: _v followed by one or more digits at the end (case insensitive) const versionMatch = nameWithoutExt.match(/_v(\d+)$/i); const version = versionMatch ? `v${versionMatch[1]}` : ""; const taskNameBase = versionMatch ? nameWithoutExt.slice(0, nameWithoutExt.length - versionMatch[0].length) : nameWithoutExt; const type: "mp4" | "mov" | "other" = ext === "mp4" ? "mp4" : ext === "mov" ? "mov" : "other"; return { originalName: filename, nameWithoutExt, ext, taskNameBase, taskNameWithVersion: nameWithoutExt, version, type, }; } /** * Strip a trailing version suffix (_v###) from a task title. * e.g. "UNG_106_035_010_cmp_TT_v001" → "UNG_106_035_010_cmp_TT" */ export function stripVersionFromTitle(title: string): string { return title.replace(/_v\d+$/i, ""); } /** * Given a taskNameBase and a list of shot codes, return the shot code that * is a prefix of the task name base (shot code + "_" separator). * * e.g. taskNameBase "UNG_106_035_010_cmp_TT" matches shot "UNG_106_035_010" */ export function findMatchingShotCode( taskNameBase: string, shotCodes: string[] ): string | null { // Sort by length descending so longer (more specific) codes are matched first const sorted = [...shotCodes].sort((a, b) => b.length - a.length); for (const code of sorted) { if ( taskNameBase === code || taskNameBase.toLowerCase().startsWith(code.toLowerCase() + "_") ) { return code; } } return null; } // ── Footage plates ──────────────────────────────────────────────────────────── /** Video extensions accepted by the footage plate batch upload. */ export const PLATE_EXTS = new Set(["mov", "mp4", "m4v", "mxf", "avi", "mkv", "webm"]); const PLATE_CONTENT_TYPES: Record = { mov: "video/quicktime", mp4: "video/mp4", m4v: "video/mp4", mxf: "video/mxf", avi: "video/x-msvideo", mkv: "video/x-matroska", webm: "video/webm", }; /** * Derive a `video/*` content type from a plate filename's extension. * * Browsers routinely report an empty MIME type for .mov / .mxf files * (especially on Windows), and the presign endpoint rejects anything that * isn't `video/*`, so never trust `File.type` for these. */ export function plateContentType(fileName: string): string { const ext = fileName.split(".").pop()?.toLowerCase() ?? ""; return PLATE_CONTENT_TYPES[ext] ?? "application/octet-stream"; } export interface ParsedPlateFilename { /** Lowercased extension without the leading dot */ ext: string; /** Filename without the extension */ nameWithoutExt: string; /** Shot code this file belongs to, or null when nothing matched */ shotCode: string | null; /** * The filename remainder after the shot code, used as the plate label. * e.g. "DH2_VFX_058_120_REF.mov" → "REF". * Empty when the filename is exactly the shot code. */ label: string; } /** * Resolve a footage plate filename to a shot code and a plate label. * * e.g. "DH2_VFX_058_120_REF.mov" against shot "DH2_VFX_058_120" * → { shotCode: "DH2_VFX_058_120", label: "REF" } */ export function parsePlateFilename( fileName: string, shotCodes: string[] ): ParsedPlateFilename { const lastDot = fileName.lastIndexOf("."); const ext = lastDot >= 0 ? fileName.slice(lastDot + 1).toLowerCase() : ""; const nameWithoutExt = lastDot >= 0 ? fileName.slice(0, lastDot) : fileName; const shotCode = findMatchingShotCode(nameWithoutExt, shotCodes); const label = shotCode ? nameWithoutExt.slice(shotCode.length).replace(/^[_\-\s]+/, "").trim() : ""; return { ext, nameWithoutExt, shotCode, label }; }