feat: add original filename preservation and Lightroom export support
Addresses GitHub issue #132 - enables filtering client feedback and exporting filenames for use in Lightroom. Changes: - Add original_filename column to photos table via migration - Store original filename during photo upload - Fix export service column name mismatches (path, size_bytes, uploaded_at) - Fix table name (photo_categories instead of categories) - Fix toFixed() calls to handle string ratings from database Export formats available: - TXT with comma separator (for Lightroom Library Filter) - CSV with full metadata - JSON for automation - XMP sidecar files (for Lightroom/Bridge/Capture One)
This commit is contained in:
@@ -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.
|
||||||
|
};
|
||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user