Merge pull request #134 from the-luap/feat/optional-event-date-expiration-beta

feat: add original filename preservation and Lightroom export support
This commit is contained in:
Paul Nothaft
2026-01-21 17:06:06 +01:00
committed by GitHub
6 changed files with 49 additions and 24 deletions
@@ -0,0 +1,23 @@
/**
* Migration 062: Add original_filename to photos table
* - photos.original_filename: preserves the original filename from upload
* This enables Lightroom integration by exporting filtered filenames
*/
const { addColumnIfNotExists } = require('../helpers');
exports.up = async function(knex) {
console.log('Running migration: 062_add_original_filename');
// photos.original_filename (nullable - original filename before renaming)
await addColumnIfNotExists(knex, 'photos', 'original_filename', (table) => {
table.string('original_filename', 512);
});
console.log('Migration 062_add_original_filename completed');
};
exports.down = async function(knex) {
console.log('Rollback: 062_add_original_filename');
// Keep column (safe rollback not removing data). Intentionally no-op.
};
+4 -4
View File
@@ -71,12 +71,12 @@ router.get('/:eventId/filtered', adminAuth, requirePermission('photos.view'), [
// Build filtered query // Build filtered query
const filterBuilder = new PhotoFilterBuilder( const filterBuilder = new PhotoFilterBuilder(
db('photos') db('photos')
.leftJoin('categories', 'photos.category_id', 'categories.id') .leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id')
.select( .select(
'photos.id', 'photos.id',
'photos.filename', 'photos.filename',
'photos.original_filename', 'photos.original_filename',
'photos.file_path', 'photos.path',
'photos.average_rating', 'photos.average_rating',
'photos.feedback_count', 'photos.feedback_count',
'photos.like_count', 'photos.like_count',
@@ -84,8 +84,8 @@ router.get('/:eventId/filtered', adminAuth, requirePermission('photos.view'), [
'photos.comment_count', 'photos.comment_count',
'photos.width', 'photos.width',
'photos.height', 'photos.height',
'photos.created_at', 'photos.uploaded_at',
'categories.name as category_name' 'photo_categories.name as category_name'
), ),
eventId eventId
); );
+1
View File
@@ -257,6 +257,7 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), u
const photoData = { const photoData = {
event_id: parseInt(eventId), event_id: parseInt(eventId),
filename: newFilename, filename: newFilename,
original_filename: file.originalname, // Preserve original filename for Lightroom export
path: relativePath, path: relativePath,
thumbnail_path: null, // Will generate after successful commit thumbnail_path: null, // Will generate after successful commit
type: photoType, type: photoType,
+19 -19
View File
@@ -23,13 +23,13 @@ class PhotoExportService {
*/ */
async getPhotosWithFeedback(eventId, photoIds = null) { async getPhotosWithFeedback(eventId, photoIds = null) {
let query = db('photos') let query = db('photos')
.leftJoin('categories', 'photos.category_id', 'categories.id') .leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id')
.where('photos.event_id', eventId) .where('photos.event_id', eventId)
.select( .select(
'photos.id', 'photos.id',
'photos.filename', 'photos.filename',
'photos.original_filename', 'photos.original_filename',
'photos.file_path', 'photos.path',
'photos.average_rating', 'photos.average_rating',
'photos.feedback_count', 'photos.feedback_count',
'photos.like_count', 'photos.like_count',
@@ -37,9 +37,9 @@ class PhotoExportService {
'photos.comment_count', 'photos.comment_count',
'photos.width', 'photos.width',
'photos.height', 'photos.height',
'photos.file_size', 'photos.size_bytes',
'photos.created_at', 'photos.uploaded_at',
'categories.name as category_name' 'photo_categories.name as category_name'
) )
.orderBy('photos.filename', 'asc'); .orderBy('photos.filename', 'asc');
@@ -86,7 +86,7 @@ class PhotoExportService {
const { filename_format = 'original', separator = 'newline' } = options; const { filename_format = 'original', separator = 'newline' } = options;
const filenames = photos.map(photo => const filenames = photos.map(photo =>
filename_format === 'original' ? photo.original_filename : photo.filename filename_format === 'original' ? (photo.original_filename || photo.filename) : photo.filename
); );
let content; let content;
@@ -126,14 +126,14 @@ class PhotoExportService {
'category', 'category',
'width', 'width',
'height', 'height',
'file_size', 'size_bytes',
'created_at' 'uploaded_at'
]; ];
const rows = photos.map(photo => [ const rows = photos.map(photo => [
filename_format === 'original' ? photo.original_filename : photo.filename, filename_format === 'original' ? (photo.original_filename || photo.filename) : photo.filename,
photo.original_filename || '', photo.original_filename || '',
photo.average_rating ? photo.average_rating.toFixed(2) : '0.00', photo.average_rating ? parseFloat(photo.average_rating).toFixed(2) : '0.00',
photo.feedback_count || 0, photo.feedback_count || 0,
photo.like_count || 0, photo.like_count || 0,
photo.favorite_count || 0, photo.favorite_count || 0,
@@ -141,8 +141,8 @@ class PhotoExportService {
photo.category_name || '', photo.category_name || '',
photo.width || '', photo.width || '',
photo.height || '', photo.height || '',
photo.file_size || '', photo.size_bytes || '',
photo.created_at ? new Date(photo.created_at).toISOString() : '' photo.uploaded_at ? new Date(photo.uploaded_at).toISOString() : ''
]); ]);
const csvContent = [ const csvContent = [
@@ -170,7 +170,7 @@ class PhotoExportService {
for (const photo of photos) { for (const photo of photos) {
const baseFilename = filename_format === 'original' const baseFilename = filename_format === 'original'
? photo.original_filename ? (photo.original_filename || photo.filename)
: photo.filename; : photo.filename;
const xmpFilename = this.xmpGenerator.getXmpFilename(baseFilename); const xmpFilename = this.xmpGenerator.getXmpFilename(baseFilename);
const xmpContent = this.xmpGenerator.generateXmp(photo, options); const xmpContent = this.xmpGenerator.generateXmp(photo, options);
@@ -209,21 +209,21 @@ class PhotoExportService {
photos: photos.map(photo => ({ photos: photos.map(photo => ({
id: photo.id, id: photo.id,
filename: photo.filename, filename: photo.filename,
original_filename: photo.original_filename, original_filename: photo.original_filename || null,
category: photo.category_name || null, category: photo.category_name || null,
rating: { rating: {
average: photo.average_rating ? parseFloat(photo.average_rating.toFixed(2)) : 0, average: photo.average_rating ? parseFloat(parseFloat(photo.average_rating).toFixed(2)) : 0,
count: photo.feedback_count || 0 count: photo.feedback_count || 0
}, },
likes: photo.like_count || 0, likes: photo.like_count || 0,
favorites: photo.favorite_count || 0, favorites: photo.favorite_count || 0,
comments: photo.comment_count || 0, comments: photo.comment_count || 0,
dimensions: { dimensions: {
width: photo.width, width: photo.width || null,
height: photo.height height: photo.height || null
}, },
file_size: photo.file_size, size_bytes: photo.size_bytes || null,
created_at: photo.created_at uploaded_at: photo.uploaded_at || null
})) }))
}; };
+1
View File
@@ -186,6 +186,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
const photoData = { const photoData = {
event_id: eventId, event_id: eventId,
filename: newFilename, filename: newFilename,
original_filename: file.originalname,
path: relativePath, path: relativePath,
thumbnail_path: relativeThumbPath, thumbnail_path: relativeThumbPath,
type: photoType, type: photoType,
+1 -1
View File
@@ -76,7 +76,7 @@ class XmpGenerator {
* @returns {string} Description XML * @returns {string} Description XML
*/ */
generateDescription(photo) { generateDescription(photo) {
const rating = photo.average_rating ? photo.average_rating.toFixed(1) : '0'; const rating = photo.average_rating ? parseFloat(photo.average_rating).toFixed(1) : '0';
const likes = photo.like_count || 0; const likes = photo.like_count || 0;
const favorites = photo.favorite_count || 0; const favorites = photo.favorite_count || 0;