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 { 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 { 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 => Bun.write(thumbnailPath, thumbnailBuffer.buffer).then( (): Promise => 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 { 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 => { await createThumbnails(event.data.files); }; self.onerror = (error: ErrorEvent): void => { logger.error(["An error occurred in the thumbnail worker:", error.message]); };