From d6e6f275b7a57892507c1ad49197e06b8432a76f Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 14:01:32 -0800 Subject: [PATCH] feat: generate multiple thumbs and posters --- .../src/__tests__/variants-sizes.test.ts | 9 ++ apps/worker/src/jobs.ts | 144 +++++++++--------- apps/worker/src/variants.ts | 18 +++ 3 files changed, 95 insertions(+), 76 deletions(-) create mode 100644 apps/worker/src/__tests__/variants-sizes.test.ts create mode 100644 apps/worker/src/variants.ts diff --git a/apps/worker/src/__tests__/variants-sizes.test.ts b/apps/worker/src/__tests__/variants-sizes.test.ts new file mode 100644 index 0000000..5701667 --- /dev/null +++ b/apps/worker/src/__tests__/variants-sizes.test.ts @@ -0,0 +1,9 @@ +import { test, expect } from "bun:test"; +import { computeImageVariantPlan } from "../variants"; + +test("computeImageVariantPlan includes 256 and 768 thumbs", () => { + expect(computeImageVariantPlan()).toEqual([ + { kind: "thumb", size: 256 }, + { kind: "thumb", size: 768 }, + ]); +}); diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index eee8e8c..6135f08 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -7,6 +7,8 @@ import { Readable } from "stream"; import sharp from "sharp"; +import { computeImageVariantPlan, computeVideoPosterPlan } from "./variants"; + import { CopyObjectCommand, GetObjectCommand, @@ -426,53 +428,37 @@ export async function handleProcessAsset(raw: unknown) { if (updates.width === null && imgMeta.width) updates.width = imgMeta.width; if (updates.height === null && imgMeta.height) updates.height = imgMeta.height; - const thumb256Path = join(tempDir, "thumb_256.jpg"); - const thumb768Path = join(tempDir, "thumb_768.jpg"); - await sharp(inputPath) - .rotate() - .resize(256, 256, { fit: "inside", withoutEnlargement: true }) - .jpeg({ quality: 80 }) - .toFile(thumb256Path); - await sharp(inputPath) - .rotate() - .resize(768, 768, { fit: "inside", withoutEnlargement: true }) - .jpeg({ quality: 80 }) - .toFile(thumb768Path); + const imagePlan = computeImageVariantPlan(); + const thumbKeys: Record = {}; + for (const item of imagePlan) { + const size = item.size; + const thumbPath = join(tempDir, `thumb_${size}.jpg`); + await sharp(inputPath) + .rotate() + .resize(size, size, { fit: "inside", withoutEnlargement: true }) + .jpeg({ quality: 80 }) + .toFile(thumbPath); - const thumb256Key = `thumbs/${asset.id}/image_256.jpg`; - const thumb768Key = `thumbs/${asset.id}/image_768.jpg`; - await uploadObject({ - bucket: asset.bucket, - key: thumb256Key, - filePath: thumb256Path, - contentType: "image/jpeg", - }); - await uploadObject({ - bucket: asset.bucket, - key: thumb768Key, - filePath: thumb768Path, - contentType: "image/jpeg", - }); - await upsertVariant({ - assetId: asset.id, - kind: "thumb", - size: 256, - key: thumb256Key, - mimeType: "image/jpeg", - width: typeof updates.width === "number" ? updates.width : null, - height: typeof updates.height === "number" ? updates.height : null, - }); - await upsertVariant({ - assetId: asset.id, - kind: "thumb", - size: 768, - key: thumb768Key, - mimeType: "image/jpeg", - width: typeof updates.width === "number" ? updates.width : null, - height: typeof updates.height === "number" ? updates.height : null, - }); - updates.thumb_small_key = thumb256Key; - updates.thumb_med_key = thumb768Key; + const thumbKey = `thumbs/${asset.id}/image_${size}.jpg`; + await uploadObject({ + bucket: asset.bucket, + key: thumbKey, + filePath: thumbPath, + contentType: "image/jpeg", + }); + await upsertVariant({ + assetId: asset.id, + kind: "thumb", + size, + key: thumbKey, + mimeType: "image/jpeg", + width: typeof updates.width === "number" ? updates.width : null, + height: typeof updates.height === "number" ? updates.height : null, + }); + thumbKeys[size] = thumbKey; + } + updates.thumb_small_key = thumbKeys[256] ?? null; + updates.thumb_med_key = thumbKeys[768] ?? null; } else if (asset.media_type === "video") { rawTags = await tryReadExifTags(); maybeSetCaptureDateFromTags(rawTags); @@ -512,36 +498,42 @@ export async function handleProcessAsset(raw: unknown) { rawTags = { ...rawTags, ffprobe: ffprobeData }; - const posterPath = join(tempDir, "poster_256.jpg"); - await runCommand("ffmpeg", [ - "-i", - inputPath, - "-vf", - "scale=256:256:force_original_aspect_ratio=decrease", - "-vframes", - "1", - "-q:v", - "2", - "-y", - posterPath - ]); - const posterKey = `thumbs/${asset.id}/poster_256.jpg`; - await uploadObject({ - bucket: asset.bucket, - key: posterKey, - filePath: posterPath, - contentType: "image/jpeg", - }); - await upsertVariant({ - assetId: asset.id, - kind: "poster", - size: 256, - key: posterKey, - mimeType: "image/jpeg", - width: typeof updates.width === "number" ? updates.width : null, - height: typeof updates.height === "number" ? updates.height : null, - }); - updates.poster_key = posterKey; + const posterPlan = computeVideoPosterPlan(); + const posterKeys: Record = {}; + for (const item of posterPlan) { + const size = item.size; + const posterPath = join(tempDir, `poster_${size}.jpg`); + await runCommand("ffmpeg", [ + "-i", + inputPath, + "-vf", + `scale=${size}:${size}:force_original_aspect_ratio=decrease`, + "-vframes", + "1", + "-q:v", + "2", + "-y", + posterPath + ]); + const posterKey = `thumbs/${asset.id}/poster_${size}.jpg`; + await uploadObject({ + bucket: asset.bucket, + key: posterKey, + filePath: posterPath, + contentType: "image/jpeg", + }); + await upsertVariant({ + assetId: asset.id, + kind: "poster", + size, + key: posterKey, + mimeType: "image/jpeg", + width: typeof updates.width === "number" ? updates.width : null, + height: typeof updates.height === "number" ? updates.height : null, + }); + posterKeys[size] = posterKey; + } + updates.poster_key = posterKeys[256] ?? null; } if (asset.media_type === "video" && typeof updates.poster_key !== "string") { diff --git a/apps/worker/src/variants.ts b/apps/worker/src/variants.ts new file mode 100644 index 0000000..8e1296a --- /dev/null +++ b/apps/worker/src/variants.ts @@ -0,0 +1,18 @@ +export type VariantPlanItem = { + kind: "thumb" | "poster"; + size: number; +}; + +export function computeImageVariantPlan(): VariantPlanItem[] { + return [ + { kind: "thumb", size: 256 }, + { kind: "thumb", size: 768 }, + ]; +} + +export function computeVideoPosterPlan(): VariantPlanItem[] { + return [ + { kind: "poster", size: 256 }, + { kind: "poster", size: 768 }, + ]; +}