Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 269a5cbfda |
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { DELIVERY_CONFIG_DEFAULT, applyTokens, nextPackageName, tidyPath } from "./deliveries";
|
||||||
|
|
||||||
|
const BASE = "260901_Delivery";
|
||||||
|
|
||||||
|
describe("nextPackageName", () => {
|
||||||
|
it("uses the bare base for the first package of the day", () => {
|
||||||
|
expect(nextPackageName(BASE, [])).toBe(BASE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suffixes later packages sharing the base", () => {
|
||||||
|
expect(nextPackageName(BASE, [BASE])).toBe(`${BASE}_02`);
|
||||||
|
expect(nextPackageName(BASE, [BASE, `${BASE}_02`])).toBe(`${BASE}_03`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never reuses a number after a gap", () => {
|
||||||
|
expect(nextPackageName(BASE, [BASE, `${BASE}_02`, `${BASE}_04`])).toBe(`${BASE}_05`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores names that only share a prefix", () => {
|
||||||
|
expect(nextPackageName(BASE, [`${BASE}_final`, "260902_Delivery"])).toBe(BASE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("delivery layout", () => {
|
||||||
|
const tokens = {
|
||||||
|
deliveryRoot: "//SAN/DELIVERY",
|
||||||
|
showId: "ACME",
|
||||||
|
episode: "101",
|
||||||
|
packageName: BASE,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("separates shows so two shows' episode 101 cannot collide", () => {
|
||||||
|
const acme = applyTokens(DELIVERY_CONFIG_DEFAULT.layout.packageDir, tokens);
|
||||||
|
const other = applyTokens(DELIVERY_CONFIG_DEFAULT.layout.packageDir, { ...tokens, showId: "BRAVO" });
|
||||||
|
expect(acme).toBe("//SAN/DELIVERY/ACME/101/260901_Delivery");
|
||||||
|
expect(other).not.toBe(acme);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("puts producer reviews under the show too", () => {
|
||||||
|
expect(applyTokens(DELIVERY_CONFIG_DEFAULT.layout.reviewDir, tokens)).toBe(
|
||||||
|
"//SAN/DELIVERY/ACME/PRODUCER_REVIEWS/260901_Delivery"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("tidyPath", () => {
|
||||||
|
it("drops the empty segment a non-episodic show leaves behind", () => {
|
||||||
|
const raw = applyTokens(DELIVERY_CONFIG_DEFAULT.layout.packageDir, {
|
||||||
|
deliveryRoot: "//SAN/DELIVERY",
|
||||||
|
showId: "ACME",
|
||||||
|
episode: "",
|
||||||
|
packageName: BASE,
|
||||||
|
});
|
||||||
|
expect(tidyPath(raw)).toBe("//SAN/DELIVERY/ACME/260901_Delivery");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the UNC root intact", () => {
|
||||||
|
expect(tidyPath("//SAN/DELIVERY/ACME")).toBe("//SAN/DELIVERY/ACME");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves a well-formed path alone", () => {
|
||||||
|
expect(tidyPath("V:/DELIVERY/ACME/101/x")).toBe("V:/DELIVERY/ACME/101/x");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -46,10 +46,12 @@ export const DELIVERY_CONFIG_DEFAULT: DeliveryConfig = {
|
|||||||
packageNameTemplate: "{yymmdd}_Delivery",
|
packageNameTemplate: "{yymmdd}_Delivery",
|
||||||
reviewNameTemplate: "{yyyymmdd}_Producer_review",
|
reviewNameTemplate: "{yyyymmdd}_Producer_review",
|
||||||
layout: {
|
layout: {
|
||||||
packageDir: "{deliveryRoot}/{episode}/{packageName}",
|
// {showId} is not optional in practice: without it every show's episode 101
|
||||||
|
// lands in one folder on the delivery volume.
|
||||||
|
packageDir: "{deliveryRoot}/{showId}/{episode}/{packageName}",
|
||||||
shotDir: "{packageDir}/{shotCode}",
|
shotDir: "{packageDir}/{shotCode}",
|
||||||
reportFile: "{packageDir}/{packageName}_report.csv",
|
reportFile: "{packageDir}/{packageName}_report.csv",
|
||||||
reviewDir: "{deliveryRoot}/PRODUCER_REVIEWS/{packageName}",
|
reviewDir: "{deliveryRoot}/{showId}/PRODUCER_REVIEWS/{packageName}",
|
||||||
},
|
},
|
||||||
deliveryRoot: "//SAN/DELIVERY",
|
deliveryRoot: "//SAN/DELIVERY",
|
||||||
includeMovs: true,
|
includeMovs: true,
|
||||||
@@ -108,6 +110,36 @@ export function applyTokens(template: string, tokens: Partial<TokenValues>): str
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapse the empty segment an unset token leaves behind — a non-episodic
|
||||||
|
* show resolves {episode} to "" and would otherwise build ".../SHOW//NAME".
|
||||||
|
* The leading pair of a UNC root (//SAN/...) is preserved.
|
||||||
|
*/
|
||||||
|
export function tidyPath(path: string): string {
|
||||||
|
const uncPrefix = /^\/\//.test(path) ? "//" : "";
|
||||||
|
return uncPrefix + path.slice(uncPrefix.length).replace(/\/{2,}/g, "/").replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(value: string): string {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Second and later packages sharing a base name get `_02`, `_03`, ... Numbering
|
||||||
|
* counts every package built under that base — cancelled and failed ones
|
||||||
|
* included — so a number is never reused for a different folder.
|
||||||
|
*/
|
||||||
|
export function nextPackageName(base: string, existingNames: string[]): string {
|
||||||
|
const pattern = new RegExp(`^${escapeRegExp(base)}(?:_(\\d+))?$`);
|
||||||
|
let highest = 0;
|
||||||
|
for (const name of existingNames) {
|
||||||
|
const match = pattern.exec(name);
|
||||||
|
if (!match) continue;
|
||||||
|
highest = Math.max(highest, match[1] ? Number.parseInt(match[1], 10) : 1);
|
||||||
|
}
|
||||||
|
return highest === 0 ? base : `${base}_${String(highest + 1).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function dateTokens(date: Date) {
|
export function dateTokens(date: Date) {
|
||||||
const yyyy = String(date.getFullYear());
|
const yyyy = String(date.getFullYear());
|
||||||
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
@@ -280,8 +312,17 @@ export async function createDeliveryPackage(input: CreateDeliveryInput, userId:
|
|||||||
versionString: "",
|
versionString: "",
|
||||||
packageNumber: String(packageNumber),
|
packageNumber: String(packageNumber),
|
||||||
};
|
};
|
||||||
tokens.packageName = input.options?.name ?? applyTokens(config.packageNameTemplate, tokens);
|
// A second delivery on the same day is normal — a retake batch, or a client
|
||||||
tokens.packageDir = applyTokens(config.layout.packageDir, tokens);
|
// asking for the rest of a sequence — and the date-only template gives it a
|
||||||
|
// name identical to the morning's. Suffix it the way MOV reviews already
|
||||||
|
// are, so it gets its own folder instead of merging into the first one.
|
||||||
|
const baseName = applyTokens(config.packageNameTemplate, tokens);
|
||||||
|
const sameBase = await tx.deliveryPackage.findMany({
|
||||||
|
where: { projectId: project.id, kind: "EXR_DELIVERY", name: { startsWith: baseName } },
|
||||||
|
select: { name: true },
|
||||||
|
});
|
||||||
|
tokens.packageName = input.options?.name ?? nextPackageName(baseName, sameBase.map((p) => p.name));
|
||||||
|
tokens.packageDir = tidyPath(applyTokens(config.layout.packageDir, tokens));
|
||||||
|
|
||||||
const pkg = await tx.deliveryPackage.create({
|
const pkg = await tx.deliveryPackage.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { getConfigNumber } from "./config";
|
|||||||
import {
|
import {
|
||||||
applyTokens,
|
applyTokens,
|
||||||
dateTokens,
|
dateTokens,
|
||||||
|
nextPackageName,
|
||||||
resolveDeliveryConfig,
|
resolveDeliveryConfig,
|
||||||
|
tidyPath,
|
||||||
type DeliveryConfig,
|
type DeliveryConfig,
|
||||||
type DeliveryManifest,
|
type DeliveryManifest,
|
||||||
type TokenValues,
|
type TokenValues,
|
||||||
@@ -165,25 +167,12 @@ export async function resolveReviewCandidates(shotIds: string[]) {
|
|||||||
|
|
||||||
// ── Folder naming ────────────────────────────────────────────────────────────
|
// ── Folder naming ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function escapeRegExp(value: string): string {
|
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `20260828_Producer_review`, then `_02`, `_03` for further reviews the same
|
* `20260828_Producer_review`, then `_02`, `_03` for further reviews the same
|
||||||
* day. Numbering counts every package built under that base — cancelled and
|
* day. EXR deliveries now number the same way, so the rule lives in
|
||||||
* failed ones included — so a number is never reused for a different folder.
|
* deliveries.ts; this name is kept because it reads better at the call site.
|
||||||
*/
|
*/
|
||||||
export function nextReviewName(base: string, existingNames: string[]): string {
|
export const nextReviewName = nextPackageName;
|
||||||
const pattern = new RegExp(`^${escapeRegExp(base)}(?:_(\\d+))?$`);
|
|
||||||
let highest = 0;
|
|
||||||
for (const name of existingNames) {
|
|
||||||
const match = pattern.exec(name);
|
|
||||||
if (!match) continue;
|
|
||||||
highest = Math.max(highest, match[1] ? Number.parseInt(match[1], 10) : 1);
|
|
||||||
}
|
|
||||||
return highest === 0 ? base : `${base}_${String(highest + 1).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reviewNameFor(
|
async function reviewNameFor(
|
||||||
tx: Prisma.TransactionClient | typeof db,
|
tx: Prisma.TransactionClient | typeof db,
|
||||||
@@ -243,7 +232,7 @@ export async function previewMovReviewPackage(shotIds: string[]) {
|
|||||||
const tokens = reviewTokens(config, project, commonEpisode(candidates), buildDate, 0);
|
const tokens = reviewTokens(config, project, commonEpisode(candidates), buildDate, 0);
|
||||||
const base = applyTokens(config.reviewNameTemplate, tokens);
|
const base = applyTokens(config.reviewNameTemplate, tokens);
|
||||||
tokens.packageName = await reviewNameFor(db, project.id, base);
|
tokens.packageName = await reviewNameFor(db, project.id, base);
|
||||||
tokens.packageDir = applyTokens(config.layout.reviewDir, tokens);
|
tokens.packageDir = tidyPath(applyTokens(config.layout.reviewDir, tokens));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
@@ -299,7 +288,7 @@ export async function createMovReviewPackage(input: CreateMovReviewInput, userId
|
|||||||
const tokens = reviewTokens(config, project, episode, buildDate, packageNumber);
|
const tokens = reviewTokens(config, project, episode, buildDate, packageNumber);
|
||||||
const base = applyTokens(config.reviewNameTemplate, tokens);
|
const base = applyTokens(config.reviewNameTemplate, tokens);
|
||||||
tokens.packageName = input.options?.name ?? (await reviewNameFor(tx, project.id, base));
|
tokens.packageName = input.options?.name ?? (await reviewNameFor(tx, project.id, base));
|
||||||
tokens.packageDir = applyTokens(config.layout.reviewDir, tokens);
|
tokens.packageDir = tidyPath(applyTokens(config.layout.reviewDir, tokens));
|
||||||
|
|
||||||
const pkg = await tx.deliveryPackage.create({
|
const pkg = await tx.deliveryPackage.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
Reference in New Issue
Block a user