From 7c767b580a1d32009fdf23b72799a35fee46d6da Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:18:59 +0100 Subject: [PATCH] feat: extract media dimensions and video thumbnails on upload --- packages/server/src/routes/uploads.ts | 39 +++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/server/src/routes/uploads.ts b/packages/server/src/routes/uploads.ts index 73a10bb0..762e9362 100644 --- a/packages/server/src/routes/uploads.ts +++ b/packages/server/src/routes/uploads.ts @@ -8,7 +8,7 @@ import fs from 'fs'; import path from 'path'; import { pipeline } from 'stream/promises'; import type { Attachment } from '@backspace/shared'; -import { generateThumbnail, isResizableImage } from '../utils/thumbnail.js'; +import { generateThumbnail, isResizableImage, probeImageDimensions, probeMediaMeta, generateVideoThumbnail } from '../utils/thumbnail.js'; const EXT_MIMETYPES: Record = { '.webp': 'image/webp', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', @@ -69,10 +69,39 @@ export async function uploadRoutes(app: FastifyInstance): Promise { const now = Date.now(); const db = getDb(); - // Generate thumbnail for resizable images (non-blocking — upload succeeds regardless) + // ─── Media processing ──────────────────────────────────────────────────── let thumbnailFilename: string | null = null; + let width: number | null = null; + let height: number | null = null; + let duration: number | null = null; + if (isResizableImage(mimetype)) { + // Image: generate thumbnail (unchanged) + probe dimensions thumbnailFilename = await generateThumbnail(filepath, mimetype, config.uploadDir); + const dims = await probeImageDimensions(filepath); + if (dims) { + width = dims.width; + height = dims.height; + } + } else if (mimetype.startsWith('video/')) { + // Video: generate thumbnail frame + probe duration + const videoThumb = await generateVideoThumbnail(filepath, config.uploadDir); + if (videoThumb) { + thumbnailFilename = videoThumb.thumbnailFilename; + width = videoThumb.width; + height = videoThumb.height; + } + // Duration (and fallback dimensions if thumbnail failed) + const meta = await probeMediaMeta(filepath, mimetype); + if (meta) { + duration = meta.duration ?? null; + if (width === null && meta.width) width = meta.width; + if (height === null && meta.height) height = meta.height; + } + } else if (mimetype.startsWith('audio/')) { + // Audio: duration only + const meta = await probeMediaMeta(filepath, mimetype); + if (meta?.duration) duration = meta.duration; } // Save attachment record @@ -84,6 +113,9 @@ export async function uploadRoutes(app: FastifyInstance): Promise { mimetype, size, thumbnailFilename, + width, + height, + duration, createdAt: now, }).run(); @@ -95,6 +127,9 @@ export async function uploadRoutes(app: FastifyInstance): Promise { mimetype, size, thumbnailFilename: thumbnailFilename ?? undefined, + width: width ?? undefined, + height: height ?? undefined, + duration: duration ?? undefined, createdAt: now, };