Skip to content

feat(lightroom-plugin): model Lumio Gallery as a collection set with chapters - #32

Open
manuzzi wants to merge 23 commits into
markusthiel:mainfrom
manuzzi-photo:feat/lightroom-plugin-gallery-chapters
Open

manuzzi wants to merge 23 commits into
markusthiel:mainfrom
manuzzi-photo:feat/lightroom-plugin-gallery-chapters

Conversation

@manuzzi

@manuzzi manuzzi commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements #31: a published collection can now be a Collection Set, which becomes a Chapters Gallery on Lumio. Saving the Set immediately creates a "Default" child collection (photos published there land in the gallery's normal unsectioned bucket), and every additional child collection becomes a real Chapter (GallerySection) on Lumio, with its uploaded photos assigned to that chapter automatically. Renaming or deleting a chapter collection in Lightroom syncs to the corresponding chapter on Lumio, and "Show in Lumio" now also works on the Set itself.

A plain collection directly under the publish service keeps working exactly as before. No server-side changes were needed — this reuses the existing Studio Section API (/galleries/:id/sections*), which already accepts the same Bearer-token auth the plugin already uses for uploads.

Plugin version bumped to 0.4.1.

Diff scope note: this branch is built on top of feat/lightroom-plugin-icon-dedupe-hash-matching (#27), which hasn't merged yet, so this diff includes those commits too (icon, anti-duplicate republish, hash-based selection matching, and their follow-up fixes) in addition to the new Chapters Gallery work. Happy to rebase and re-scope once #27 merges, if that's preferred — just let me know.

What changed (Chapters Gallery, on top of #27)

  • Info.lua: version 0.4.1
  • LumioApi.lua: 4 new methods (createSection, patchSection, deleteSection, assignFilesToSection), thin wrappers around the existing Studio Section-CRUD routes
  • LumioPublishService.lua: Collection Set support (maxCollectionSetDepth = 1), the three required set-level dialog callbacks, gallery inheritance from parent Set on first publish, chapter creation + batched/chunked file assignment, rename/delete sync for chapters, and a goToPublishedCollection fix that now handles both plain collections and Sets
  • READMEs (DE/IT) and CHANGELOG updated

Known SDK limitations (not bugs, confirmed against the official Lightroom Classic SDK reference)

  • The native "Create Published Collection..." menu entry, both at the top level and inside a Chapters Gallery, uses Lightroom's own generic wording rather than a custom name — this string is shared across both contexts with no way to vary it selectively, so a custom label would misname a Chapter as a "Simple Gallery" (or vice versa).
  • The native "Create Smart Collection..." entry cannot be hidden — there is no SDK-level flag to remove it, only to rename it, so it's left with Lightroom's generic wording rather than a misleading Lumio-branded name.

Test plan

No automated test coverage exists for this plugin (no CI job touches apps/lightroom-plugin, no Lua test framework) — everything below was verified manually against a real Lightroom Classic 15.5.1 install on macOS, against a live Lumio instance, across several rounds of real-device testing and fixes:

  • Creating a new Collection Set (Chapters Gallery) shows the Lumio gallery-picker dialog and immediately creates a nested "Default" collection
  • Publishing through "Default" creates the Lumio gallery and lands photos in the unsectioned bucket
  • Adding an extra collection inside the Set and publishing creates a matching Chapter (GallerySection) and assigns its photos to it
  • Renaming a chapter collection syncs the chapter's title on Lumio
  • Deleting a chapter collection deletes the corresponding Lumio section (files fall back to unsectioned)
  • "Show in Lumio" works both on a chapter collection and on the Set itself
  • Existing flat/plain published collections continue to work unchanged
  • Plugin reloaded and re-tested after each fix round; several real bugs (wrong/non-existent SDK method names, an invalid same-write-transaction read, a redundant title field) were only caught this way and are already fixed in this PR

@canja006 — given your real-device testing on #27, it'd be great if you could also give this a try on your own Lightroom Classic setup when you have a chance, the same way you helped catch the goToPublishedPhoto issue there. In particular any environment/version differences in how Lightroom handles Collection Set creation dialogs and the right-click menu would be valuable to confirm.

🤖 Generated with Claude Code

manuzzi and others added 18 commits August 31, 2026 18:10
…lection matching

Three fixes/improvements to the Lightroom Classic plug-in, found and
fixed while dogfooding it against a real LrC instance:

- Ship the publish service's icon (icon.png/icon@2x.png). It was
  referenced in code but never added to the plug-in folder, so it
  showed up blank/broken in Lightroom's Publishing Services panel.

- Stop duplicate files piling up on re-publish. uploadOnePhoto always
  created a new Lumio file and never removed the previous one, so
  editing a photo and republishing (or a manual "Republish") left
  several versions of the same photo in the online gallery. The
  plug-in now deletes the old remote file (via
  rendition.publishedPhotoId) before uploading the new one.

- Make Selection-Import matching survive renames and same-basename
  ambiguity. The Publish-Service now embeds the MD5 of the original
  master file into a custom XMP segment of the uploaded JPEG
  (JpegXmp.lua). The worker extracts it via the existing exiftool call
  and stores it under the already-present (and previously unused)
  exif.lumio.originalMd5 JSON field -- no schema migration needed.
  Selection-Import uses it to auto-resolve ambiguous filename matches
  and, with a new opt-in option, to recover files renamed after
  publishing.

Also: bump the plug-in to v0.3, translate all plug-in comments to
English (previously a mix of German/Finnish/English), and update the
plug-in's own README/known-limitations docs to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… public gallery

goToPublishedCollection built the public customer-facing link
(host + /g/<slug>). A photographer clicking "Show in Lumio" from
Lightroom wants to manage the gallery, not see the customer's view, so
it now opens the Studio management page (host + /studio/<galleryId>)
instead. The Studio route is keyed by gallery id, not slug, so this
also drops the now-unneeded slug-from-cache lookup and the gallerySlug
tracking in processRenderedPhotos that only existed to support it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- JpegXmp.lua: insert the XMP hash segment after a leading APP0/JFIF
  segment when present, instead of unconditionally right after SOI.
  Verified byte-for-byte against a real JPEG with and without a
  leading APP0.
- ImportSelectionTask.lua: the rename-recovery hash map was
  hash -> file (single), silently dropping every file after the first
  one sharing a hash -- normal when virtual copies of one master were
  all published. Now hash -> list of files, all resolved to the same
  matched photo.
- LumioPublishService.lua: add goToPublishedPhoto (per-photo "Show
  public gallery"), opening the public customer-facing gallery link.
  Complements the collection-level "Show in Lumio" now pointing at the
  Studio view, for when you do want to see the client's side -- and
  doesn't need an active Studio session the way the Studio link does.
  Restores the gallerySlug tracking this needs.
- db.py: mark_file_ready no longer touches the exif column at all when
  there's no original_md5 to write (previously flipped NULL to {} on
  every file). When there is one, it's written via jsonb_set over the
  existing 'lumio' object instead of a flat merge, so a future second
  field under exif.lumio survives being written alongside originalMd5.
  Verified both the previous silent-{} behaviour and the new jsonb_set
  merge against a throwaway Postgres instance.
- README.md (DE): fixed an Italian checkbox label ("Rinominati per
  hash") accidentally left in the German option description.
- Added worker tests for extract_original_md5/extract_metadata,
  including one asserting a single exiftool call for both values.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… speed up rename recovery

Addresses the real end-to-end test report from @canja006 on PR markusthiel#27
(LrC 15.5.1, 2774-photo catalog).

- goToPublishedPhoto ("Show public gallery") always failed: it assumed
  info.publishedCollectionInfo had a .collectionSettings field like
  getCollectionInfoSummary() elsewhere in this file. On a real install
  it only ever carries {isDefaultCollection, name, parents}, and
  LrPublishedPhoto has no getPublishedCollection() either. Adopts
  @canja006's tested fix: fetch the collection through
  photo:getContainedPublishedCollections() instead, wrapped in
  LrTasks.startAsyncTask since that call yields.

- gallerySlug was only ever captured when a NEW gallery was created
  from the plug-in dialog; a collection bound to an EXISTING gallery
  (picked from the dropdown) never got one, leaving the public-gallery
  link dependent on a cache that goes stale as soon as the gallery is
  edited in Studio. processRenderedPhotos now self-heals it (same
  proven catalog:withWriteAccessDo pattern already used for gallery
  creation) on the next publish to any such collection.

- Rename-recovery hash pass took 4m12s on the full 2774-photo catalog
  (the default scope) vs 3.8s on a 70-photo collection, with Lightroom
  visibly sluggish and memory climbing. Three fixes: stop scanning once
  every target hash is found (it never exited early before), skip
  photos pass 1 already matched by filename (they were hashed again
  for nothing), and -- the structural fix -- embed the original
  master's byte size alongside its MD5 (free: the file is already
  fully in RAM to hash it) so a rename-recovery candidate can be ruled
  out with a plain LrFileUtils.fileAttributes stat instead of being
  fully read into memory. Exact byte-size collisions between different
  photos are rare; identity is still always confirmed by the real hash.
  exif_meta.py/db.py/plugin.ts extended to carry originalSize alongside
  originalMd5 through the same pipeline, same reasoning as the
  original hash (no schema change, jsonb_set-based merge).

- Logger.lua/README: corrected the macOS log path for current
  Lightroom Classic (confirmed on 15.5: ~/Library/Logs/Adobe/Lightroom/
  LrClassicLogs/, not ~/Documents/LrClassicLogs/).

Verified: XMP round-trip with the new OriginalSize field against a
real JPEG + exiftool (with and without a size, matching the existing
OriginalMD5 test pattern); worker db.py's extended jsonb_set query
against a throwaway Postgres instance including the actual
mark_file_ready() function end-to-end (exif=NULL populated with both
fields when given, an existing exif with sibling keys left completely
untouched when no hash is given); 4 new + all 19 existing worker tests
pass; tsc --noEmit clean; luac -p clean on all 9 plug-in files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scoped to the Python files touched by this PR (db.py, process_file.py,
process_raw.py, test_exif_meta.py) -- exif_meta.py was already in
English. Comments only, no functional changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Prepares the plug-in for the Collection Set / Chapters feature
(markusthiel#31).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dialog

Raise maxCollectionSetDepth to 1 and implement the three set-level
callbacks (viewForCollectionSetSettings, endDialogForCollectionSetSettings,
updateCollectionSetSettings) that were missing when the crash-avoidance
maxCollectionSetDepth = 0 was originally put in place. The existing
gallery-picker dialog is factored into shared helpers
(initGalleryPickerProps/buildGalleryPickerView) reused by both a root
collection ("Simple Gallery", the renamed original flat mode) and a
Collection Set ("Lumio Gallery").

viewForCollectionSettings now branches by collection role via the new
classifyCollection helper: root, the Set's auto-created default child,
or an additional chapter collection -- the latter two get placeholder
dialog text for now; wiring them to gallery inheritance and chapter
creation follows in later commits.

No Lumio API calls yet -- pure dialog/terminology groundwork, part 1 of
markusthiel#31's implementation checklist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…publish

A default-child or chapter collection with no galleryId yet now
resolves its parent Collection Set's Lumio gallery settings
(getParentSetSettings) and inherits galleryId/gallerySlug from it,
creating the gallery on the Set's own first-ever publish if none
exists yet -- mirroring the existing root/flat "create on first
publish" pattern one level up. The resolved galleryId/gallerySlug are
also cached onto the child's own collection settings, so
deletePhotosFromPublishedCollection/goToPublishedCollection/
goToPublishedPhoto keep working completely unchanged.

makeLive is read fresh from the parent Set on every publish rather
than cached on the child, since it's a live toggle the photographer
may flip on the Set after chapters already exist.

No section/chapter creation yet -- both default and chapter children
just inherit the gallery for now, landing photos in the unsectioned
bucket. Part of markusthiel#31.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
createSection, patchSection, deleteSection, assignFilesToSection --
thin wrappers around the generic Studio Section-CRUD routes
(galleries.ts), reusing M.request's existing retry/auth handling
exactly like initUpload/completeUpload already do. Plumbing only, not
wired into the publish flow yet. Part of markusthiel#31.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…them

A chapter (non-default) child collection now resolves or creates its
Lumio Section on first publish (api.createSection, title = the LR
collection's own name), persisting sectionId onto the collection's
settings the same withWriteAccessDo way galleryId already is.

uploadOnePhoto now returns the Lumio fileId on success instead of
nothing, threaded through its LrTasks.pcall call site in
processRenderedPhotos so a chapter publish can collect this run's
uploaded fileIds and batch-assign them to the Section afterwards
(api.assignFilesToSection), chunked to the server's 500-fileIds-per-call
limit. A default-child or root collection never gets a sectionId, so
this is a no-op for them -- their photos keep landing in the gallery's
unsectioned bucket exactly as before.

A section-assignment failure is reported in the existing summary
dialog but does not fail the publish -- the photos are already
uploaded either way.

Completes the core Chapters functionality from
markusthiel#31's implementation checklist (steps up to markusthiel#7).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New renamePublishedCollection/deletePublishedCollection hooks: for a
chapter (non-default) child collection with a sectionId, a rename in
Lightroom PATCHes the Section's title and a delete DELETEs the
Section (files fall back to the gallery's unsectioned bucket
server-side, ON DELETE SET NULL). Root and default-child collections
never had a sectionId, so both hooks are a no-op for them.

Deleting the whole Collection Set (Lumio Gallery) is deliberately NOT
targeted here -- only per-child collection deletes clean up their own
Section. The Gallery itself stays Studio-only to delete, unchanged
from today, tracked separately.

Both hooks are best-effort/logged, matching the file's existing
swallow-and-log philosophy -- a sync failure must never block the
rename/delete Lightroom already performed locally. The exact `info`
payload shape is written defensively pending real-device verification
(no CI covers this plugin).

Completes markusthiel#31's implementation checklist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CHANGELOG entry (English-only, matching the current convention) plus
a new "Chapters" section in both the German and Italian plugin READMEs
explaining Collection Sets, the auto-created default child, and how
chapter rename/delete syncs to Lumio.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… chapters, auto-create Default

- Rename the Collection Set concept from "Lumio Gallery" to "Chapters
  Gallery" throughout (dialog titles, error messages, comments).
  "Simple Gallery" (the root/flat mode) is unchanged.
- Drop Smart Collection support (titleForPublishedSmartCollection/
  _standalone) entirely: a Chapters Gallery's right-click menu now
  offers exactly one way to add a child -- a plain collection/chapter.
  Nested Collection Sets were already blocked by maxCollectionSetDepth
  = 1. Smart Collections were never wired into any Lumio-specific
  logic beyond the title strings, so nothing else is affected.
- updateCollectionSetSettings now explicitly creates a "Default" child
  collection the moment the Set itself is saved
  (publishService:createPublishedCollection("Default", newSet, true),
  inside catalog:withWriteAccessDo -- canReturnExisting = true keeps
  it idempotent if the dialog is saved again). The new collection gets
  collectionSettings.isDefaultChapter = true.
- classifyCollection now keys off that own isDefaultChapter flag
  instead of the SDK's info.isDefaultCollection, which was never
  real-device-confirmed at this call site and would misclassify a
  collection we create ourselves anyway. This also means renaming
  "Default" no longer risks turning it into a chapter -- classification
  is entirely under this plug-in's own control now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Version bump for the Chapters Gallery terminology/menu/Default-chapter
refinements. CHANGELOG entry updated in place (still under
[Unreleased], 0.4.0 was never released) and both plugin READMEs
(German, Italian) updated to describe the Chapters Gallery naming,
the restricted right-click menu, and the immediately-created
"Default" chapter collection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nheritance

Three real, log-confirmed bugs found via real-device testing on 0.4.1,
all wrong SDK method/field names guessed without a live Lightroom
Classic install to verify against -- fixed now against the official
Lightroom Classic 15 SDK reference (local docs, not third-party
mirrors):

1. getParentSetSettings called a non-existent
   catalog:getPublishedCollectionSetByLocalIdentifier, throwing
   "attempt to call method ... (a nil value)" on every publish of a
   default/chapter child -- confirmed in Lumio.log. Fixed by using
   LrPublishedCollection:getParent() directly on the collection object
   (confirmed to exist), which also simplifies the function (no more
   catalog round-trip via localIdentifier).

2. updateCollectionSetSettings read info.publishedCollectionSet, which
   doesn't exist on this hook's info table -- the confirmed field name
   is info.publishedCollection (despite holding an
   LrPublishedCollectionSet value here). This silently skipped Default
   chapter creation every time, logged as "missing
   publishService/publishedCollectionSet".

3. The parent Set's own settings were read/written via
   getCollectionInfoSummary()/setCollectionSettings() -- the
   plain-collection methods, which don't exist on
   LrPublishedCollectionSet. Fixed to
   getCollectionSetInfoSummary()/setCollectionSetSettings(), the
   confirmed Set-object equivalents. This would have thrown as soon as
   bug markusthiel#1 was fixed, since it's on the same gallery-creation path.

Also simplified renamePublishedCollection/deletePublishedCollection
(info.publishedCollection is confirmed singular, never an array, and
info.collectionSettings is not a documented field on either hook --
removed the now-provably-dead branches for both) and corrected several
comments that claimed uncertainty or behavior later confirmed false
(e.g. Set rename does NOT currently sync its title to Lumio, despite
an earlier comment implying it did).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… limitation

The 0.4.1 CHANGELOG entry and both plugin READMEs claimed the
right-click menu on a Chapters Gallery shows no Smart Collection
option. Confirmed false via the official SDK reference: omitting
titleForPublishedSmartCollection only changes Lightroom's own fallback
wording ("Published Smart Collection") -- there is no documented way
to remove the menu entry entirely (no canAddSmartCollection-style gate
exists in getCollectionBehaviorInfo). Docs corrected to describe the
actual, confirmed behavior instead of the incorrect earlier claim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gates, drop redundant title field

Four issues found via a second round of real-device testing on 0.4.1,
this time with "Show in Lumio" on a Chapters Gallery itself and a
closer look at the log after the previous fixes:

1. "Show in Lumio" on a Collection Set threw an unguarded Lua error
   (worked fine on a chapter). goToPublishedCollection only ever tried
   info.publishedCollection:getCollectionInfoSummary() -- the
   plain-collection method, which doesn't exist on a
   LrPublishedCollectionSet. Now tries that first and falls back to
   getCollectionSetInfoSummary() for a Set, matching the same
   dual-type handling already used in getParentSetSettings.

2. Default chapter creation could still fail even with the
   updateCollectionSetSettings field-name fix from the previous
   commit: log confirmed "Can't get collection information after
   creating collection inside the same withWriteAccessDo function" --
   querying/setting collectionSettings on a collection in the SAME
   write-access gate that created it isn't allowed (the same
   restriction LrPublishedCollection:getParent's own doc warns about,
   evidently not unique to getParent). Split into two separate
   withWriteAccessDo calls: one to create, a second to mark
   isDefaultChapter.

3. Creating a plain collection inside a Chapters Gallery showed the
   full root-style dialog (existing-gallery picker, including picking
   an EXISTING gallery) because viewForCollectionSettings cannot
   classify a collection as root vs. chapter before it exists
   (info.parents is only populated when editing an existing
   collection). If the photographer picked an existing gallery there,
   that WRONG galleryId would have been used at publish time instead
   of the parent Set's, since the resolution logic was gated on
   "galleryId already set" before checking `kind`. Restructured so a
   default/chapter child's gallery is now ALWAYS resolved from its
   parent Set, unconditionally overriding any such stray value.

4. Removed the separate "gallery title" edit field from the picker
   dialog entirely (used both for a root collection and, incorrectly,
   shown to what turns out to be a chapter per markusthiel#3) -- a new gallery's
   or chapter's title is now always the Lightroom collection's own
   name, read at first-publish time via getName(), exactly like a
   chapter's title already worked. Removes asking the photographer for
   the same name twice.

Also dropped the custom titleForPublishedCollection/_standalone
("Simple Gallery") -- confirmed via the official SDK reference that
this single string drives LR's native "Create .../Rename ..." menu
text for EVERY plain collection, including ones created inside a
Chapters Gallery (which are chapters, not "Simple Gallery"), with no
way to vary it by context. Lightroom's own generic default,
"Published Collection", is a more honest compromise than a misleading
custom name. The dialog content we fully control (group_box titles
per classifyCollection) is unaffected and still reads correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ified dialog

Document that "Show in Lumio" now works on a Chapters Gallery itself,
that creating a gallery/chapter no longer asks for a separate title
(uses the Lightroom collection's own name), and that the native
"Create Published Collection" menu wording inside a Chapters Gallery
is Lightroom's generic default rather than "Simple Gallery" -- that
text isn't selectively customizable per context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 6, 2026 09:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There is a confirmed crash risk in the publish flow due to an unguarded fileAttributes() nil dereference, plus a doc formatting issue in the Unreleased changelog entry.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR upgrades the Lightroom Classic plug-in to support modeling a Lumio Gallery as a Collection Set (“Chapters Gallery”), where child collections become Lumio Chapters/Sections, while also carrying forward the earlier work to embed an original-file MD5 (+ size) into published JPEGs for more robust Selection-Import matching.

Changes:

  • Lightroom Publish-Service: add Collection Set (“Chapters Gallery”) support with automatic “Default” child creation, chapter (section) create/rename/delete sync, and chapter file assignment batching.
  • Lightroom Publish-Service + Worker + API: embed original master MD5/size into uploaded JPEG XMP, extract it in the worker, and surface it via plugin endpoints for Selection-Import hash-based disambiguation/rename recovery.
  • Documentation/versioning updates: plug-in version bump plus README/CHANGELOG updates.
File summaries
File Description
CHANGELOG.md Documents new plug-in capabilities and hash-based matching behavior.
apps/worker/tests/test_exif_meta.py Adds worker tests for reading embedded original MD5/size and single-call extraction.
apps/worker/tasks/process_raw.py Switches RAW pipeline to single-call EXIF+original-hash extraction and persists it.
apps/worker/tasks/process_file.py Switches image pipeline to single-call EXIF+original-hash extraction and persists it.
apps/worker/exif_meta.py Implements extraction of custom XMP tags + combined extract_metadata() to avoid multiple exiftool invocations.
apps/worker/db.py Persists originalMd5/originalSize into files.exif->lumio without forcing NULL→{} churn.
apps/lightroom-plugin/README.md Updates German README for Simple vs Chapters gallery workflow and hash-matching limitations.
apps/lightroom-plugin/README.it.md Updates Italian README for Simple vs Chapters gallery workflow and hash-matching limitations.
apps/lightroom-plugin/lumio.lrdevplugin/PluginManager.lua Updates plug-in manager strings/comments (host/token UI).
apps/lightroom-plugin/lumio.lrdevplugin/LumioPublishService.lua Core publish-service changes: set support, chapter syncing, assignment batching, “Show in Lumio” fixes, and hash embedding.
apps/lightroom-plugin/lumio.lrdevplugin/LumioApi.lua Adds section CRUD + section file assignment API wrappers for chapters.
apps/lightroom-plugin/lumio.lrdevplugin/Logger.lua Updates logging-path documentation/comments.
apps/lightroom-plugin/lumio.lrdevplugin/Json.lua English comment cleanups in embedded JSON library wrapper.
apps/lightroom-plugin/lumio.lrdevplugin/JpegXmp.lua New module to embed original MD5/size as an APP1-XMP segment (APP0-aware insertion).
apps/lightroom-plugin/lumio.lrdevplugin/Info.lua Bumps plug-in version to 0.4.1 and updates top-level docs/comments.
apps/lightroom-plugin/lumio.lrdevplugin/ImportSelectionTask.lua Adds hash-based disambiguation + optional rename recovery with size prefilter.
apps/lightroom-plugin/lumio.lrdevplugin/ImportSelectionDialog.lua Adds opt-in UI setting for hash-based rename recovery.
apps/api/src/routes/plugin.ts Exposes originalMd5/originalSize from exif.lumio defensively in plugin endpoints.
apps/api/prisma/schema.prisma Documents the exif.lumio.originalMd5 meaning/usage in the schema.
Review details
  • Files reviewed: 19/21 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 669 to 670
-- File size (measure AFTER embedding, see above)
local sizeBytes = LrFileUtils.fileAttributes(filepath).fileSize or 0
Comment thread CHANGELOG.md Outdated

### Added

- Lightroom plug-in: the Publish-Service now embeds the MD5 hash of the original master file into a custom XMP field of the uploaded JPEG, and the Selection-Import side uses it to automatically resolve ambiguous filename matches (e.g. the same shot published as both `.NEF` and `.DNG`) and, with a new opt-in option, to recover files that were renamed in Lightroom after publishing. Only available for files published from this plug-in version onward. · *The Lightroom plug-in's Publish-Service now embeds the original master file's MD5 hash into a custom XMP field of the uploaded JPEG, and Selection-Import uses it to automatically resolve ambiguous filename matches (e.g. the same shot published as both `.NEF` and `.DNG`) and, with a new opt-in option, to recover files renamed in Lightroom after publishing. Only available for files published from this plug-in version onward.*
…CHANGELOG text

Addresses Copilot's review on PR markusthiel#32:

- uploadOnePhoto: LrFileUtils.fileAttributes(filepath) can return nil
  (temp render deleted/moved from under us, disk hiccup), and indexing
  .fileSize on it threw an unhandled "attempt to index a nil value"
  instead of a clear error. Contained to the one photo (the call site
  is already inside a pcall in processRenderedPhotos's render loop),
  but the error message was unhelpful -- now guarded with a clear
  message instead.
- CHANGELOG.md: the 11 bullets in the Unreleased section duplicated
  their own text as a trailing " · *...*" span -- leftover from this
  changelog's old bilingual (German · English) format, except the
  "German" half was never actually translated, so it just repeated
  the English sentence. Stripped the duplicated span from all 11,
  matching the plain-English convention already used everywhere else
  (confirmed: this project moved away from the bilingual per-entry
  format after 0.74.0). Dated historical sections are untouched --
  those bilingual pairs are genuine German+English, not duplicates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@manuzzi

manuzzi commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please review the latest changes.

@canja006

canja006 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for the ping — happy to run this on my setup, the same way as on #27.

The earliest I can get to it is tomorrow. I'll go through the full list on Lightroom Classic 15.5.1 / macOS against my self-hosted instance: Set creation dialog, the right-click menus on the Set and on a chapter collection, the auto-created "Default" child, chapter create/rename/delete round-trips, and "Show in Lumio" on the Set itself. Report in the same format as before.

@canja006

canja006 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Ran the Chapters Gallery flow on my install — Lightroom Classic 15.5.1 on macOS, self-hosted Lumio still at 052b0f9 (the api/worker diff of this branch against it is comment translations only, so the server side needed no rebuild), plug-in from be39ca5 (0.4.1). Same 2774-photo catalog as on #27. Everything below is checked against the database (gallery_sections, files.sectionId) and Lumio.log, not just the panel.

Menus and dialogs, since you asked specifically. Right-click on the service shows Create Published Collection…, Create Published Smart Collection…, Create Chapters Gallery… — both SDK limitations exactly as described. The Set dialog shows the "Chapters Gallery" group box with the gallery picker. Inside the Set, right-click gives the generic Create Published Collection…, and its first-open dialog shows the Simple Gallery picker (the cosmetic gap you documented). Nothing surprising on 15.5.

What works

  • Create Chapters Gallery… → Set created, Default child appears immediately, its settings are { isDefaultChapter = true }.
  • Publish 3 photos via DefaultPOST /plugin/galleries (new gallery), three uploads, all three with sectionId = NULL. The Set's own settings picked up galleryId/gallerySlug/galleryMode, so later chapters inherit the right gallery.
  • New collection inside the Set, 3 photos → POST /galleries/:id/sections before the first upload, then a single batched POST /sections/:id/files → all three assigned to the chapter, no stray gallery created.
  • Rename chapter → PATCH /sections/:id within the same second, title updated.
  • "Show in Lumio" on the Set and on a chapter → Studio view of the gallery.
  • Plain collections at the service root behave as before (re-publish into an existing flat collection: uploads + PATCH gallery).

Chapter deletion never reaches Lumio in the case the README describes

README step 4 says: delete the collection → chapter deleted on Lumio, photos fall back to the unsectioned bucket. On a real install it depends entirely on which button the user presses in Lightroom's own confirmation dialog:

Choice in LrC's delete dialog deletePublishedCollection invoked? On Lumio
Leave on Service No — not a single line logged Chapter stays, its photos stay assigned to it. Only the Lightroom collection disappears.
Delete Yes DELETE /sections/:id → chapter removed; the photos remain in the gallery, unsectioned (no deletePhotosFromPublishedCollection, no photoIds in info).

This is documented SDK behaviour, not a 15.5 quirk — the reference for deletePublishedCollection says: "this callback function is only invoked when the user clicks the 'Delete' button in the dialog which Lightroom presents to alert the user of what happens to photos which have been published via this published collection. If the user chooses to leave the photos in their published location, the function is not called."

So the README outcome is reachable only through the button whose own dialog says the photos will be deleted from the service, while the choice a photographer will naturally make to keep their photos ("Leave on Service") leaves an orphaned chapter on Lumio. I verified with a log line as the first statement of deletePublishedCollection: silence for "Leave on Service"; for "Delete" the whole path runs (info keys isDefaultCollection, name, parents, publishService, publishedCollection, getCollectionInfoSummary fine, sectionId present, DELETE 200).

I don't see a way for the plug-in to cover the "Leave on Service" case — there is no hook. What it can do is say so. README/CHANGELOG should state that removing a chapter on Lumio requires choosing Delete in Lightroom's dialog, that the photos are nevertheless kept in the gallery (unsectioned), and that "Leave on Service" only removes the Lightroom collection — the chapter then has to be deleted in Studio.

Deleting the Set

Deleting the whole Set (with "Delete") leaves the gallery untouched, as intended — but by accident. Lightroom invokes deletePublishedCollection for the Set as well, info.publishedCollection is then an LrPublishedCollectionSet, and the code calls getCollectionInfoSummary() on it:

attempt to call method 'getCollectionInfoSummary' (a nil value)

LrTasks.pcall swallows it, collSettings ends up nil, nothing happens. An explicit early return for Sets (or getCollectionSetInfoSummary, the way getParentSetSettings already does it) would make the non-goal hold on purpose rather than by exception.

Two small things

  • remoteId is never set — remoteCollectionId is NULL for every Lumio collection in the catalog. The current logic doesn't need it, but publishedCollection:setRemoteId(sectionId) inside the existing "assign chapter" write block works (tested) and gives info.remoteId in the rename/delete callbacks for free. Optional.
  • One photo whose master was offline gave "2 succeeded, 1 failed … The file could not be found". That's Lightroom's render failing; the plug-in reported it cleanly.

Happy to re-run the delete cases on a follow-up commit.

manuzzi and others added 2 commits September 8, 2026 10:58
…chapter remoteId

Addresses @canja006's real-device follow-up testing on PR markusthiel#32:

- renamePublishedCollection/deletePublishedCollection: previously
  relied on getCollectionInfoSummary() throwing when
  info.publishedCollection is actually a LrPublishedCollectionSet (the
  Set itself renamed/deleted, not a chapter), with the pcall silently
  swallowing that error and falling through to the same nil-collSettings
  no-op used for "not a chapter". The end result was correct (a Set's
  rename/delete must never touch the Lumio gallery) but only by
  accident. Confirmed via real-device testing: "attempt to call method
  'getCollectionInfoSummary' (a nil value)" when deleting a Set. Both
  functions now check for that failure explicitly and return, making
  the non-goal hold by design.

- processRenderedPhotos: also call
  exportContext.publishedCollection:setRemoteId(sectionId) in the same
  write-access block that already persists sectionId onto a new
  chapter's own settings. Not read anywhere in this plug-in today, but
  costs nothing and gives info.remoteId "for free" in future
  rename/delete callback work -- verified working on a real install.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… dialog choice

README (DE/IT) and CHANGELOG updated per @canja006's real-device
testing: a chapter collection's Section only gets deleted on Lumio if
the photographer picks "Delete" (not "Leave on Service") in
Lightroom's own delete-confirmation dialog -- documented Lightroom SDK
behavior (deletePublishedCollection is only invoked for "Delete"), not
something the plug-in can influence. Previously the README implied
deleting the collection always synced the deletion; now it explains
both outcomes and that "Leave on Service" leaves an orphaned chapter
that has to be removed manually in Lumio Studio.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@manuzzi

manuzzi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@canja006 thank you for the thorough follow-up testing — checking against the DB and log, not just the panel, caught things I wouldn't have found otherwise. Pushed two commits addressing your report:

  • e8d1451: renamePublishedCollection/deletePublishedCollection now explicitly check for a Collection Set and return, instead of relying on getCollectionInfoSummary() throwing and the pcall swallowing it — same end result, but by design now, not by accident. Also added setRemoteId(sectionId) in the chapter-assignment write block, as you verified.
  • 7d04d7c: README (DE/IT) and CHANGELOG now explain that a chapter's Section is only deleted on Lumio when "Delete" (not "Leave on Service") is chosen in Lightroom's own confirmation dialog, and that "Leave on Service" leaves an orphaned chapter that needs manual cleanup in Studio — exactly the SDK behavior you quoted.

Really appreciate you running the delete-case matrix in such detail. Let me know if you spot anything else on a follow-up pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@manuzzi

manuzzi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Forgot to bump to Plugin Version 0.4.2 for easier identification in LrC

@canja006

canja006 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Re-ran the delete matrix on 7d04d7c — plug-in reinstalled from the branch, Lightroom fully restarted, same server (052b0f9, unchanged as before).

  • Chapter created inside a fresh Set → POST /sections, batched assignment as before, and the catalog now carries remoteCollectionId = <sectionId> for the chapter — setRemoteId lands where it should.
  • Rename the Set itself → nothing sent to Lumio, gallery title untouched, no dialog.
  • Delete the chapter with DeleteDELETE /sections/:id, section gone, all photos still in the gallery, unsectioned.
  • Delete the whole Set with Delete → nothing sent, gallery intact (6 files, 0 sections), no error.

So the three cases from my report now behave the same as before but on purpose, and the README wording matches what Lightroom actually does.

One small thing on e8d1451: the Set detection still keys off getCollectionInfoSummary() failing inside the pcall, so a genuine failure of that call on a real chapter would take the same silent no-op path as a Set. The SDK gives you the type directly — LrPublishedCollectionSet:type() returns "LrPublishedCollectionSet" (and "LrPublishedCollection" for a plain collection) — so

if info.publishedCollection:type() == "LrPublishedCollectionSet" then return end

before the pcall would make the Set no-op a real check and leave the pcall failure as what it is, an error worth a log:warn. Not blocking; the behaviour is right either way.

Nothing else outstanding from my side.

…t a caught error

Non-blocking follow-up from @canja006's re-test of e8d1451 (confirmed
all three delete/rename cases now behave correctly and on purpose,
and setRemoteId lands as expected). Their one remaining note: keying
the Set no-op off getCollectionInfoSummary() throwing conflates two
different situations -- "this really is a Set" (intentional no-op)
and "this is a chapter but the read genuinely failed for some other
reason" (a real bug) -- under the same silent no-op.

Both LrPublishedCollection and LrPublishedCollectionSet have a type()
method, confirmed against the official Lightroom Classic 15 SDK
reference to return exactly "LrPublishedCollection" /
"LrPublishedCollectionSet". New shared isPublishedCollectionSet()
helper checks that directly before ever calling
getCollectionInfoSummary(), so a genuine failure on a real chapter now
logs a warning instead of silently doing nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@manuzzi

manuzzi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@canja006 thanks for the re-test — good to have all three cases confirmed clean after a full reinstall/restart, and that setRemoteId lands correctly.

Applied your type() suggestion in 241d07b: added a shared isPublishedCollectionSet() helper that checks info.publishedCollection:type() (confirmed against the SDK reference to return "LrPublishedCollection" / "LrPublishedCollectionSet") before ever calling getCollectionInfoSummary(). A genuine failure on a real chapter now logs a warning instead of silently taking the same path as a Set. Same behavior for the cases that matter, just an honest check instead of an inferred one — appreciate you flagging it even though it wasn't blocking.

Nothing else pending on my end either. Thanks again for all the real-device rounds on this one.

@markusthiel

Copy link
Copy Markdown
Owner

Read through this and the whole exchange above. Nothing to add on the code — you two closed this out without me, and the review round is the kind I'd want as the norm here: @canja006 checking gallery_sections and files.sectionId in the database and the log rather than the panel, which is what surfaced the delete-dialog case at all; and the :type() catch, where the old code would have sent a genuine getCollectionInfoSummary() failure down the same silent path as a Set. Documenting the "Leave on Service" orphan instead of pretending it away was the right call too — the SDK does what it does.

One structural note. GitHub shows +2279 across 21 files here, but #27 is an ancestor of this branch, so most of that isn't yours: schema.prisma, routes/plugin.ts, apps/worker/* and JpegXmp.lua are all #27's. The actual contribution is +878 in 6 files, all inside the plug-in — which incidentally confirms the "no backend/API changes expected" note in #31.

So this goes in after #27, which still has three open items from Copilot's re-review on the 8th. Merging this first would pull #27 in silently, unfixed.

Makes sense that @canja006 tested against a server at 052b0f9 on the grounds that the API diff was comment translations only — that was accurate, and it means the two were effectively verified together.

No retargeting needed from my side, unlike the print-shop set; I mostly want the ordering written down somewhere. Queue is: #27 → this one.

@manuzzi

manuzzi commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

@markusthiel #27 has got an update, if successful I will rebase this PR with main branch and push agin to update this PR as in my flow this was straight after #27

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants