feat: generate multiple thumbs and posters

This commit is contained in:
William Valentin
2026-02-01 14:01:32 -08:00
parent 517e21d0b7
commit d6e6f275b7
3 changed files with 95 additions and 76 deletions

View File

@@ -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 },
]);
});

View File

@@ -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<number, string> = {};
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<number, string> = {};
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") {

View File

@@ -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 },
];
}