fix(export): name the camera master in photo exports, not the delivered render (#1229) (#1230)

#1165 added photos.source_filename to this service's select, with a comment
saying it was there so the Lightroom round-trip could still match after a
re-upload — and then nothing read it. Every output path still used
original_filename, which is overwritten the first time an edited render is
uploaded over a proof (#745).

So after a round-trip the exports named the render. Each of these formats
exists to help a photographer find the master on disk, and the render's name
does not. The XMP case is the sharpest: the sidecar is written next to a RAW
master, so a wrongly-named one is never associated with it.

Two helpers, because the sites want different things when nothing is known:

  cameraName()             source_filename || original_filename || null
  cameraFilenameOrStored() the above, else the stored name

The dedicated `original_filename` fields (CSV column, JSON key) keep reporting
blank/null when unrecorded — echoing the sanitized stored name there would
invite a match against a file that does not exist under it. The places that
must emit some name (text list, CSV filename cell, XMP sidecar) fall back to
the stored one, as they did before.

filename_format='stored' is untouched, and rows with no source_filename still
resolve to original_filename, so nothing moves for installs that have never
run a replacement.

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-08-29 11:10:05 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent 4f684eb482
commit f4c054a661
2 changed files with 169 additions and 5 deletions
+33 -5
View File
@@ -14,6 +14,32 @@ const { db } = require('../database/db');
const path = require('path');
const fs = require('fs').promises;
/**
* The name the camera gave the file, or null when nothing was recorded (#1229).
*
* `source_filename` first. `original_filename` is overwritten the first time an
* edited render is uploaded over a proof (#745), so after a Lightroom
* round-trip it holds the render's name — and every export here is for finding
* the master on disk, which that name no longer does. `source_filename` is
* written once at ingest and survives a replace by design (migration 193).
*
* Null rather than the stored name: this feeds the two dedicated
* "original_filename" fields, where blank honestly reports "not recorded"
* instead of echoing a sanitized name that matches nothing.
*/
function cameraName(photo) {
return photo.source_filename || photo.original_filename || null;
}
/**
* A filename to actually write: the camera name when known, else the stored
* one. Used where the output needs *some* name — the text list, the CSV's
* filename cell, the XMP sidecar — and an empty string would be useless.
*/
function cameraFilenameOrStored(photo) {
return cameraName(photo) || photo.filename;
}
class PhotoExportService {
constructor() {
this.xmpGenerator = new XmpGenerator();
@@ -135,7 +161,7 @@ class PhotoExportService {
const filenames = photos.map(photo => {
const name = filename_format === 'original'
? (photo.original_filename || photo.filename)
? cameraFilenameOrStored(photo)
: photo.filename;
return include_extension ? name : path.parse(name).name;
});
@@ -185,8 +211,8 @@ class PhotoExportService {
];
const rows = photos.map(photo => [
filename_format === 'original' ? (photo.original_filename || photo.filename) : photo.filename,
photo.original_filename || '',
filename_format === 'original' ? cameraFilenameOrStored(photo) : photo.filename,
cameraName(photo) || '',
photo.average_rating ? parseFloat(photo.average_rating).toFixed(2) : '0.00',
photo.feedback_count || 0,
photo.like_count || 0,
@@ -239,8 +265,10 @@ class PhotoExportService {
archive.pipe(passthrough);
for (const photo of photos) {
// The sidecar is written next to a RAW master. Naming it after the
// edited render means Lightroom never associates the two.
const baseFilename = filename_format === 'original'
? (photo.original_filename || photo.filename)
? cameraFilenameOrStored(photo)
: photo.filename;
const xmpFilename = this.xmpGenerator.getXmpFilename(baseFilename);
const xmpContent = this.xmpGenerator.generateXmp(project(photo), options);
@@ -279,7 +307,7 @@ class PhotoExportService {
photos: photos.map(photo => ({
id: photo.id,
filename: photo.filename,
original_filename: photo.original_filename || null,
original_filename: cameraName(photo),
category: photo.category_name || null,
rating: {
average: photo.average_rating ? parseFloat(parseFloat(photo.average_rating).toFixed(2)) : 0,