9660cdc21c
Deploy / deploy (push) Successful in 3m23s
Worker 1.7.0 (preview.highQuality, default on) now, after the AE render: - encodes a High quality MP4 from the delivery MOV (full-res H.264 High, CRF 14 capped at 100 Mbps) -> Version.hqFileUrl - uploads the MOV itself -> Version.highResKey/Filename/Size Both best-effort (the preview never fails over them), with a lease keep-alive during the long encode/uploads; the export timeline records what was attached. Player: HQ switch when a High quality source exists; keeps frame, play state, speed and zoom, remembered per browser, falls back to Standard if the file won't load. ProRes itself stays download-only (browser support). Downloads: the client portal's High Res button serves the version's ProRes (client-code filename), falling back to the shot's manual file; staff get a ProRes button on the review page. Housekeeping (lib/render-pipeline/high-res.ts): HQ MP4 + ProRes are kept while a version is latest, client-shared, approved or younger than preview.highResRetentionDays (14); otherwise deleted from storage. Runs per task after finalize and hourly from the reaper; deleting a version or a reslate removes its files. The Standard MP4 is never touched. Also: ProRes/Handles top-bar buttons go icon-only below lg so they don't overlap the version selector. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
4.3 KiB
TypeScript
125 lines
4.3 KiB
TypeScript
import { db } from "@/lib/db";
|
||
import { deleteFromHetzner } from "@/lib/storage";
|
||
import { getConfigNumber } from "./config";
|
||
|
||
/**
|
||
* Housekeeping for per-version high-quality media — the High quality review
|
||
* MP4 (`hqFileUrl`) and the ProRes delivery MOV (`highResKey`). The worker
|
||
* uploads both for every version, and at UHD they are 5–10× the Standard MP4,
|
||
* so versions nobody can need any more have them removed. The Standard MP4 is
|
||
* never touched: every version stays reviewable.
|
||
*/
|
||
|
||
export interface HighResCandidate {
|
||
isLatest: boolean;
|
||
isClientVisible: boolean;
|
||
approvalStatus: string;
|
||
createdAt: Date;
|
||
}
|
||
|
||
/**
|
||
* Whether a version still needs its high-quality media. Kept while it is:
|
||
* - the latest version of its task — what everyone is looking at;
|
||
* - shared with the client — they may download it;
|
||
* - approved — the version of record;
|
||
* - younger than the retention window — supervisors compare recent takes.
|
||
*/
|
||
export function shouldKeepHighRes(v: HighResCandidate, now: Date, retentionDays: number): boolean {
|
||
if (v.isLatest || v.isClientVisible || v.approvalStatus === "APPROVED") return true;
|
||
const ageMs = now.getTime() - v.createdAt.getTime();
|
||
return ageMs < Math.max(0, retentionDays) * 24 * 3600 * 1000;
|
||
}
|
||
|
||
/** Object-storage keys behind a version's high-quality media. */
|
||
export function highResStorageKeys(v: { hqFileUrl?: string | null; highResKey?: string | null }): string[] {
|
||
const keys: string[] = [];
|
||
if (v.highResKey) keys.push(v.highResKey);
|
||
if (v.hqFileUrl?.startsWith("/api/files/")) keys.push(v.hqFileUrl.slice("/api/files/".length));
|
||
return keys;
|
||
}
|
||
|
||
/** Best-effort deletes; a failure only leaves an orphan, never a broken link. */
|
||
export async function deleteStorageKeys(keys: string[]): Promise<void> {
|
||
for (const key of keys) {
|
||
await deleteFromHetzner(key).catch((err) =>
|
||
console.error(`[high-res] could not delete ${key} from storage`, err)
|
||
);
|
||
}
|
||
}
|
||
|
||
const candidateSelect = {
|
||
id: true,
|
||
isLatest: true,
|
||
isClientVisible: true,
|
||
approvalStatus: true,
|
||
createdAt: true,
|
||
hqFileUrl: true,
|
||
highResKey: true,
|
||
} as const;
|
||
|
||
type Candidate = HighResCandidate & { id: string; hqFileUrl: string | null; highResKey: string | null };
|
||
|
||
/**
|
||
* Clears the row first, then deletes the files: if the delete fails the file
|
||
* is merely orphaned, whereas the other order could leave a download pointing
|
||
* at nothing.
|
||
*/
|
||
async function prune(candidates: Candidate[], now: Date, retentionDays: number): Promise<number> {
|
||
let pruned = 0;
|
||
for (const v of candidates) {
|
||
if (shouldKeepHighRes(v, now, retentionDays)) continue;
|
||
const keys = highResStorageKeys(v);
|
||
await db.version.update({
|
||
where: { id: v.id },
|
||
data: {
|
||
hqFileUrl: null,
|
||
highResKey: null,
|
||
highResFilename: null,
|
||
highResSize: null,
|
||
highResPrunedAt: now,
|
||
},
|
||
});
|
||
await deleteStorageKeys(keys);
|
||
pruned++;
|
||
}
|
||
return pruned;
|
||
}
|
||
|
||
const hasHighRes = { OR: [{ hqFileUrl: { not: null } }, { highResKey: { not: null } }] };
|
||
|
||
/** Run after a new version lands: its predecessors may now be redundant. */
|
||
export async function pruneHighResForTask(taskId: string): Promise<number> {
|
||
const retentionDays = await getConfigNumber("preview.highResRetentionDays");
|
||
const candidates = await db.version.findMany({
|
||
where: { taskId, ...hasHighRes },
|
||
select: candidateSelect,
|
||
});
|
||
return prune(candidates, new Date(), retentionDays);
|
||
}
|
||
|
||
/**
|
||
* Studio-wide pass, for versions that age out of the retention window or are
|
||
* unshared/unapproved after the fact. Bounded per run; the reaper calls it
|
||
* hourly, so a backlog drains over a few runs.
|
||
*/
|
||
export async function pruneHighResSweep(batch = 200): Promise<number> {
|
||
const retentionDays = await getConfigNumber("preview.highResRetentionDays");
|
||
const now = new Date();
|
||
const cutoff = new Date(now.getTime() - Math.max(0, retentionDays) * 24 * 3600 * 1000);
|
||
const candidates = await db.version.findMany({
|
||
where: {
|
||
...hasHighRes,
|
||
isLatest: false,
|
||
isClientVisible: false,
|
||
approvalStatus: { not: "APPROVED" },
|
||
createdAt: { lt: cutoff },
|
||
},
|
||
select: candidateSelect,
|
||
orderBy: { createdAt: "asc" },
|
||
take: batch,
|
||
});
|
||
const pruned = await prune(candidates, now, retentionDays);
|
||
if (pruned > 0) console.log(`[high-res] housekeeping removed high-quality media from ${pruned} version(s)`);
|
||
return pruned;
|
||
}
|