Add video support, media filters, and translations
This commit is contained in:
@@ -3,8 +3,10 @@ const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { db } = require('../database/db');
|
||||
const { formatBoolean } = require('../utils/dbCompat');
|
||||
const { generateThumbnail } = require('./imageProcessor');
|
||||
const { generateThumbnail, generateVideoPlaceholder } = require('./imageProcessor');
|
||||
const logger = require('../utils/logger');
|
||||
const { isVideoMimeType } = require('../utils/fileSecurityUtils');
|
||||
const mime = require('mime-types');
|
||||
|
||||
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
const WATCH_PATH = () => path.join(getStoragePath(), 'events/active');
|
||||
@@ -47,9 +49,11 @@ async function processNewPhoto(filePath) {
|
||||
const eventSlug = pathParts[0];
|
||||
const photoType = pathParts[1] === 'collages' ? 'collage' : 'individual';
|
||||
|
||||
// Check if this is an image file
|
||||
// Check if this is an image or video file
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (!['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) return;
|
||||
const detectedMime = mime.lookup(filePath) || '';
|
||||
const isVideo = isVideoMimeType(detectedMime, filePath) || ['.mp4', '.mov', '.webm'].includes(ext);
|
||||
if (!isVideo && !['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) return;
|
||||
|
||||
// Skip temporary upload files
|
||||
const filename = path.basename(filePath);
|
||||
@@ -65,11 +69,17 @@ async function processNewPhoto(filePath) {
|
||||
// Get file stats
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
// Generate thumbnail
|
||||
const thumbnailPath = await generateThumbnail(filePath);
|
||||
// Generate thumbnail or placeholder
|
||||
let thumbnailPath = null;
|
||||
if (isVideo) {
|
||||
thumbnailPath = await generateVideoPlaceholder(filename);
|
||||
} else {
|
||||
thumbnailPath = await generateThumbnail(filePath);
|
||||
}
|
||||
|
||||
// Calculate relative thumbnail path
|
||||
const relativeThumbPath = thumbnailPath; // thumbnailPath is already relative to storage root
|
||||
const mimeType = detectedMime || (isVideo ? 'video/mp4' : 'image/jpeg');
|
||||
|
||||
// Check if photo already exists
|
||||
const existingPhoto = await db('photos')
|
||||
@@ -83,8 +93,9 @@ async function processNewPhoto(filePath) {
|
||||
filename: path.basename(filePath),
|
||||
path: relativePath,
|
||||
thumbnail_path: relativeThumbPath,
|
||||
type: photoType,
|
||||
size_bytes: stats.size
|
||||
type: isVideo ? 'video' : photoType,
|
||||
size_bytes: stats.size,
|
||||
mime_type: mimeType
|
||||
});
|
||||
|
||||
logger.info(`Added new photo: ${relativePath}`);
|
||||
|
||||
@@ -237,4 +237,54 @@ async function ensureThumbnail(photo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { generateThumbnail, isThumbnailValid, ensureThumbnail };
|
||||
async function generateVideoPlaceholder(originalFilename, options = {}) {
|
||||
const parsed = path.parse(originalFilename || '');
|
||||
const baseName = parsed.name || 'video';
|
||||
const thumbnailDir = getThumbnailPath();
|
||||
const thumbnailFilename = `thumb_${baseName}.jpg`;
|
||||
const thumbnailPath = path.join(thumbnailDir, thumbnailFilename);
|
||||
|
||||
const settings = await getThumbnailSettings();
|
||||
const width = settings.width || DEFAULT_THUMBNAIL_WIDTH;
|
||||
const height = settings.height || DEFAULT_THUMBNAIL_HEIGHT;
|
||||
|
||||
if (options.regenerate) {
|
||||
try {
|
||||
await fs.unlink(thumbnailPath);
|
||||
} catch (_) {
|
||||
// ignore if missing
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.mkdir(thumbnailDir, { recursive: true });
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0f172a" stop-opacity="0.9"/>
|
||||
<stop offset="100%" stop-color="#1e293b" stop-opacity="0.9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="${width}" height="${height}" rx="18" fill="url(#grad)"/>
|
||||
<circle cx="${width / 2}" cy="${height / 2}" r="${Math.min(width, height) / 6}" fill="rgba(255,255,255,0.85)"/>
|
||||
<polygon points="${width / 2 - 10},${height / 2 - 14} ${width / 2 - 10},${height / 2 + 14} ${width / 2 + 16},${height / 2}" fill="#0f172a"/>
|
||||
<text x="50%" y="${height - 18}" font-family="Arial, sans-serif" font-size="16" fill="rgba(255,255,255,0.9)" text-anchor="middle">
|
||||
VIDEO
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
await sharp(Buffer.from(svg))
|
||||
.resize(width, height, { fit: 'cover' })
|
||||
.jpeg({ quality: settings.quality || DEFAULT_THUMBNAIL_QUALITY })
|
||||
.toFile(thumbnailPath);
|
||||
|
||||
return path.relative(getStoragePath(), thumbnailPath);
|
||||
} catch (error) {
|
||||
logger.error('Failed to generate video placeholder thumbnail:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { generateThumbnail, isThumbnailValid, ensureThumbnail, generateVideoPlaceholder };
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { db } = require('../database/db');
|
||||
const { generateThumbnail } = require('./imageProcessor');
|
||||
const { generateThumbnail, generateVideoPlaceholder } = require('./imageProcessor');
|
||||
const { generatePhotoFilename } = require('../utils/filenameSanitizer');
|
||||
const { isVideoMimeType } = require('../utils/fileSecurityUtils');
|
||||
const mime = require('mime-types');
|
||||
|
||||
// Get storage path from environment or default
|
||||
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
@@ -70,12 +72,15 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
|
||||
const trx = await db.transaction();
|
||||
|
||||
try {
|
||||
const resolvedMime = file?.mimetype || mime.lookup(file?.originalname || '') || 'application/octet-stream';
|
||||
const isVideo = isVideoMimeType(resolvedMime, file?.originalname);
|
||||
|
||||
// Count existing photos to generate sequence number
|
||||
let counter = 1;
|
||||
let photoType = 'individual'; // default type
|
||||
let photoType = isVideo ? 'video' : 'individual'; // default type
|
||||
|
||||
// If categoryId is provided and matches photo types, use it as type
|
||||
if (categoryId === 'collage') {
|
||||
if (!isVideo && categoryId === 'collage') {
|
||||
photoType = 'collage';
|
||||
}
|
||||
|
||||
@@ -90,7 +95,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
|
||||
|
||||
// Generate new filename
|
||||
const extension = path.extname(file.originalname);
|
||||
const categoryName = photoType === 'collage' ? 'collages' : 'individual';
|
||||
const categoryName = photoType === 'collage' ? 'collages' : (isVideo ? 'videos' : 'individual');
|
||||
const newFilename = generatePhotoFilename(
|
||||
event.event_name,
|
||||
categoryName,
|
||||
@@ -150,8 +155,13 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
|
||||
}
|
||||
}
|
||||
|
||||
// Generate thumbnail
|
||||
const thumbnailPath = await generateThumbnail(newPath);
|
||||
// Generate thumbnail or placeholder
|
||||
let thumbnailPath = null;
|
||||
if (isVideo) {
|
||||
thumbnailPath = await generateVideoPlaceholder(newFilename);
|
||||
} else {
|
||||
thumbnailPath = await generateThumbnail(newPath);
|
||||
}
|
||||
|
||||
// Calculate relative paths
|
||||
const storagePath = getStoragePath();
|
||||
@@ -173,20 +183,22 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
|
||||
type: photoType,
|
||||
size_bytes: file.size,
|
||||
uploaded_by: uploadedBy,
|
||||
source_origin: 'managed'
|
||||
source_origin: 'managed',
|
||||
mime_type: resolvedMime
|
||||
})
|
||||
.returning('id');
|
||||
} else {
|
||||
insertResult = await trx('photos').insert({
|
||||
event_id: eventId,
|
||||
filename: newFilename,
|
||||
path: relativePath,
|
||||
thumbnail_path: relativeThumbPath,
|
||||
type: photoType,
|
||||
size_bytes: file.size,
|
||||
uploaded_by: uploadedBy,
|
||||
source_origin: 'managed'
|
||||
});
|
||||
path: relativePath,
|
||||
thumbnail_path: relativeThumbPath,
|
||||
type: photoType,
|
||||
size_bytes: file.size,
|
||||
uploaded_by: uploadedBy,
|
||||
source_origin: 'managed',
|
||||
mime_type: resolvedMime
|
||||
});
|
||||
}
|
||||
|
||||
const insertedId = Array.isArray(insertResult)
|
||||
@@ -206,7 +218,8 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
|
||||
id: photoId,
|
||||
filename: newFilename,
|
||||
size: file.size,
|
||||
type: photoType
|
||||
type: photoType,
|
||||
media_type: isVideo ? 'video' : 'photo'
|
||||
});
|
||||
|
||||
console.log(`Successfully processed file ${file.originalname} (ID: ${photoId})`);
|
||||
|
||||
Reference in New Issue
Block a user