import { z } from "zod"; import { getDb } from "@tline/db"; import { presignGetObjectUrl } from "@tline/minio"; import { pickLegacyKeyForRequest, pickVariantKey } from "./variant"; export const runtime = "nodejs"; const paramsSchema = z.object({ id: z.string().uuid() }); 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 videoMp4DefaultSize = 720; 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, context: { params: Promise<{ id: string }> } ): Promise { const rawParams = await context.params; const paramsParsed = paramsSchema.safeParse(rawParams); if (!paramsParsed.success) { return Response.json( { error: "invalid_params", issues: paramsParsed.error.issues }, { status: 400 }, ); } const params = paramsParsed.data; const url = new URL(request.url); const kindParam = url.searchParams.get("kind"); const sizeParam = url.searchParams.get("size"); const legacyVariantParam = url.searchParams.get("variant"); let requestedKind: z.infer = "original"; let requestedSize: number | null = null; let legacyVariant: z.infer | 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") { if (requestedKind === "video_mp4" && !sizeParam) { requestedSize = videoMp4DefaultSize; } else { 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 db = getDb(); const rows = await db< { bucket: string; active_key: string; thumb_small_key: string | null; thumb_med_key: string | null; poster_key: string | null; mime_type: string; }[] >` select bucket, active_key, thumb_small_key, thumb_med_key, poster_key, mime_type from assets where id = ${params.id} 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 ? pickLegacyKeyForRequest( { asset }, { kind: requestedKind, size: requestedSize ?? 0 }, ) : requestedSize !== null ? pickLegacyKeyForRequest( { asset }, { kind: requestedKind, size: requestedSize }, ) : null; const key = requestedKind === "original" ? asset.active_key : requestedSize !== null ? pickVariantKey( { variants }, { kind: requestedKind, size: requestedSize }, ) ?? legacyKey : null; if (!key) { return Response.json( { error: "variant_not_available", kind: requestedKind, size: requestedSize }, { status: 404 } ); } // Hint the browser; especially helpful for Range playback. 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 = (requestedKind === "original" && asset.mime_type.startsWith("video/")) || requestedKind === "video_mp4" ? "inline" : undefined; const signed = await presignGetObjectUrl({ bucket: asset.bucket, key, responseContentType, responseContentDisposition, }); return Response.json(signed, { headers: { "Cache-Control": "no-store" } }); }