import { Router } from "@oak/oak"; import { APIErrorCode, APIException } from "../model/interfaces.ts"; import { getDump } from "../services/dump-service.ts"; import { DUMPS_DIR, THUMBNAILS_DIR } from "../config.ts"; const router = new Router({ prefix: "/api/thumbnails" }); async function ffmpegAvailable(): Promise { try { const cmd = new Deno.Command("ffmpeg", { args: ["-version"], stderr: "null", stdout: "null", }); const { success } = await cmd.output(); return success; } catch { return false; } } async function generateThumbnail( srcPath: string, outPath: string, ): Promise { await Deno.mkdir(THUMBNAILS_DIR, { recursive: true }); const cmd = new Deno.Command("ffmpeg", { args: [ "-ss", "00:00:01", "-i", srcPath, "-frames:v", "1", "-vf", "scale=320:-1", "-f", "image2", "-y", outPath, ], stdout: "null", stderr: "null", }); const { success } = await cmd.output(); if (!success) throw new Error("ffmpeg failed"); } router.get("/:dumpId", async (ctx) => { const { dumpId } = ctx.params; if (!/^[0-9a-f-]{36}$/.test(dumpId)) { throw new APIException(APIErrorCode.BAD_REQUEST, 400, "Invalid dump ID"); } const dump = getDump(dumpId); if (dump.kind !== "file" || !dump.fileMime?.startsWith("video/")) { throw new APIException( APIErrorCode.NOT_FOUND, 404, "No video for this dump", ); } const thumbPath = `${THUMBNAILS_DIR}/${dumpId}.jpg`; // Serve cached thumbnail if it exists try { const data = await Deno.readFile(thumbPath); ctx.response.headers.set("Content-Type", "image/jpeg"); ctx.response.headers.set( "Cache-Control", "public, max-age=31536000, immutable", ); ctx.response.body = data; return; } catch { // Not cached yet — generate it } if (!await ffmpegAvailable()) { throw new APIException( APIErrorCode.NOT_FOUND, 404, "Thumbnail generation unavailable", ); } const srcPath = `${DUMPS_DIR}/${dumpId}`; try { await generateThumbnail(srcPath, thumbPath); } catch { throw new APIException( APIErrorCode.NOT_FOUND, 404, "Could not generate thumbnail", ); } const data = await Deno.readFile(thumbPath); ctx.response.headers.set("Content-Type", "image/jpeg"); ctx.response.headers.set( "Cache-Control", "public, max-age=31536000, immutable", ); ctx.response.body = data; }); export default router;