feat(pipeline): per-shot client naming alias for slates, overlays, and delivery
Deploy / deploy (push) Successful in 7m25s
Deploy / deploy (push) Successful in 7m25s
Clients sometimes ship footage under a naming convention that doesn't match our own shot codes. Adds an optional Shot.clientCode, editable on the Slate tab: when set, it replaces shotCode only in what the client actually sees — slate identity, overlay burn-in, and delivered EXR/MOV file and folder names in Netflix packages — with our own version string flatly appended (no "_cmp_TT_" studio jargon mixed in). Internal render paths, EXR filenames on the SAN, comp names, and DB lookups are untouched. Shots without a client code render identically to before. Delivery packages denormalize clientCode at build time, same as the existing shotCode/versionString snapshot, so a later shot edit can't retroactively change already-built delivery history. The worker now renames delivered EXR frames and the delivery MOV using the existing frame-number parser (SequencePattern) so only the base name changes — frame numbers and padding survive exactly. This is the one piece of the change that could plausibly ship the wrong bytes, so it's covered by new unit tests (renaming, padding, MOV rename, checksum-survives-rename, and a graceful fallback for an unparseable frame number) rather than resting on manual verification alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -224,7 +224,7 @@ export async function createDeliveryPackage(input: CreateDeliveryInput, userId:
|
||||
...(input.exportIds?.length ? { id: { in: input.exportIds } } : {}),
|
||||
...(input.episode ? { shot: { episode: input.episode } } : {}),
|
||||
},
|
||||
include: { shot: { select: { id: true, shotCode: true, episode: true } } },
|
||||
include: { shot: { select: { id: true, shotCode: true, clientCode: true, episode: true } } },
|
||||
orderBy: { shotId: "asc" },
|
||||
});
|
||||
|
||||
@@ -293,6 +293,7 @@ export async function createDeliveryPackage(input: CreateDeliveryInput, userId:
|
||||
exportId: e.id,
|
||||
shotId: e.shotId,
|
||||
shotCode: e.shot.shotCode,
|
||||
clientCode: e.shot.clientCode,
|
||||
versionString: e.versionString,
|
||||
})),
|
||||
});
|
||||
@@ -341,6 +342,13 @@ export interface DeliveryManifestItem {
|
||||
checksum: string | null;
|
||||
deliveryMovPath: string | null;
|
||||
shotDir: string;
|
||||
/**
|
||||
* `{clientCode}_{versionString}` when the shot has a client code, else the
|
||||
* studio's own `{shotCode}_cmp_TT_{versionString}` — what the worker names
|
||||
* the delivered EXR sequence and MOV as, replacing the internal render
|
||||
* filename it copies from.
|
||||
*/
|
||||
deliveredBaseName: string;
|
||||
/** When validation last ran for this export; null when it never did. */
|
||||
validatedAt: string | null;
|
||||
}
|
||||
@@ -417,11 +425,16 @@ export async function buildDeliveryJobManifest(
|
||||
checksum: item.export.checksum,
|
||||
deliveryMovPath: snapshot.includeMovs ? item.export.deliveryMovPath : null,
|
||||
validatedAt: item.export.validations[0]?.createdAt.toISOString() ?? null,
|
||||
// Client code substitutes for shotCode in what the client sees; folder
|
||||
// naming follows the same rule as the delivered filenames themselves.
|
||||
shotDir: applyTokens(snapshot.layout.shotDir, {
|
||||
...baseTokens,
|
||||
shotCode: item.shotCode,
|
||||
shotCode: item.clientCode || item.shotCode,
|
||||
versionString: item.versionString,
|
||||
}),
|
||||
deliveredBaseName: item.clientCode
|
||||
? `${item.clientCode}_${item.versionString}`
|
||||
: `${item.shotCode}_cmp_TT_${item.versionString}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ export async function buildPreviewManifest(exportRow: ExportForPreview): Promise
|
||||
where: { id: exportRow.shotId },
|
||||
select: {
|
||||
shotCode: true,
|
||||
clientCode: true,
|
||||
episode: true,
|
||||
scene: true,
|
||||
description: true,
|
||||
@@ -89,6 +90,11 @@ export async function buildPreviewManifest(exportRow: ExportForPreview): Promise
|
||||
// preview that is quietly the wrong colour.
|
||||
const templates = await resolveProjectTemplates(shot.project);
|
||||
const base = `${shot.shotCode}_cmp_TT_${exportRow.versionString}`;
|
||||
// Client code, when set, replaces shotCode in everything the client sees —
|
||||
// slate identity and (via ctx.slate.versionName) the overlay burn-in — but
|
||||
// never in internal paths: movOutput/mp4Output below stay on `base`. Just
|
||||
// their own naming with our version appended, no "_cmp_TT_" studio jargon.
|
||||
const clientBase = shot.clientCode ? `${shot.clientCode}_${exportRow.versionString}` : base;
|
||||
|
||||
return {
|
||||
stage: "PREVIEW",
|
||||
@@ -111,14 +117,14 @@ export async function buildPreviewManifest(exportRow: ExportForPreview): Promise
|
||||
slateScopeProp: cfg.slateScopeProp,
|
||||
slateSubmissionProp: cfg.slateSubmissionProp,
|
||||
slate: {
|
||||
versionName: base,
|
||||
versionName: clientBase,
|
||||
date: slateDate(),
|
||||
// The slate's own description is authored on the Shot's Slate tab and
|
||||
// falls back to the general shot description — the same precedence the
|
||||
// tab shows as its placeholder.
|
||||
description: shot.slateDescription ?? shot.description,
|
||||
notes: shot.notes,
|
||||
shotCode: shot.shotCode,
|
||||
shotCode: shot.clientCode || shot.shotCode,
|
||||
// The production's own name, not its code: this is read off a slate.
|
||||
show: shot.project?.name ?? shot.project?.code ?? null,
|
||||
episode: shot.episode,
|
||||
|
||||
Reference in New Issue
Block a user