@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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/LightroomMCP.lrplugin/HandlerAI.lua +466 -0
- package/dist/LightroomMCP.lrplugin/HandlerAIMasks.lua +549 -0
- package/dist/LightroomMCP.lrplugin/HandlerCatalog.lua +256 -0
- package/dist/LightroomMCP.lrplugin/HandlerCollections.lua +337 -0
- package/dist/LightroomMCP.lrplugin/HandlerDevelop.lua +1668 -0
- package/dist/LightroomMCP.lrplugin/HandlerExport.lua +149 -0
- package/dist/LightroomMCP.lrplugin/HandlerImport.lua +108 -0
- package/dist/LightroomMCP.lrplugin/HandlerLocalAdjustments.lua +330 -0
- package/dist/LightroomMCP.lrplugin/HandlerMetadata.lua +169 -0
- package/dist/LightroomMCP.lrplugin/HandlerOrganization.lua +783 -0
- package/dist/LightroomMCP.lrplugin/HandlerPreview.lua +189 -0
- package/dist/LightroomMCP.lrplugin/HandlerSearch.lua +185 -0
- package/dist/LightroomMCP.lrplugin/HandlerSelection.lua +302 -0
- package/dist/LightroomMCP.lrplugin/HandlerSpots.lua +313 -0
- package/dist/LightroomMCP.lrplugin/HandlerWatermark.lua +59 -0
- package/dist/LightroomMCP.lrplugin/Info.lua +25 -0
- package/dist/LightroomMCP.lrplugin/JSON.lua +270 -0
- package/dist/LightroomMCP.lrplugin/Log.lua +92 -0
- package/dist/LightroomMCP.lrplugin/MenuShowStatus.lua +22 -0
- package/dist/LightroomMCP.lrplugin/PhotoLookup.lua +39 -0
- package/dist/LightroomMCP.lrplugin/PluginInfoProvider.lua +865 -0
- package/dist/LightroomMCP.lrplugin/PluginInit.lua +48 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +31 -0
- package/dist/cli.js.map +1 -0
- package/dist/configs/claude-code.mcp.json +9 -0
- package/dist/configs/claude-desktop.json +12 -0
- package/dist/configs/generic-stdio-client.json +10 -0
- package/dist/configs/mcp-proxy-http.json +9 -0
- package/dist/create-server.d.ts +12 -0
- package/dist/create-server.d.ts.map +1 -0
- package/dist/create-server.js +17 -0
- package/dist/create-server.js.map +1 -0
- package/dist/deepseek-harness/README.md +112 -0
- package/dist/deepseek-harness/deepseek-mcp-harness.mjs +292 -0
- package/dist/dispatcher.d.ts +34 -0
- package/dist/dispatcher.d.ts.map +1 -0
- package/dist/dispatcher.js +72 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/heartbeat.d.ts +17 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +36 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +255 -0
- package/dist/index.js.map +1 -0
- package/dist/install-plugin.d.ts +16 -0
- package/dist/install-plugin.d.ts.map +1 -0
- package/dist/install-plugin.js +125 -0
- package/dist/install-plugin.js.map +1 -0
- package/dist/instance-lock.d.ts +5 -0
- package/dist/instance-lock.d.ts.map +1 -0
- package/dist/instance-lock.js +77 -0
- package/dist/instance-lock.js.map +1 -0
- package/dist/list-tools-handler.d.ts +16 -0
- package/dist/list-tools-handler.d.ts.map +1 -0
- package/dist/list-tools-handler.js +50 -0
- package/dist/list-tools-handler.js.map +1 -0
- package/dist/plugin-liveness.d.ts +48 -0
- package/dist/plugin-liveness.d.ts.map +1 -0
- package/dist/plugin-liveness.js +75 -0
- package/dist/plugin-liveness.js.map +1 -0
- package/dist/plugin-socket.d.ts +32 -0
- package/dist/plugin-socket.d.ts.map +1 -0
- package/dist/plugin-socket.js +97 -0
- package/dist/plugin-socket.js.map +1 -0
- package/dist/ports.d.ts +3 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +22 -0
- package/dist/ports.js.map +1 -0
- package/dist/token.d.ts +3 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +23 -0
- package/dist/token.js.map +1 -0
- package/dist/tool-contracts.d.ts +13 -0
- package/dist/tool-contracts.d.ts.map +1 -0
- package/dist/tool-contracts.js +1422 -0
- package/dist/tool-contracts.js.map +1 -0
- package/dist/tool-handler.d.ts +23 -0
- package/dist/tool-handler.d.ts.map +1 -0
- package/dist/tool-handler.js +96 -0
- package/dist/tool-handler.js.map +1 -0
- package/dist/validate-args.d.ts +7 -0
- package/dist/validate-args.d.ts.map +1 -0
- package/dist/validate-args.js +37 -0
- package/dist/validate-args.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/wait-until.d.ts +7 -0
- package/dist/wait-until.d.ts.map +1 -0
- package/dist/wait-until.js +15 -0
- package/dist/wait-until.js.map +1 -0
- 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
|