feat: extract media dimensions and video thumbnails on upload
This commit is contained in:
@@ -8,7 +8,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { pipeline } from 'stream/promises';
|
import { pipeline } from 'stream/promises';
|
||||||
import type { Attachment } from '@backspace/shared';
|
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<string, string> = {
|
const EXT_MIMETYPES: Record<string, string> = {
|
||||||
'.webp': 'image/webp', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
'.webp': 'image/webp', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
||||||
@@ -69,10 +69,39 @@ export async function uploadRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
|
|
||||||
// Generate thumbnail for resizable images (non-blocking — upload succeeds regardless)
|
// ─── Media processing ────────────────────────────────────────────────────
|
||||||
let thumbnailFilename: string | null = null;
|
let thumbnailFilename: string | null = null;
|
||||||
|
let width: number | null = null;
|
||||||
|
let height: number | null = null;
|
||||||
|
let duration: number | null = null;
|
||||||
|
|
||||||
if (isResizableImage(mimetype)) {
|
if (isResizableImage(mimetype)) {
|
||||||
|
// Image: generate thumbnail (unchanged) + probe dimensions
|
||||||
thumbnailFilename = await generateThumbnail(filepath, mimetype, config.uploadDir);
|
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
|
// Save attachment record
|
||||||
@@ -84,6 +113,9 @@ export async function uploadRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
mimetype,
|
mimetype,
|
||||||
size,
|
size,
|
||||||
thumbnailFilename,
|
thumbnailFilename,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
duration,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
}).run();
|
}).run();
|
||||||
|
|
||||||
@@ -95,6 +127,9 @@ export async function uploadRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
mimetype,
|
mimetype,
|
||||||
size,
|
size,
|
||||||
thumbnailFilename: thumbnailFilename ?? undefined,
|
thumbnailFilename: thumbnailFilename ?? undefined,
|
||||||
|
width: width ?? undefined,
|
||||||
|
height: height ?? undefined,
|
||||||
|
duration: duration ?? undefined,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user