backend/src/lib/workers/thumbnails.ts
creations 8a9499be85
All checks were successful
Code quality checks / biome (push) Successful in 8s
re-order alot, move to bun redis, generalized
2025-05-18 09:53:23 -04:00

190 lines
4.6 KiB
TypeScript

import { join, resolve } from "node:path";
import { dataType } from "@config";
import { logger } from "@creations.works/logger";
import { type BunFile, s3, sql } from "bun";
import ffmpeg from "fluent-ffmpeg";
import imageThumbnail from "image-thumbnail";
declare let self: Worker;
async function generateVideoThumbnail(
filePath: string,
thumbnailPath: string,
): Promise<ArrayBuffer | null> {
return new Promise(
(
resolve: (value: ArrayBuffer) => void,
reject: (reason: Error) => void,
): void => {
ffmpeg(filePath)
.videoFilters("thumbnail")
.frames(1)
.format("mjpeg")
.output(thumbnailPath)
.on("error", (error: Error) => {
logger.error(["failed to generate thumbnail", error as Error]);
reject(error);
})
.on("end", async () => {
const thumbnailFile: BunFile = Bun.file(thumbnailPath);
const file: BunFile = Bun.file(filePath);
const thumbnailArraybuffer: ArrayBuffer =
await thumbnailFile.arrayBuffer();
await thumbnailFile.unlink();
await file.unlink();
resolve(thumbnailArraybuffer);
})
.run();
},
);
}
async function generateImageThumbnail(
filePath: string,
thumbnailPath: string,
): Promise<ArrayBuffer> {
return new Promise(
(
resolve: (value: ArrayBuffer) => void,
reject: (reason: Error) => void,
): void => {
const options = {
height: 320,
responseType: "buffer" as const,
jpegOptions: {
force: true,
quality: 60,
},
};
imageThumbnail(filePath, options)
.then(
(thumbnailBuffer: Buffer): Promise<ArrayBuffer> =>
Bun.write(thumbnailPath, thumbnailBuffer.buffer).then(
(): Promise<ArrayBuffer> => Bun.file(thumbnailPath).arrayBuffer(),
),
)
.then((arrayBuffer: ArrayBuffer) => {
resolve(arrayBuffer);
return Promise.all([
Bun.file(filePath).unlink(),
Bun.file(thumbnailPath).unlink(),
]);
})
.catch(reject);
},
);
}
async function createThumbnails(files: FileEntry[]): Promise<void> {
const { type, path } = dataType;
for (const file of files) {
const { id, mime_type } = file;
let fileArrayBuffer: ArrayBuffer | null = null;
const isVideo: boolean = mime_type.startsWith("video/");
const fileName: string = `${id}.${file.extension || ""}`;
if (type === "local") {
const filePath: string | undefined = join(path as string, fileName);
try {
fileArrayBuffer = await Bun.file(filePath).arrayBuffer();
} catch {
logger.error(["Could not generate thumbnail for file:", fileName]);
continue;
}
} else {
try {
fileArrayBuffer = await s3.file(fileName).arrayBuffer();
} catch {
logger.error(["Could not generate thumbnail for file:", fileName]);
continue;
}
}
if (!fileArrayBuffer) {
logger.error(["Could not generate thumbnail for file:", fileName]);
continue;
}
const tempFilePath: string = resolve("temp", `${id}.tmp`);
const tempThumbnailPath: string = resolve("temp", `${id}.jpg`);
try {
await Bun.write(tempFilePath, fileArrayBuffer, {
createPath: true,
});
} catch (error) {
logger.error([
"Could not write file to temp path:",
fileName,
error as Error,
]);
continue;
}
try {
const thumbnailArrayBuffer: ArrayBuffer | null = isVideo
? await generateVideoThumbnail(tempFilePath, tempThumbnailPath)
: await generateImageThumbnail(tempFilePath, tempThumbnailPath);
if (!thumbnailArrayBuffer) {
logger.error(["Could not generate thumbnail for file:", fileName]);
continue;
}
try {
if (type === "local") {
const thumbnailPath: string = join(
path as string,
"thumbnails",
`${id}.jpg`,
);
await Bun.write(thumbnailPath, thumbnailArrayBuffer, {
createPath: true,
});
} else {
const thumbnailPath: string = `thumbnails/${id}.jpg`;
await s3.file(thumbnailPath).write(thumbnailArrayBuffer);
}
} catch (error) {
logger.error([
"Could not write thumbnail to storage:",
fileName,
error as Error,
]);
}
try {
await sql`UPDATE files SET thumbnail = true WHERE id = ${id}`;
} catch (error) {
logger.error([
"Could not update file with thumbnail status:",
fileName,
error as Error,
]);
}
} catch (error) {
logger.error([
"An error occurred while generating thumbnail for file:",
fileName,
error as Error,
]);
}
}
}
self.onmessage = async (event: MessageEvent): Promise<void> => {
await createThumbnails(event.data.files);
};
self.onerror = (error: ErrorEvent): void => {
logger.error(["An error occurred in the thumbnail worker:", error.message]);
};