Files
gerbeur/api/routes/thumbnails.ts

111 lines
2.5 KiB
TypeScript

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<boolean> {
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<void> {
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;