@pired/lightroom-mcp 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/LightroomMCP.lrplugin/HandlerAI.lua +466 -0
  4. package/dist/LightroomMCP.lrplugin/HandlerAIMasks.lua +549 -0
  5. package/dist/LightroomMCP.lrplugin/HandlerCatalog.lua +256 -0
  6. package/dist/LightroomMCP.lrplugin/HandlerCollections.lua +337 -0
  7. package/dist/LightroomMCP.lrplugin/HandlerDevelop.lua +1668 -0
  8. package/dist/LightroomMCP.lrplugin/HandlerExport.lua +149 -0
  9. package/dist/LightroomMCP.lrplugin/HandlerImport.lua +108 -0
  10. package/dist/LightroomMCP.lrplugin/HandlerLocalAdjustments.lua +330 -0
  11. package/dist/LightroomMCP.lrplugin/HandlerMetadata.lua +169 -0
  12. package/dist/LightroomMCP.lrplugin/HandlerOrganization.lua +783 -0
  13. package/dist/LightroomMCP.lrplugin/HandlerPreview.lua +189 -0
  14. package/dist/LightroomMCP.lrplugin/HandlerSearch.lua +185 -0
  15. package/dist/LightroomMCP.lrplugin/HandlerSelection.lua +302 -0
  16. package/dist/LightroomMCP.lrplugin/HandlerSpots.lua +313 -0
  17. package/dist/LightroomMCP.lrplugin/HandlerWatermark.lua +59 -0
  18. package/dist/LightroomMCP.lrplugin/Info.lua +25 -0
  19. package/dist/LightroomMCP.lrplugin/JSON.lua +270 -0
  20. package/dist/LightroomMCP.lrplugin/Log.lua +92 -0
  21. package/dist/LightroomMCP.lrplugin/MenuShowStatus.lua +22 -0
  22. package/dist/LightroomMCP.lrplugin/PhotoLookup.lua +39 -0
  23. package/dist/LightroomMCP.lrplugin/PluginInfoProvider.lua +865 -0
  24. package/dist/LightroomMCP.lrplugin/PluginInit.lua +48 -0
  25. package/dist/cli.d.ts +6 -0
  26. package/dist/cli.d.ts.map +1 -0
  27. package/dist/cli.js +31 -0
  28. package/dist/cli.js.map +1 -0
  29. package/dist/configs/claude-code.mcp.json +9 -0
  30. package/dist/configs/claude-desktop.json +12 -0
  31. package/dist/configs/generic-stdio-client.json +10 -0
  32. package/dist/configs/mcp-proxy-http.json +9 -0
  33. package/dist/create-server.d.ts +12 -0
  34. package/dist/create-server.d.ts.map +1 -0
  35. package/dist/create-server.js +17 -0
  36. package/dist/create-server.js.map +1 -0
  37. package/dist/deepseek-harness/README.md +112 -0
  38. package/dist/deepseek-harness/deepseek-mcp-harness.mjs +292 -0
  39. package/dist/dispatcher.d.ts +34 -0
  40. package/dist/dispatcher.d.ts.map +1 -0
  41. package/dist/dispatcher.js +72 -0
  42. package/dist/dispatcher.js.map +1 -0
  43. package/dist/heartbeat.d.ts +17 -0
  44. package/dist/heartbeat.d.ts.map +1 -0
  45. package/dist/heartbeat.js +36 -0
  46. package/dist/heartbeat.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +255 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/install-plugin.d.ts +16 -0
  52. package/dist/install-plugin.d.ts.map +1 -0
  53. package/dist/install-plugin.js +125 -0
  54. package/dist/install-plugin.js.map +1 -0
  55. package/dist/instance-lock.d.ts +5 -0
  56. package/dist/instance-lock.d.ts.map +1 -0
  57. package/dist/instance-lock.js +77 -0
  58. package/dist/instance-lock.js.map +1 -0
  59. package/dist/list-tools-handler.d.ts +16 -0
  60. package/dist/list-tools-handler.d.ts.map +1 -0
  61. package/dist/list-tools-handler.js +50 -0
  62. package/dist/list-tools-handler.js.map +1 -0
  63. package/dist/plugin-liveness.d.ts +48 -0
  64. package/dist/plugin-liveness.d.ts.map +1 -0
  65. package/dist/plugin-liveness.js +75 -0
  66. package/dist/plugin-liveness.js.map +1 -0
  67. package/dist/plugin-socket.d.ts +32 -0
  68. package/dist/plugin-socket.d.ts.map +1 -0
  69. package/dist/plugin-socket.js +97 -0
  70. package/dist/plugin-socket.js.map +1 -0
  71. package/dist/ports.d.ts +3 -0
  72. package/dist/ports.d.ts.map +1 -0
  73. package/dist/ports.js +22 -0
  74. package/dist/ports.js.map +1 -0
  75. package/dist/token.d.ts +3 -0
  76. package/dist/token.d.ts.map +1 -0
  77. package/dist/token.js +23 -0
  78. package/dist/token.js.map +1 -0
  79. package/dist/tool-contracts.d.ts +13 -0
  80. package/dist/tool-contracts.d.ts.map +1 -0
  81. package/dist/tool-contracts.js +1422 -0
  82. package/dist/tool-contracts.js.map +1 -0
  83. package/dist/tool-handler.d.ts +23 -0
  84. package/dist/tool-handler.d.ts.map +1 -0
  85. package/dist/tool-handler.js +96 -0
  86. package/dist/tool-handler.js.map +1 -0
  87. package/dist/validate-args.d.ts +7 -0
  88. package/dist/validate-args.d.ts.map +1 -0
  89. package/dist/validate-args.js +37 -0
  90. package/dist/validate-args.js.map +1 -0
  91. package/dist/version.d.ts +2 -0
  92. package/dist/version.d.ts.map +1 -0
  93. package/dist/version.js +4 -0
  94. package/dist/version.js.map +1 -0
  95. package/dist/wait-until.d.ts +7 -0
  96. package/dist/wait-until.d.ts.map +1 -0
  97. package/dist/wait-until.js +15 -0
  98. package/dist/wait-until.js.map +1 -0
  99. package/package.json +73 -0
@@ -0,0 +1,256 @@
1
+ -- HandlerCatalog.lua
2
+ -- Catalog-level inventory tools: folders, keyword tree and the Library view
3
+ -- filter. Ported from znznzna/lightroom-cli (MIT) CatalogModule.lua and
4
+ -- adapted to this plugin's gate/verification conventions.
5
+
6
+ local LrApplication = import 'LrApplication'
7
+
8
+ local Log = require 'Log'
9
+
10
+ local CatalogHandler = {}
11
+
12
+ -- =====================================================================
13
+ -- list_folders — folder tree with photo counts
14
+ -- =====================================================================
15
+ --
16
+ -- LrCatalog:getFolders() returns the root folders shown in the Folders
17
+ -- panel; each LrFolder exposes getPath()/getName()/getPhotos(false|true)/
18
+ -- getChildren()/getParent(). Folders have no localIdentifier, so the path
19
+ -- doubles as the id (same convention as lightroom-cli). The tree is walked
20
+ -- inside a read gate — these are plain catalog reads like the collection
21
+ -- enumeration in HandlerCollections (unlike getAllPhotos/findPhotos, they
22
+ -- do not yield, see issues #124/#134 for the calls that must stay outside).
23
+
24
+ function CatalogHandler.listFolders(args)
25
+ args = args or {}
26
+ local includeSubfolders = args.include_subfolders == true
27
+
28
+ local catalog = LrApplication.activeCatalog()
29
+
30
+ local folders = nil
31
+ catalog:withReadAccessDo(function()
32
+ folders = catalog:getFolders() or {}
33
+ end)
34
+
35
+ local function describeFolder(folder, depth)
36
+ local path = folder:getPath()
37
+ local entry = {
38
+ id = path,
39
+ name = folder:getName(),
40
+ path = path,
41
+ type = folder:type(),
42
+ depth = depth,
43
+ photo_count = #(folder:getPhotos(false) or {}),
44
+ total_photo_count = #(folder:getPhotos(true) or {}),
45
+ }
46
+
47
+ local parent = folder:getParent()
48
+ if parent then
49
+ entry.parent = parent:getPath()
50
+ end
51
+
52
+ if includeSubfolders then
53
+ entry.subfolders = {}
54
+ local children = folder:getChildren() or {}
55
+ for _, child in ipairs(children) do
56
+ table.insert(entry.subfolders, describeFolder(child, depth + 1))
57
+ end
58
+ end
59
+
60
+ return entry
61
+ end
62
+
63
+ local result = {}
64
+ for _, folder in ipairs(folders) do
65
+ table.insert(result, describeFolder(folder, 0))
66
+ end
67
+
68
+ Log.info(string.format("listFolders: %d root folder(s), subfolders=%s",
69
+ #result, tostring(includeSubfolders)))
70
+
71
+ return {
72
+ success = true,
73
+ folders = result,
74
+ count = #result,
75
+ include_subfolders = includeSubfolders,
76
+ message = string.format("Found %d root folder(s)", #result),
77
+ }
78
+ end
79
+
80
+ -- =====================================================================
81
+ -- list_keywords — keyword tree entries with photo counts
82
+ -- =====================================================================
83
+ --
84
+ -- LrCatalog:getKeywords() returns the top-level keyword objects; each one
85
+ -- exposes getName()/getPhotos(). The SDK does not expose a child-keyword
86
+ -- walk, so this lists the flat top-level set (same coverage as
87
+ -- lightroom-cli's `catalog keywords`). Use set_keywords to attach them.
88
+
89
+ function CatalogHandler.listKeywords(args)
90
+ args = args or {}
91
+
92
+ local catalog = LrApplication.activeCatalog()
93
+
94
+ local keywords = nil
95
+ catalog:withReadAccessDo(function()
96
+ keywords = catalog:getKeywords() or {}
97
+ end)
98
+
99
+ local result = {}
100
+ for _, keyword in ipairs(keywords) do
101
+ local ok, name = pcall(function() return keyword:getName() end)
102
+ local okCount, photos = pcall(function() return keyword:getPhotos() end)
103
+ table.insert(result, {
104
+ id = keyword.localIdentifier,
105
+ name = ok and name or nil,
106
+ photo_count = (okCount and type(photos) == "table") and #photos or 0,
107
+ })
108
+ end
109
+
110
+ table.sort(result, function(a, b) return tostring(a.name) < tostring(b.name) end)
111
+
112
+ Log.info(string.format("listKeywords: %d keyword(s)", #result))
113
+
114
+ return {
115
+ success = true,
116
+ keywords = result,
117
+ count = #result,
118
+ message = string.format("Found %d top-level keyword(s)", #result),
119
+ }
120
+ end
121
+
122
+ -- =====================================================================
123
+ -- manage_view_filter — Library grid filter (get / set / clear)
124
+ -- =====================================================================
125
+ --
126
+ -- catalog:setViewFilter(filterDesc) applies the Library filter bar; the
127
+ -- descriptor uses the same { combine, { criteria, operation, value } }
128
+ -- shape as findPhotos (documented for LrCatalog:setViewFilter in the SDK).
129
+ -- lightroom-cli calls it without a write gate (their tested
130
+ -- implementation, CatalogModule.lua:1637) — matched here, pcall-wrapped
131
+ -- so an LrC version that does gate it surfaces as an honest error instead
132
+ -- of wedging the bridge. `clear` passes an empty descriptor, which LrC
133
+ -- treats as "no filter" (the Filters Off state).
134
+
135
+ local MAX_VIEW_FILTER_RULES = 20
136
+
137
+ local function buildViewFilterDesc(rules, combine)
138
+ local desc = { combine = combine or "intersect" }
139
+ for i, rule in ipairs(rules) do
140
+ if type(rule) ~= "table" then
141
+ error("rules[" .. i .. "] must be an object", 0)
142
+ end
143
+ if type(rule.criteria) ~= "string" or rule.criteria == "" then
144
+ error("rules[" .. i .. "].criteria is required (e.g. 'keywords', 'rating', 'filename', 'cameraModel', 'copyName')", 0)
145
+ end
146
+ if type(rule.operation) ~= "string" or rule.operation == "" then
147
+ error("rules[" .. i .. "].operation is required (e.g. 'all', 'any', '==', '>=', 'in', 'startsWith')", 0)
148
+ end
149
+ if rule.value == nil then
150
+ error("rules[" .. i .. "].value is required", 0)
151
+ end
152
+ local r = {
153
+ criteria = rule.criteria,
154
+ operation = rule.operation,
155
+ value = tostring(rule.value),
156
+ }
157
+ if rule.value2 ~= nil then r.value2 = tostring(rule.value2) end
158
+ table.insert(desc, r)
159
+ end
160
+ return desc
161
+ end
162
+
163
+ function CatalogHandler.manageViewFilter(args)
164
+ args = args or {}
165
+ local action = args.action or "get"
166
+
167
+ if action ~= "get" and action ~= "set" and action ~= "clear" then
168
+ error("action must be 'get', 'set' or 'clear'", 0)
169
+ end
170
+
171
+ local catalog = LrApplication.activeCatalog()
172
+
173
+ if action == "get" then
174
+ local current = nil
175
+ local ok, err = pcall(function()
176
+ catalog:withReadAccessDo(function()
177
+ current = catalog:getCurrentViewFilter()
178
+ end)
179
+ end)
180
+ if not ok then
181
+ -- Fall back to a bare call the way lightroom-cli reads it.
182
+ local bareOk, bareErr = pcall(function()
183
+ return catalog:getCurrentViewFilter()
184
+ end)
185
+ if not bareOk then
186
+ error("getCurrentViewFilter failed: " .. tostring(err) .. " / " .. tostring(bareErr), 0)
187
+ end
188
+ current = bareErr
189
+ end
190
+ return {
191
+ success = true,
192
+ action = "get",
193
+ filter = current,
194
+ has_filter = type(current) == "table" and next(current) ~= nil,
195
+ message = "Current Library view filter read"
196
+ .. (type(current) == "table" and next(current) ~= nil and "" or " (no filter active)"),
197
+ }
198
+ end
199
+
200
+ if action == "set" then
201
+ local rules = args.rules
202
+ if type(rules) ~= "table" or #rules == 0 then
203
+ error("rules is required when action is 'set'", 0)
204
+ end
205
+ if #rules > MAX_VIEW_FILTER_RULES then
206
+ error("rules must contain at most " .. MAX_VIEW_FILTER_RULES .. " entries", 0)
207
+ end
208
+ local combine = args.combine or "intersect"
209
+ if combine ~= "intersect" and combine ~= "union" then
210
+ error("combine must be 'intersect' (AND, default) or 'union' (OR)", 0)
211
+ end
212
+
213
+ local desc = buildViewFilterDesc(rules, combine)
214
+
215
+ local ok, err = pcall(function()
216
+ catalog:setViewFilter(desc)
217
+ end)
218
+ if not ok then
219
+ error("setViewFilter failed: " .. tostring(err), 0)
220
+ end
221
+
222
+ Log.info(string.format("manageViewFilter: set %d rule(s)", #rules))
223
+
224
+ return {
225
+ success = true,
226
+ action = "set",
227
+ rule_count = #rules,
228
+ combine = combine,
229
+ message = string.format("Library view filter set (%d rule(s))", #rules),
230
+ }
231
+ end
232
+
233
+ -- action == "clear"
234
+ local ok, err = pcall(function()
235
+ catalog:setViewFilter(nil)
236
+ end)
237
+ if not ok then
238
+ -- Some LrC builds reject nil; an empty descriptor is the fallback.
239
+ local emptyOk, emptyErr = pcall(function()
240
+ catalog:setViewFilter({})
241
+ end)
242
+ if not emptyOk then
243
+ error("could not clear the view filter: " .. tostring(err) .. " / " .. tostring(emptyErr), 0)
244
+ end
245
+ end
246
+
247
+ Log.info("manageViewFilter: cleared")
248
+
249
+ return {
250
+ success = true,
251
+ action = "clear",
252
+ message = "Library view filter cleared",
253
+ }
254
+ end
255
+
256
+ return CatalogHandler
@@ -0,0 +1,337 @@
1
+ local LrApplication = import 'LrApplication'
2
+
3
+ local PhotoLookup = require 'PhotoLookup'
4
+ local Log = require 'Log'
5
+
6
+ local CollectionsHandler = {}
7
+
8
+ function CollectionsHandler.listCollections(args)
9
+ args = args or {}
10
+ local catalog = LrApplication.activeCatalog()
11
+ local all = {}
12
+
13
+ local limit = tonumber(args.limit) or 100
14
+ if limit < 0 then limit = 0 end
15
+ local offset = tonumber(args.offset) or 0
16
+ if offset < 0 then offset = 0 end
17
+
18
+ catalog:withReadAccessDo(function()
19
+ for _, collection in ipairs(catalog:getChildCollections()) do
20
+ table.insert(all, {
21
+ name = collection:getName(),
22
+ type = collection:type(),
23
+ photoCount = #collection:getPhotos(),
24
+ })
25
+ end
26
+
27
+ local function addCollectionsFromSet(collSet, prefix)
28
+ for _, coll in ipairs(collSet:getChildCollections()) do
29
+ table.insert(all, {
30
+ name = prefix .. coll:getName(),
31
+ parent = collSet:getName(),
32
+ type = coll:type(),
33
+ photoCount = #coll:getPhotos(),
34
+ })
35
+ end
36
+ for _, childSet in ipairs(collSet:getChildCollectionSets()) do
37
+ addCollectionsFromSet(childSet, prefix .. childSet:getName() .. " / ")
38
+ end
39
+ end
40
+
41
+ for _, set in ipairs(catalog:getChildCollectionSets()) do
42
+ addCollectionsFromSet(set, set:getName() .. " / ")
43
+ end
44
+ end)
45
+
46
+ local total = #all
47
+ local last = math.min(offset + limit, total)
48
+ local slice = {}
49
+ for i = offset + 1, last do
50
+ table.insert(slice, all[i])
51
+ end
52
+
53
+ Log.info(string.format("Found %d collections, returning %d (offset=%d, limit=%d)",
54
+ total, #slice, offset, limit))
55
+
56
+ return {
57
+ count = total,
58
+ collections = slice,
59
+ has_more = (offset + #slice) < total,
60
+ }
61
+ end
62
+
63
+ function CollectionsHandler.createCollection(args)
64
+ if type(args.name) ~= "string" or args.name:match("^%s*$") then
65
+ error("name is required")
66
+ end
67
+
68
+ local catalog = LrApplication.activeCatalog()
69
+ local collectionName = args.name
70
+
71
+ -- add_to_collection addresses collections by name, so a second collection
72
+ -- with the same name makes that lookup ambiguous: photos would silently
73
+ -- land in whichever one enumerates first.
74
+ for _, collection in ipairs(catalog:getChildCollections()) do
75
+ if collection:getName() == collectionName then
76
+ error("Collection already exists: " .. collectionName)
77
+ end
78
+ end
79
+
80
+ catalog:withWriteAccessDo("Create Collection", function()
81
+ catalog:createCollection(collectionName)
82
+ Log.info("Created collection: " .. collectionName)
83
+ end)
84
+
85
+ return {
86
+ success = true,
87
+ message = "Collection created: " .. collectionName
88
+ }
89
+ end
90
+
91
+ function CollectionsHandler.addToCollection(args)
92
+ if not args.collection_name then
93
+ error("collection_name is required")
94
+ end
95
+
96
+ if not args.photo_ids or #args.photo_ids == 0 then
97
+ error("photo_ids is required")
98
+ end
99
+
100
+ local catalog = LrApplication.activeCatalog()
101
+ local addedCount = 0
102
+ local missingIds = {}
103
+ local missingCount = 0
104
+
105
+ catalog:withWriteAccessDo("Add Photos to Collection", function()
106
+ -- Find the collection
107
+ local targetCollection = nil
108
+ local collections = catalog:getChildCollections()
109
+
110
+ for _, collection in ipairs(collections) do
111
+ if collection:getName() == args.collection_name then
112
+ targetCollection = collection
113
+ break
114
+ end
115
+ end
116
+
117
+ -- Also search in collection sets
118
+ if not targetCollection then
119
+ local collectionSets = catalog:getChildCollectionSets()
120
+ local function findInSet(collSet)
121
+ local setCollections = collSet:getChildCollections()
122
+ for _, coll in ipairs(setCollections) do
123
+ if coll:getName() == args.collection_name then
124
+ return coll
125
+ end
126
+ end
127
+
128
+ local childSets = collSet:getChildCollectionSets()
129
+ for _, childSet in ipairs(childSets) do
130
+ local found = findInSet(childSet)
131
+ if found then
132
+ return found
133
+ end
134
+ end
135
+
136
+ return nil
137
+ end
138
+
139
+ for _, set in ipairs(collectionSets) do
140
+ targetCollection = findInSet(set)
141
+ if targetCollection then
142
+ break
143
+ end
144
+ end
145
+ end
146
+
147
+ if not targetCollection then
148
+ error("Collection not found: " .. args.collection_name)
149
+ end
150
+
151
+ -- Find and add photos
152
+ local photosToAdd = {}
153
+ local resolved = PhotoLookup.resolveMany(catalog, args.photo_ids)
154
+ for _, entry in ipairs(resolved) do
155
+ if entry.photo then
156
+ table.insert(photosToAdd, entry.photo)
157
+ else
158
+ missingCount = missingCount + 1
159
+ missingIds[missingCount] = tostring(entry.id)
160
+ end
161
+ end
162
+
163
+ if #photosToAdd > 0 then
164
+ targetCollection:addPhotos(photosToAdd)
165
+ addedCount = #photosToAdd
166
+ end
167
+ end)
168
+
169
+ Log.info(string.format("Added %d photos to collection: %s", addedCount, args.collection_name))
170
+
171
+ -- Unresolvable ids used to vanish into a "success" with added=0, leaving the
172
+ -- caller no way to tell a typo'd id from an empty add.
173
+ return {
174
+ success = true,
175
+ added = addedCount,
176
+ missing = missingIds,
177
+ message = string.format("Added %d photos to collection (%d ids not found)",
178
+ addedCount, missingCount)
179
+ }
180
+ end
181
+
182
+ -- =====================================================================
183
+ -- create_collection_set — folder that groups collections
184
+ -- =====================================================================
185
+ --
186
+ -- catalog:createCollectionSet(name, targetSet, position) works like
187
+ -- createCollection. The parent is addressed by name like every other
188
+ -- collection tool here; an optional parent set keeps collections
189
+ -- organized in the Catalog panel.
190
+
191
+ function CollectionsHandler.createCollectionSet(args)
192
+ if type(args.name) ~= "string" or args.name:match("^%s*$") then
193
+ error("name is required")
194
+ end
195
+
196
+ local parentName = args.parent_set_name
197
+
198
+ local catalog = LrApplication.activeCatalog()
199
+
200
+ local parentSet = nil
201
+ if parentName ~= nil and parentName ~= "" then
202
+ catalog:withReadAccessDo(function()
203
+ for _, set in ipairs(catalog:getChildCollectionSets()) do
204
+ if set:getName() == parentName then
205
+ parentSet = set
206
+ break
207
+ end
208
+ end
209
+ end)
210
+ if not parentSet then
211
+ error("Parent collection set not found: " .. parentName)
212
+ end
213
+ end
214
+
215
+ local createdSet = nil
216
+ catalog:withWriteAccessDo("Create Collection Set", function()
217
+ createdSet = catalog:createCollectionSet(args.name, parentSet, true)
218
+ end)
219
+
220
+ if not createdSet then
221
+ error("createCollectionSet returned no set (duplicate name?)")
222
+ end
223
+
224
+ local verifiedName = nil
225
+ pcall(function() verifiedName = createdSet:getName() end)
226
+
227
+ Log.info(string.format("Created collection set '%s'", args.name))
228
+
229
+ local result = {
230
+ success = true,
231
+ name = args.name,
232
+ verified_name = verifiedName,
233
+ parent = parentName,
234
+ message = string.format("Collection set created: %s", args.name),
235
+ }
236
+ if verifiedName == nil then
237
+ result.warning = "Set created but its name could not be read back."
238
+ end
239
+ return result
240
+ end
241
+
242
+ -- =====================================================================
243
+ -- get_collection_photos — photos inside a named collection
244
+ -- =====================================================================
245
+ --
246
+ -- The same recursive by-name lookup addToCollection uses, then a paginated
247
+ -- listing with the fields harnesses need to keep working (id, path,
248
+ -- filename, rating, label). Ported from lightroom-cli getCollectionPhotos,
249
+ -- which addresses collections by localIdentifier; names fit this fork's
250
+ -- conventions better (list_collections exposes names).
251
+
252
+ function CollectionsHandler.getCollectionPhotos(args)
253
+ if type(args.collection_name) ~= "string" or args.collection_name == "" then
254
+ error("collection_name is required")
255
+ end
256
+
257
+ local limit = math.floor(tonumber(args.limit) or 100)
258
+ if limit < 0 then limit = 0 end
259
+ local offset = math.floor(tonumber(args.offset) or 0)
260
+ if offset < 0 then offset = 0 end
261
+
262
+ local catalog = LrApplication.activeCatalog()
263
+
264
+ -- Find the collection by name, top level first, then inside sets
265
+ -- (recursive, same walk as addToCollection).
266
+ local targetCollection = nil
267
+ catalog:withReadAccessDo(function()
268
+ for _, collection in ipairs(catalog:getChildCollections()) do
269
+ if collection:getName() == args.collection_name then
270
+ targetCollection = collection
271
+ break
272
+ end
273
+ end
274
+
275
+ if not targetCollection then
276
+ local function findInSet(collSet)
277
+ for _, coll in ipairs(collSet:getChildCollections()) do
278
+ if coll:getName() == args.collection_name then
279
+ return coll
280
+ end
281
+ end
282
+ for _, childSet in ipairs(collSet:getChildCollectionSets()) do
283
+ local found = findInSet(childSet)
284
+ if found then return found end
285
+ end
286
+ return nil
287
+ end
288
+
289
+ for _, set in ipairs(catalog:getChildCollectionSets()) do
290
+ targetCollection = findInSet(set)
291
+ if targetCollection then break end
292
+ end
293
+ end
294
+ end)
295
+
296
+ if not targetCollection then
297
+ error("Collection not found: " .. args.collection_name)
298
+ end
299
+
300
+ local allPhotos = nil
301
+ catalog:withReadAccessDo(function()
302
+ allPhotos = targetCollection:getPhotos() or {}
303
+ end)
304
+
305
+ local total = #allPhotos
306
+
307
+ local results = {}
308
+ local last = math.min(offset + limit, total)
309
+ catalog:withReadAccessDo(function()
310
+ for i = offset + 1, last do
311
+ local photo = allPhotos[i]
312
+ table.insert(results, {
313
+ id = photo.localIdentifier,
314
+ path = photo:getRawMetadata('path'),
315
+ filename = photo:getFormattedMetadata('fileName'),
316
+ rating = photo:getRawMetadata('rating'),
317
+ color_label = photo:getRawMetadata('label'),
318
+ })
319
+ end
320
+ end)
321
+
322
+ Log.info(string.format("getCollectionPhotos('%s'): %d photo(s), returning %d",
323
+ args.collection_name, total, #results))
324
+
325
+ return {
326
+ success = true,
327
+ collection = args.collection_name,
328
+ count = total,
329
+ photos = results,
330
+ returned = #results,
331
+ offset = offset,
332
+ has_more = (offset + #results) < total,
333
+ message = string.format("Collection '%s' has %d photo(s)", args.collection_name, total),
334
+ }
335
+ end
336
+
337
+ return CollectionsHandler