Add new gallery variant
This commit is contained in:
130
src/actions/artworks/generateGalleryVariant.ts
Normal file
130
src/actions/artworks/generateGalleryVariant.ts
Normal file
@ -0,0 +1,130 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { s3 } from "@/lib/s3";
|
||||
import { getImageBufferFromS3Key } from "@/utils/getImageBufferFromS3";
|
||||
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import sharp from "sharp";
|
||||
|
||||
const GALLERY_TARGET_SIZE = 300;
|
||||
|
||||
export async function generateGalleryVariant(
|
||||
artworkId: string,
|
||||
opts?: { force?: boolean },
|
||||
) {
|
||||
const artwork = await prisma.artwork.findUnique({
|
||||
where: { id: artworkId },
|
||||
include: { file: true, variants: true },
|
||||
});
|
||||
|
||||
if (!artwork || !artwork.file) {
|
||||
throw new Error("Artwork or file not found");
|
||||
}
|
||||
|
||||
const existing = artwork.variants.find((v) => v.type === "gallery");
|
||||
if (existing && !opts?.force) {
|
||||
return { ok: true, skipped: true, variantId: existing.id };
|
||||
}
|
||||
|
||||
const source =
|
||||
artwork.variants.find((v) => v.type === "modified") ??
|
||||
artwork.variants.find((v) => v.type === "original");
|
||||
|
||||
if (!source?.s3Key) {
|
||||
throw new Error("Missing source variant");
|
||||
}
|
||||
|
||||
const buffer = await getImageBufferFromS3Key(source.s3Key);
|
||||
const srcMeta = await sharp(buffer).metadata();
|
||||
|
||||
const { width, height } = srcMeta;
|
||||
let resizeOptions: { width?: number; height?: number };
|
||||
if (width && height) {
|
||||
resizeOptions =
|
||||
height < width
|
||||
? { height: GALLERY_TARGET_SIZE }
|
||||
: { width: GALLERY_TARGET_SIZE };
|
||||
} else {
|
||||
resizeOptions = { height: GALLERY_TARGET_SIZE };
|
||||
}
|
||||
|
||||
const galleryBuffer = await sharp(buffer)
|
||||
.resize({ ...resizeOptions, withoutEnlargement: true })
|
||||
.toFormat("webp")
|
||||
.toBuffer();
|
||||
|
||||
const galleryMetadata = await sharp(galleryBuffer).metadata();
|
||||
const galleryKey = `gallery/${artwork.file.fileKey}.webp`;
|
||||
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: `${process.env.BUCKET_NAME}`,
|
||||
Key: galleryKey,
|
||||
Body: galleryBuffer,
|
||||
ContentType: "image/" + galleryMetadata.format,
|
||||
}),
|
||||
);
|
||||
|
||||
const variant = await prisma.fileVariant.upsert({
|
||||
where: { artworkId_type: { artworkId: artwork.id, type: "gallery" } },
|
||||
create: {
|
||||
s3Key: galleryKey,
|
||||
type: "gallery",
|
||||
height: galleryMetadata.height ?? 0,
|
||||
width: galleryMetadata.width ?? 0,
|
||||
fileExtension: galleryMetadata.format,
|
||||
mimeType: "image/" + galleryMetadata.format,
|
||||
sizeBytes: galleryMetadata.size,
|
||||
artworkId: artwork.id,
|
||||
},
|
||||
update: {
|
||||
s3Key: galleryKey,
|
||||
height: galleryMetadata.height ?? 0,
|
||||
width: galleryMetadata.width ?? 0,
|
||||
fileExtension: galleryMetadata.format,
|
||||
mimeType: "image/" + galleryMetadata.format,
|
||||
sizeBytes: galleryMetadata.size,
|
||||
},
|
||||
});
|
||||
|
||||
return { ok: true, skipped: false, variantId: variant.id };
|
||||
}
|
||||
|
||||
export async function generateGalleryVariantsMissing(args?: {
|
||||
limit?: number;
|
||||
}) {
|
||||
const limit = Math.min(Math.max(args?.limit ?? 20, 1), 100);
|
||||
|
||||
const artworks = await prisma.artwork.findMany({
|
||||
where: { variants: { none: { type: "gallery" } } },
|
||||
orderBy: [{ updatedAt: "asc" }, { id: "asc" }],
|
||||
take: limit,
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const results: Array<{ artworkId: string; ok: boolean; error?: string }> = [];
|
||||
|
||||
for (const a of artworks) {
|
||||
try {
|
||||
await generateGalleryVariant(a.id);
|
||||
results.push({ artworkId: a.id, ok: true });
|
||||
} catch (err) {
|
||||
results.push({
|
||||
artworkId: a.id,
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : "Failed",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ok = results.filter((r) => r.ok).length;
|
||||
const failed = results.length - ok;
|
||||
|
||||
return {
|
||||
picked: artworks.length,
|
||||
processed: results.length,
|
||||
ok,
|
||||
failed,
|
||||
results,
|
||||
};
|
||||
}
|
||||
24
src/actions/artworks/getGalleryVariantStats.ts
Normal file
24
src/actions/artworks/getGalleryVariantStats.ts
Normal file
@ -0,0 +1,24 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export type GalleryVariantStats = {
|
||||
total: number;
|
||||
withGallery: number;
|
||||
missing: number;
|
||||
};
|
||||
|
||||
export async function getGalleryVariantStats(): Promise<GalleryVariantStats> {
|
||||
const [total, withGallery] = await Promise.all([
|
||||
prisma.artwork.count(),
|
||||
prisma.artwork.count({
|
||||
where: { variants: { some: { type: "gallery" } } },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
total,
|
||||
withGallery,
|
||||
missing: total - withGallery,
|
||||
};
|
||||
}
|
||||
@ -8,7 +8,10 @@ import sharp from "sharp";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { generateArtworkColorsForArtwork } from "../artworks/generateArtworkColors";
|
||||
|
||||
export async function createImageFromFile(imageFile: File, opts?: { originalName?: string, colorMode?: "inline" | "defer" | "off" }) {
|
||||
export async function createImageFromFile(
|
||||
imageFile: File,
|
||||
opts?: { originalName?: string; colorMode?: "inline" | "defer" | "off" },
|
||||
) {
|
||||
if (!(imageFile instanceof File)) {
|
||||
console.log("No image or invalid type");
|
||||
return null;
|
||||
@ -29,6 +32,7 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
|
||||
const modifiedKey = `modified/${fileKey}.webp`;
|
||||
const resizedKey = `resized/${fileKey}.webp`;
|
||||
const thumbnailKey = `thumbnail/${fileKey}.webp`;
|
||||
const galleryKey = `gallery/${fileKey}.webp`;
|
||||
|
||||
const sharpData = sharp(buffer);
|
||||
const metadata = await sharpData.metadata();
|
||||
@ -40,7 +44,7 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
|
||||
Key: originalKey,
|
||||
Body: buffer,
|
||||
ContentType: "image/" + metadata.format,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
//--- Modified file
|
||||
@ -53,7 +57,7 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
|
||||
Key: modifiedKey,
|
||||
Body: modifiedBuffer,
|
||||
ContentType: "image/" + modifiedMetadata.format,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
//--- Resized file
|
||||
@ -62,7 +66,8 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
|
||||
|
||||
let resizeOptions: { width?: number; height?: number };
|
||||
if (width && height) {
|
||||
resizeOptions = height < width ? { height: targetSize } : { width: targetSize };
|
||||
resizeOptions =
|
||||
height < width ? { height: targetSize } : { width: targetSize };
|
||||
} else {
|
||||
resizeOptions = { height: targetSize };
|
||||
}
|
||||
@ -80,7 +85,7 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
|
||||
Key: resizedKey,
|
||||
Body: resizedBuffer,
|
||||
ContentType: "image/" + resizedMetadata.format,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
//--- Thumbnail file
|
||||
@ -88,7 +93,10 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
|
||||
|
||||
let thumbnailOptions: { width?: number; height?: number };
|
||||
if (width && height) {
|
||||
thumbnailOptions = height < width ? { height: thumbnailTargetSize } : { width: thumbnailTargetSize };
|
||||
thumbnailOptions =
|
||||
height < width
|
||||
? { height: thumbnailTargetSize }
|
||||
: { width: thumbnailTargetSize };
|
||||
} else {
|
||||
thumbnailOptions = { height: thumbnailTargetSize };
|
||||
}
|
||||
@ -106,7 +114,36 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
|
||||
Key: thumbnailKey,
|
||||
Body: thumbnailBuffer,
|
||||
ContentType: "image/" + thumbnailMetadata.format,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
//--- Gallery file
|
||||
const galleryTargetSize = 300;
|
||||
|
||||
let galleryOptions: { width?: number; height?: number };
|
||||
if (width && height) {
|
||||
galleryOptions =
|
||||
height < width
|
||||
? { height: galleryTargetSize }
|
||||
: { width: galleryTargetSize };
|
||||
} else {
|
||||
galleryOptions = { height: galleryTargetSize };
|
||||
}
|
||||
|
||||
const galleryBuffer = await sharp(modifiedBuffer)
|
||||
.resize({ ...galleryOptions, withoutEnlargement: true })
|
||||
.toFormat("webp")
|
||||
.toBuffer();
|
||||
|
||||
const galleryMetadata = await sharp(galleryBuffer).metadata();
|
||||
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: `${process.env.BUCKET_NAME}`,
|
||||
Key: galleryKey,
|
||||
Body: galleryBuffer,
|
||||
ContentType: "image/" + galleryMetadata.format,
|
||||
}),
|
||||
);
|
||||
|
||||
const fileRecord = await prisma.fileData.create({
|
||||
@ -193,6 +230,16 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
|
||||
sizeBytes: thumbnailMetadata.size,
|
||||
artworkId: artworkRecord.id,
|
||||
},
|
||||
{
|
||||
s3Key: galleryKey,
|
||||
type: "gallery",
|
||||
height: galleryMetadata.height ?? 0,
|
||||
width: galleryMetadata.width ?? 0,
|
||||
fileExtension: galleryMetadata.format,
|
||||
mimeType: "image/" + galleryMetadata.format,
|
||||
sizeBytes: galleryMetadata.size,
|
||||
artworkId: artworkRecord.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@ -206,6 +253,5 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
|
||||
// (nothing else to do here)
|
||||
}
|
||||
|
||||
|
||||
return artworkRecord;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user