feat: add asset variants table and URL selection
This commit is contained in:
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { getDb } from "@tline/db";
|
||||
import { presignGetObjectUrl } from "@tline/minio";
|
||||
import { pickVariantKey } from "./variant";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
@@ -9,7 +10,15 @@ const paramsSchema = z.object({
|
||||
id: z.string().uuid()
|
||||
});
|
||||
|
||||
const variantSchema = z.enum(["original", "thumb_small", "thumb_med", "poster"]);
|
||||
const legacyVariantSchema = z.enum(["original", "thumb_small", "thumb_med", "poster"]);
|
||||
const kindSchema = z.enum(["original", "thumb", "poster", "video_mp4"]);
|
||||
const sizeSchema = z.coerce.number().int().positive();
|
||||
const legacyVariantMap = {
|
||||
original: { kind: "original" as const },
|
||||
thumb_small: { kind: "thumb" as const, size: 256 },
|
||||
thumb_med: { kind: "thumb" as const, size: 768 },
|
||||
poster: { kind: "poster" as const, size: 256 },
|
||||
};
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
@@ -26,14 +35,46 @@ export async function GET(
|
||||
const params = paramsParsed.data;
|
||||
|
||||
const url = new URL(request.url);
|
||||
const variantParsed = variantSchema.safeParse(url.searchParams.get("variant") ?? "original");
|
||||
if (!variantParsed.success) {
|
||||
return Response.json(
|
||||
{ error: "invalid_query", issues: variantParsed.error.issues },
|
||||
{ status: 400 },
|
||||
);
|
||||
const kindParam = url.searchParams.get("kind");
|
||||
const sizeParam = url.searchParams.get("size");
|
||||
const legacyVariantParam = url.searchParams.get("variant");
|
||||
|
||||
let requestedKind: z.infer<typeof kindSchema> = "original";
|
||||
let requestedSize: number | null = null;
|
||||
let legacyVariant: z.infer<typeof legacyVariantSchema> | null = null;
|
||||
|
||||
if (kindParam) {
|
||||
const kindParsed = kindSchema.safeParse(kindParam);
|
||||
if (!kindParsed.success) {
|
||||
return Response.json(
|
||||
{ error: "invalid_query", issues: kindParsed.error.issues },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
requestedKind = kindParsed.data;
|
||||
if (requestedKind !== "original") {
|
||||
const sizeParsed = sizeSchema.safeParse(sizeParam);
|
||||
if (!sizeParsed.success) {
|
||||
return Response.json(
|
||||
{ error: "invalid_query", issues: sizeParsed.error.issues },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
requestedSize = sizeParsed.data;
|
||||
}
|
||||
} else if (legacyVariantParam) {
|
||||
const legacyParsed = legacyVariantSchema.safeParse(legacyVariantParam);
|
||||
if (!legacyParsed.success) {
|
||||
return Response.json(
|
||||
{ error: "invalid_query", issues: legacyParsed.error.issues },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
legacyVariant = legacyParsed.data;
|
||||
const mapped = legacyVariantMap[legacyVariant];
|
||||
requestedKind = mapped.kind;
|
||||
requestedSize = "size" in mapped ? mapped.size : null;
|
||||
}
|
||||
const variant = variantParsed.data;
|
||||
|
||||
const db = getDb();
|
||||
const rows = await db<
|
||||
@@ -52,32 +93,70 @@ export async function GET(
|
||||
limit 1
|
||||
`;
|
||||
|
||||
const variants = await db<
|
||||
{
|
||||
kind: string;
|
||||
size: number;
|
||||
key: string;
|
||||
mime_type: string;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
}[]
|
||||
>`
|
||||
select kind, size, key, mime_type, width, height
|
||||
from asset_variants
|
||||
where asset_id = ${params.id}
|
||||
`;
|
||||
|
||||
const asset = rows[0];
|
||||
if (!asset) {
|
||||
return Response.json({ error: "not_found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const legacyKey =
|
||||
legacyVariant === "thumb_small"
|
||||
? asset.thumb_small_key
|
||||
: legacyVariant === "thumb_med"
|
||||
? asset.thumb_med_key
|
||||
: legacyVariant === "poster"
|
||||
? asset.poster_key
|
||||
: null;
|
||||
|
||||
const key =
|
||||
variant === "original"
|
||||
requestedKind === "original"
|
||||
? asset.active_key
|
||||
: variant === "thumb_small"
|
||||
? asset.thumb_small_key
|
||||
: variant === "thumb_med"
|
||||
? asset.thumb_med_key
|
||||
: asset.poster_key;
|
||||
: requestedSize !== null
|
||||
? pickVariantKey(
|
||||
{ variants },
|
||||
{ kind: requestedKind, size: requestedSize },
|
||||
) ?? legacyKey
|
||||
: null;
|
||||
|
||||
if (!key) {
|
||||
return Response.json(
|
||||
{ error: "variant_not_available", variant },
|
||||
{ error: "variant_not_available", kind: requestedKind, size: requestedSize },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hint the browser; especially helpful for Range playback.
|
||||
const responseContentType = variant === "original" ? asset.mime_type : "image/jpeg";
|
||||
const matchedVariant =
|
||||
requestedKind === "original" || requestedSize === null
|
||||
? null
|
||||
: variants.find(
|
||||
(item) => item.kind === requestedKind && item.size === requestedSize,
|
||||
) ?? null;
|
||||
const responseContentType =
|
||||
requestedKind === "original"
|
||||
? asset.mime_type
|
||||
: matchedVariant?.mime_type ??
|
||||
(requestedKind === "video_mp4" ? "video/mp4" : "image/jpeg");
|
||||
|
||||
const responseContentDisposition =
|
||||
variant === "original" && asset.mime_type.startsWith("video/") ? "inline" : undefined;
|
||||
(requestedKind === "original" && asset.mime_type.startsWith("video/")) ||
|
||||
requestedKind === "video_mp4"
|
||||
? "inline"
|
||||
: undefined;
|
||||
|
||||
const signed = await presignGetObjectUrl({
|
||||
bucket: asset.bucket,
|
||||
|
||||
9
apps/web/app/api/assets/[id]/url/variant.ts
Normal file
9
apps/web/app/api/assets/[id]/url/variant.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function pickVariantKey(
|
||||
input: { variants: Array<{ kind: string; size: number; key: string }> },
|
||||
req: { kind: string; size: number },
|
||||
) {
|
||||
const v = input.variants.find(
|
||||
(x) => x.kind === req.kind && x.size === req.size,
|
||||
);
|
||||
return v?.key ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user