lightroom-cli 1.0.0__py3-none-any.whl
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.
- cli/__init__.py +0 -0
- cli/commands/__init__.py +0 -0
- cli/commands/ai_mask.py +228 -0
- cli/commands/catalog.py +378 -0
- cli/commands/develop.py +686 -0
- cli/commands/plugin.py +92 -0
- cli/commands/preview.py +48 -0
- cli/commands/selection.py +191 -0
- cli/commands/system.py +107 -0
- cli/completions.py +34 -0
- cli/decorators.py +92 -0
- cli/helpers.py +168 -0
- cli/main.py +60 -0
- cli/middleware.py +37 -0
- cli/output.py +135 -0
- cli/schema.py +92 -0
- cli/structured_group.py +83 -0
- cli/validation.py +204 -0
- lightroom_cli-1.0.0.dist-info/METADATA +333 -0
- lightroom_cli-1.0.0.dist-info/RECORD +54 -0
- lightroom_cli-1.0.0.dist-info/WHEEL +5 -0
- lightroom_cli-1.0.0.dist-info/entry_points.txt +2 -0
- lightroom_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- lightroom_cli-1.0.0.dist-info/top_level.txt +2 -0
- lightroom_sdk/__init__.py +24 -0
- lightroom_sdk/client.py +145 -0
- lightroom_sdk/exceptions.py +146 -0
- lightroom_sdk/paths.py +43 -0
- lightroom_sdk/plugin/AppShutdown.lua +9 -0
- lightroom_sdk/plugin/CatalogModule.lua +1534 -0
- lightroom_sdk/plugin/CommandRouter.lua +488 -0
- lightroom_sdk/plugin/Config.lua +60 -0
- lightroom_sdk/plugin/DevelopModule.lua +3879 -0
- lightroom_sdk/plugin/ErrorUtils.lua +225 -0
- lightroom_sdk/plugin/Info.lua +35 -0
- lightroom_sdk/plugin/Logger.lua +54 -0
- lightroom_sdk/plugin/MenuActions.lua +77 -0
- lightroom_sdk/plugin/MessageProtocol.lua +403 -0
- lightroom_sdk/plugin/PlatformPaths.lua +11 -0
- lightroom_sdk/plugin/PluginInit.lua +492 -0
- lightroom_sdk/plugin/PluginShutdown.lua +9 -0
- lightroom_sdk/plugin/PreviewModule.lua +521 -0
- lightroom_sdk/plugin/SelectionModule.lua +329 -0
- lightroom_sdk/plugin/SimpleSocketBridge.lua +403 -0
- lightroom_sdk/plugin/StopMenuAction.lua +53 -0
- lightroom_sdk/presets.py +24 -0
- lightroom_sdk/protocol.py +30 -0
- lightroom_sdk/resilient_bridge.py +157 -0
- lightroom_sdk/retry.py +39 -0
- lightroom_sdk/schema.py +1639 -0
- lightroom_sdk/socket_bridge.py +228 -0
- lightroom_sdk/types/__init__.py +22 -0
- lightroom_sdk/types/catalog.py +44 -0
- lightroom_sdk/types/develop.py +156 -0
cli/__init__.py
ADDED
|
File without changes
|
cli/commands/__init__.py
ADDED
|
File without changes
|
cli/commands/ai_mask.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""AI Mask CLI commands — lr develop ai <type>"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from cli.decorators import json_input_options
|
|
8
|
+
from cli.helpers import execute_command
|
|
9
|
+
from cli.output import OutputFormatter
|
|
10
|
+
|
|
11
|
+
AI_SELECTION_TYPES = ["subject", "sky", "background", "objects", "people", "landscape"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group("ai")
|
|
15
|
+
def ai():
|
|
16
|
+
"""AI mask commands (subject, sky, background, people, landscape, objects)"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _make_ai_type_command(selection_type: str, has_part: bool = False, part_choices: list[str] | None = None):
|
|
21
|
+
"""各 AI マスクタイプコマンドのファクトリ関数"""
|
|
22
|
+
|
|
23
|
+
params = [
|
|
24
|
+
click.Option(["--adjust"], default=None, help="JSON adjustment settings"),
|
|
25
|
+
click.Option(
|
|
26
|
+
["--adjust-preset"],
|
|
27
|
+
default=None,
|
|
28
|
+
help="Named preset (darken-sky, brighten-subject, etc)",
|
|
29
|
+
),
|
|
30
|
+
click.Option(["--dry-run"], is_flag=True, default=False, help="Preview without executing"),
|
|
31
|
+
click.Option(["--json", "json_str"], default=None, help="JSON string with all parameters"),
|
|
32
|
+
click.Option(
|
|
33
|
+
["--json-stdin", "json_stdin"],
|
|
34
|
+
is_flag=True,
|
|
35
|
+
default=False,
|
|
36
|
+
help="Read JSON parameters from stdin",
|
|
37
|
+
),
|
|
38
|
+
]
|
|
39
|
+
# --part is hidden until SDK support is verified
|
|
40
|
+
if has_part and part_choices:
|
|
41
|
+
params.insert(
|
|
42
|
+
0,
|
|
43
|
+
click.Option(
|
|
44
|
+
["--part"],
|
|
45
|
+
default=None,
|
|
46
|
+
type=click.Choice(part_choices),
|
|
47
|
+
help="Specific part to mask",
|
|
48
|
+
hidden=True,
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@click.pass_context
|
|
53
|
+
def command_func(ctx, **kwargs):
|
|
54
|
+
adjust = kwargs.get("adjust")
|
|
55
|
+
adjust_preset = kwargs.get("adjust_preset")
|
|
56
|
+
part = kwargs.get("part")
|
|
57
|
+
|
|
58
|
+
# Build params
|
|
59
|
+
cmd_params: dict = {"selectionType": selection_type}
|
|
60
|
+
if part:
|
|
61
|
+
cmd_params["part"] = part
|
|
62
|
+
|
|
63
|
+
# Resolve adjustments
|
|
64
|
+
adjustments = _resolve_adjustments(adjust, adjust_preset)
|
|
65
|
+
if isinstance(adjustments, str):
|
|
66
|
+
# Error message
|
|
67
|
+
click.echo(OutputFormatter.format_error(adjustments))
|
|
68
|
+
return
|
|
69
|
+
if adjustments:
|
|
70
|
+
cmd_params["adjustments"] = adjustments
|
|
71
|
+
|
|
72
|
+
execute_command(ctx, "develop.createAIMaskWithAdjustments", cmd_params, timeout=60.0)
|
|
73
|
+
|
|
74
|
+
cmd = click.Command(
|
|
75
|
+
name=selection_type,
|
|
76
|
+
callback=command_func,
|
|
77
|
+
params=params,
|
|
78
|
+
help=f"Create AI {selection_type} mask",
|
|
79
|
+
)
|
|
80
|
+
return cmd
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _resolve_adjustments(adjust_json: str | None, adjust_preset: str | None) -> dict | str | None:
|
|
84
|
+
"""--adjust JSON と --adjust-preset を解決する。エラー時は文字列を返す。"""
|
|
85
|
+
if adjust_json and adjust_preset:
|
|
86
|
+
return "Cannot use both --adjust and --adjust-preset"
|
|
87
|
+
|
|
88
|
+
if adjust_preset:
|
|
89
|
+
from lightroom_sdk.presets import get_preset
|
|
90
|
+
|
|
91
|
+
preset = get_preset(adjust_preset)
|
|
92
|
+
if preset is None:
|
|
93
|
+
from lightroom_sdk.presets import list_presets
|
|
94
|
+
|
|
95
|
+
available = ", ".join(list_presets())
|
|
96
|
+
return f"Unknown preset '{adjust_preset}'. Available: {available}"
|
|
97
|
+
return preset
|
|
98
|
+
|
|
99
|
+
if adjust_json:
|
|
100
|
+
try:
|
|
101
|
+
parsed = json.loads(adjust_json)
|
|
102
|
+
if not isinstance(parsed, dict):
|
|
103
|
+
return "--adjust must be a JSON object"
|
|
104
|
+
return parsed
|
|
105
|
+
except json.JSONDecodeError as e:
|
|
106
|
+
return f"Invalid JSON in --adjust: {e}"
|
|
107
|
+
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Register type commands
|
|
112
|
+
ai.add_command(_make_ai_type_command("subject"))
|
|
113
|
+
ai.add_command(_make_ai_type_command("sky"))
|
|
114
|
+
ai.add_command(_make_ai_type_command("background"))
|
|
115
|
+
ai.add_command(_make_ai_type_command("objects"))
|
|
116
|
+
ai.add_command(
|
|
117
|
+
_make_ai_type_command(
|
|
118
|
+
"people",
|
|
119
|
+
has_part=True,
|
|
120
|
+
part_choices=["eyes", "hair", "skin", "lips", "teeth", "clothes"],
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
ai.add_command(
|
|
124
|
+
_make_ai_type_command(
|
|
125
|
+
"landscape",
|
|
126
|
+
has_part=True,
|
|
127
|
+
part_choices=["mountain", "tree", "water", "building", "road"],
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@ai.command("presets")
|
|
133
|
+
@click.pass_context
|
|
134
|
+
def ai_presets(ctx):
|
|
135
|
+
"""List available adjustment presets"""
|
|
136
|
+
fmt = ctx.obj.get("output", "text") if ctx.obj else "text"
|
|
137
|
+
from lightroom_sdk.presets import AI_MASK_PRESETS
|
|
138
|
+
|
|
139
|
+
click.echo(OutputFormatter.format(AI_MASK_PRESETS, fmt))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@ai.command("reset")
|
|
143
|
+
@click.option(
|
|
144
|
+
"--confirm",
|
|
145
|
+
is_flag=True,
|
|
146
|
+
default=False,
|
|
147
|
+
help="Required confirmation flag (removes all masks)",
|
|
148
|
+
)
|
|
149
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
150
|
+
@json_input_options
|
|
151
|
+
@click.pass_context
|
|
152
|
+
def ai_reset(ctx, confirm, dry_run, **kwargs):
|
|
153
|
+
"""Remove all masks from the current photo (requires --confirm)"""
|
|
154
|
+
if not confirm and not dry_run:
|
|
155
|
+
fmt = ctx.obj.get("output", "text") if ctx.obj else "text"
|
|
156
|
+
click.echo(
|
|
157
|
+
OutputFormatter.format_error(
|
|
158
|
+
"This will remove all masks. Pass --confirm to proceed.",
|
|
159
|
+
fmt,
|
|
160
|
+
code="CONFIRMATION_REQUIRED",
|
|
161
|
+
suggestions=[
|
|
162
|
+
"Add --confirm flag: lr develop ai reset --confirm",
|
|
163
|
+
"Use --dry-run first to preview: lr develop ai reset --dry-run",
|
|
164
|
+
],
|
|
165
|
+
),
|
|
166
|
+
err=True,
|
|
167
|
+
)
|
|
168
|
+
ctx.exit(2)
|
|
169
|
+
return
|
|
170
|
+
execute_command(ctx, "develop.resetMasking", {})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@ai.command("list")
|
|
174
|
+
@json_input_options
|
|
175
|
+
@click.pass_context
|
|
176
|
+
def ai_list(ctx, **kwargs):
|
|
177
|
+
"""List all masks on the current photo"""
|
|
178
|
+
execute_command(ctx, "develop.getAllMasks", {})
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@ai.command("batch")
|
|
182
|
+
@click.argument("type", type=click.Choice(AI_SELECTION_TYPES))
|
|
183
|
+
@click.option("--photos", default=None, help="Comma-separated photo IDs")
|
|
184
|
+
@click.option("--all-selected", is_flag=True, help="Apply to all selected photos")
|
|
185
|
+
@click.option("--adjust", default=None, help="JSON adjustment settings")
|
|
186
|
+
@click.option("--adjust-preset", default=None, help="Named preset")
|
|
187
|
+
@click.option("--dry-run", is_flag=True, help="Show targets without applying")
|
|
188
|
+
@click.option("--continue-on-error", is_flag=True, default=False, help="Continue on errors")
|
|
189
|
+
@json_input_options
|
|
190
|
+
@click.pass_context
|
|
191
|
+
def ai_batch(
|
|
192
|
+
ctx,
|
|
193
|
+
type,
|
|
194
|
+
photos,
|
|
195
|
+
all_selected,
|
|
196
|
+
adjust,
|
|
197
|
+
adjust_preset,
|
|
198
|
+
dry_run,
|
|
199
|
+
continue_on_error,
|
|
200
|
+
**kwargs,
|
|
201
|
+
):
|
|
202
|
+
"""Apply AI mask to multiple photos"""
|
|
203
|
+
if not photos and not all_selected:
|
|
204
|
+
click.echo(OutputFormatter.format_error("Specify --photos or --all-selected"))
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
if dry_run:
|
|
208
|
+
target = "all selected photos" if all_selected else f"photos: {photos}"
|
|
209
|
+
click.echo(f"Dry run: would apply AI {type} mask to {target}")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
# Resolve adjustments
|
|
213
|
+
adjustments = _resolve_adjustments(adjust, adjust_preset)
|
|
214
|
+
if isinstance(adjustments, str):
|
|
215
|
+
click.echo(OutputFormatter.format_error(adjustments))
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
cmd_params: dict = {
|
|
219
|
+
"selectionType": type,
|
|
220
|
+
"allSelected": all_selected,
|
|
221
|
+
"continueOnError": continue_on_error,
|
|
222
|
+
}
|
|
223
|
+
if photos:
|
|
224
|
+
cmd_params["photoIds"] = [p.strip() for p in photos.split(",")]
|
|
225
|
+
if adjustments:
|
|
226
|
+
cmd_params["adjustments"] = adjustments
|
|
227
|
+
|
|
228
|
+
execute_command(ctx, "develop.batchAIMask", cmd_params, timeout=300.0)
|
cli/commands/catalog.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from cli.decorators import json_input_options
|
|
6
|
+
from cli.helpers import execute_command
|
|
7
|
+
from cli.output import OutputFormatter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def catalog():
|
|
12
|
+
"""Catalog commands (list, search, find, get-selected, get-info, set-rating, add-keywords, etc.)"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@catalog.command("get-selected")
|
|
17
|
+
@json_input_options
|
|
18
|
+
@click.pass_context
|
|
19
|
+
def get_selected(ctx, **kwargs):
|
|
20
|
+
"""Get currently selected photos"""
|
|
21
|
+
execute_command(ctx, "catalog.getSelectedPhotos", {})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@catalog.command("list")
|
|
25
|
+
@click.option("--limit", default=50, type=int, help="Max photos to return")
|
|
26
|
+
@click.option("--offset", default=0, type=int, help="Offset for pagination")
|
|
27
|
+
@json_input_options
|
|
28
|
+
@click.pass_context
|
|
29
|
+
def list_photos(ctx, limit, offset, **kwargs):
|
|
30
|
+
"""List photos in catalog"""
|
|
31
|
+
execute_command(ctx, "catalog.getAllPhotos", {"limit": limit, "offset": offset}, timeout=60.0)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@catalog.command("search")
|
|
35
|
+
@click.argument("query")
|
|
36
|
+
@click.option("--limit", default=50, type=int)
|
|
37
|
+
@json_input_options
|
|
38
|
+
@click.pass_context
|
|
39
|
+
def search(ctx, query, limit, **kwargs):
|
|
40
|
+
"""Search photos by keyword"""
|
|
41
|
+
execute_command(ctx, "catalog.searchPhotos", {"query": query, "limit": limit}, timeout=60.0)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@catalog.command("get-info")
|
|
45
|
+
@click.argument("photo_id")
|
|
46
|
+
@json_input_options
|
|
47
|
+
@click.pass_context
|
|
48
|
+
def get_info(ctx, photo_id, **kwargs):
|
|
49
|
+
"""Get detailed info for a photo"""
|
|
50
|
+
execute_command(ctx, "catalog.getPhotoMetadata", {"photoId": photo_id})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@catalog.command("set-rating")
|
|
54
|
+
@click.argument("photo_id")
|
|
55
|
+
@click.argument("rating", type=click.IntRange(0, 5))
|
|
56
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
57
|
+
@json_input_options
|
|
58
|
+
@click.pass_context
|
|
59
|
+
def set_rating(ctx, photo_id, rating, dry_run, **kwargs):
|
|
60
|
+
"""Set photo rating (0-5)"""
|
|
61
|
+
execute_command(ctx, "catalog.setRating", {"photoId": photo_id, "rating": rating})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@catalog.command("add-keywords")
|
|
65
|
+
@click.argument("photo_id")
|
|
66
|
+
@click.argument("keywords", nargs=-1, required=True)
|
|
67
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
68
|
+
@json_input_options
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def add_keywords(ctx, photo_id, keywords, dry_run, **kwargs):
|
|
71
|
+
"""Add keywords to a photo"""
|
|
72
|
+
execute_command(ctx, "catalog.addKeywords", {"photoId": photo_id, "keywords": list(keywords)})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@catalog.command("set-flag")
|
|
76
|
+
@click.argument("photo_id")
|
|
77
|
+
@click.argument("flag", type=click.Choice(["pick", "reject", "none"]))
|
|
78
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
79
|
+
@json_input_options
|
|
80
|
+
@click.pass_context
|
|
81
|
+
def set_flag(ctx, photo_id, flag, dry_run, **kwargs):
|
|
82
|
+
"""Set photo flag (pick/reject/none)"""
|
|
83
|
+
flag_map = {"pick": 1, "reject": -1, "none": 0}
|
|
84
|
+
execute_command(ctx, "catalog.setFlag", {"photoId": photo_id, "flag": flag_map[flag]})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@catalog.command("get-flag")
|
|
88
|
+
@click.argument("photo_id")
|
|
89
|
+
@json_input_options
|
|
90
|
+
@click.pass_context
|
|
91
|
+
def get_flag(ctx, photo_id, **kwargs):
|
|
92
|
+
"""Get photo flag status"""
|
|
93
|
+
execute_command(ctx, "catalog.getFlag", {"photoId": photo_id})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@catalog.command("find")
|
|
97
|
+
@click.option("--flag", type=click.Choice(["pick", "reject", "none"]), help="Flag condition")
|
|
98
|
+
@click.option("--rating", type=int, help="Rating (0-5)")
|
|
99
|
+
@click.option(
|
|
100
|
+
"--rating-op",
|
|
101
|
+
default="==",
|
|
102
|
+
type=click.Choice(["==", ">=", "<=", ">", "<"]),
|
|
103
|
+
help="Rating comparison operator",
|
|
104
|
+
)
|
|
105
|
+
@click.option("--color-label", help="Color label (red/yellow/green/blue/purple/none)")
|
|
106
|
+
@click.option("--camera", help="Camera model name")
|
|
107
|
+
@click.option("--limit", default=50, type=int, help="Max results")
|
|
108
|
+
@click.option("--offset", default=0, type=int, help="Offset for pagination")
|
|
109
|
+
@json_input_options
|
|
110
|
+
@click.pass_context
|
|
111
|
+
def find_photos(ctx, flag, rating, rating_op, color_label, camera, limit, offset, **kwargs):
|
|
112
|
+
"""Find photos by structured criteria"""
|
|
113
|
+
search_desc = {}
|
|
114
|
+
if flag:
|
|
115
|
+
search_desc["flag"] = flag
|
|
116
|
+
if rating is not None:
|
|
117
|
+
search_desc["rating"] = rating
|
|
118
|
+
search_desc["ratingOp"] = rating_op
|
|
119
|
+
if color_label:
|
|
120
|
+
search_desc["colorLabel"] = color_label
|
|
121
|
+
if camera:
|
|
122
|
+
search_desc["camera"] = camera
|
|
123
|
+
|
|
124
|
+
execute_command(
|
|
125
|
+
ctx,
|
|
126
|
+
"catalog.findPhotos",
|
|
127
|
+
{"searchDesc": search_desc, "limit": limit, "offset": offset},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@catalog.command("select")
|
|
132
|
+
@click.argument("photo_ids", nargs=-1, required=True)
|
|
133
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
134
|
+
@json_input_options
|
|
135
|
+
@click.pass_context
|
|
136
|
+
def select_photos(ctx, photo_ids, dry_run, **kwargs):
|
|
137
|
+
"""Select photos by ID"""
|
|
138
|
+
execute_command(ctx, "catalog.setSelectedPhotos", {"photoIds": list(photo_ids)})
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@catalog.command("find-by-path")
|
|
142
|
+
@click.argument("path")
|
|
143
|
+
@json_input_options
|
|
144
|
+
@click.pass_context
|
|
145
|
+
def find_by_path(ctx, path, **kwargs):
|
|
146
|
+
"""Find photo by file path"""
|
|
147
|
+
execute_command(ctx, "catalog.findPhotoByPath", {"path": path})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@catalog.command("collections")
|
|
151
|
+
@json_input_options
|
|
152
|
+
@click.pass_context
|
|
153
|
+
def collections(ctx, **kwargs):
|
|
154
|
+
"""List collections in catalog"""
|
|
155
|
+
execute_command(ctx, "catalog.getCollections", {})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@catalog.command("keywords")
|
|
159
|
+
@json_input_options
|
|
160
|
+
@click.pass_context
|
|
161
|
+
def keywords(ctx, **kwargs):
|
|
162
|
+
"""List keywords in catalog"""
|
|
163
|
+
execute_command(ctx, "catalog.getKeywords", {})
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@catalog.command("folders")
|
|
167
|
+
@click.option("--recursive", is_flag=True, help="Include subfolders")
|
|
168
|
+
@json_input_options
|
|
169
|
+
@click.pass_context
|
|
170
|
+
def folders(ctx, recursive, **kwargs):
|
|
171
|
+
"""List folders in catalog"""
|
|
172
|
+
execute_command(ctx, "catalog.getFolders", {"includeSubfolders": recursive})
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@catalog.command("set-title")
|
|
176
|
+
@click.argument("photo_id")
|
|
177
|
+
@click.argument("title")
|
|
178
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
179
|
+
@json_input_options
|
|
180
|
+
@click.pass_context
|
|
181
|
+
def set_title(ctx, photo_id, title, dry_run, **kwargs):
|
|
182
|
+
"""Set photo title"""
|
|
183
|
+
execute_command(ctx, "catalog.setTitle", {"photoId": photo_id, "title": title})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@catalog.command("set-caption")
|
|
187
|
+
@click.argument("photo_id")
|
|
188
|
+
@click.argument("caption")
|
|
189
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
190
|
+
@json_input_options
|
|
191
|
+
@click.pass_context
|
|
192
|
+
def set_caption(ctx, photo_id, caption, dry_run, **kwargs):
|
|
193
|
+
"""Set photo caption"""
|
|
194
|
+
execute_command(ctx, "catalog.setCaption", {"photoId": photo_id, "caption": caption})
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@catalog.command("set-color-label")
|
|
198
|
+
@click.argument("photo_id")
|
|
199
|
+
@click.argument("label", type=click.Choice(["red", "yellow", "green", "blue", "purple", "none"]))
|
|
200
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
201
|
+
@json_input_options
|
|
202
|
+
@click.pass_context
|
|
203
|
+
def set_color_label(ctx, photo_id, label, dry_run, **kwargs):
|
|
204
|
+
"""Set photo color label"""
|
|
205
|
+
execute_command(ctx, "catalog.setColorLabel", {"photoId": photo_id, "label": label})
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@catalog.command("batch-metadata")
|
|
209
|
+
@click.argument("photo_ids", nargs=-1, required=True)
|
|
210
|
+
@click.option(
|
|
211
|
+
"--keys",
|
|
212
|
+
default="fileName,dateTimeOriginal,rating",
|
|
213
|
+
help="Comma-separated metadata keys",
|
|
214
|
+
)
|
|
215
|
+
@json_input_options
|
|
216
|
+
@click.pass_context
|
|
217
|
+
def batch_metadata(ctx, photo_ids, keys, **kwargs):
|
|
218
|
+
"""Get formatted metadata for multiple photos"""
|
|
219
|
+
execute_command(
|
|
220
|
+
ctx,
|
|
221
|
+
"catalog.batchGetFormattedMetadata",
|
|
222
|
+
{"photoIds": list(photo_ids), "keys": keys.split(",")},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@catalog.command("rotate-left")
|
|
227
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
228
|
+
@json_input_options
|
|
229
|
+
@click.pass_context
|
|
230
|
+
def rotate_left(ctx, dry_run, **kwargs):
|
|
231
|
+
"""Rotate selected photo left"""
|
|
232
|
+
execute_command(ctx, "catalog.rotateLeft", {})
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@catalog.command("rotate-right")
|
|
236
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
237
|
+
@json_input_options
|
|
238
|
+
@click.pass_context
|
|
239
|
+
def rotate_right(ctx, dry_run, **kwargs):
|
|
240
|
+
"""Rotate selected photo right"""
|
|
241
|
+
execute_command(ctx, "catalog.rotateRight", {})
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@catalog.command("create-virtual-copy")
|
|
245
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
246
|
+
@json_input_options
|
|
247
|
+
@click.pass_context
|
|
248
|
+
def create_virtual_copy(ctx, dry_run, **kwargs):
|
|
249
|
+
"""Create virtual copy of selected photo"""
|
|
250
|
+
execute_command(ctx, "catalog.createVirtualCopy", {})
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@catalog.command("set-metadata")
|
|
254
|
+
@click.argument("photo_id")
|
|
255
|
+
@click.argument("key")
|
|
256
|
+
@click.argument("value")
|
|
257
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
258
|
+
@json_input_options
|
|
259
|
+
@click.pass_context
|
|
260
|
+
def set_metadata(ctx, photo_id, key, value, dry_run, **kwargs):
|
|
261
|
+
"""Set arbitrary metadata key/value for a photo"""
|
|
262
|
+
execute_command(ctx, "catalog.setMetadata", {"photoId": photo_id, "key": key, "value": value})
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@catalog.command("create-collection")
|
|
266
|
+
@click.argument("name")
|
|
267
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
268
|
+
@json_input_options
|
|
269
|
+
@click.pass_context
|
|
270
|
+
def create_collection(ctx, name, dry_run, **kwargs):
|
|
271
|
+
"""Create a new collection"""
|
|
272
|
+
execute_command(ctx, "catalog.createCollection", {"name": name})
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@catalog.command("create-smart-collection")
|
|
276
|
+
@click.argument("name")
|
|
277
|
+
@click.option("--search-desc", default=None, help="JSON search descriptor")
|
|
278
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
279
|
+
@json_input_options
|
|
280
|
+
@click.pass_context
|
|
281
|
+
def create_smart_collection(ctx, name, search_desc, dry_run, **kwargs):
|
|
282
|
+
"""Create a smart collection"""
|
|
283
|
+
params = {"name": name}
|
|
284
|
+
if search_desc:
|
|
285
|
+
try:
|
|
286
|
+
params["searchDesc"] = json.loads(search_desc)
|
|
287
|
+
except json.JSONDecodeError as e:
|
|
288
|
+
click.echo(OutputFormatter.format_error(f"Invalid JSON for --search-desc: {e}"))
|
|
289
|
+
ctx.exit(1)
|
|
290
|
+
return
|
|
291
|
+
execute_command(ctx, "catalog.createSmartCollection", params)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@catalog.command("create-collection-set")
|
|
295
|
+
@click.argument("name")
|
|
296
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
297
|
+
@json_input_options
|
|
298
|
+
@click.pass_context
|
|
299
|
+
def create_collection_set(ctx, name, dry_run, **kwargs):
|
|
300
|
+
"""Create a collection set"""
|
|
301
|
+
execute_command(ctx, "catalog.createCollectionSet", {"name": name})
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@catalog.command("create-keyword")
|
|
305
|
+
@click.argument("keyword")
|
|
306
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
307
|
+
@json_input_options
|
|
308
|
+
@click.pass_context
|
|
309
|
+
def create_keyword(ctx, keyword, dry_run, **kwargs):
|
|
310
|
+
"""Create a keyword in catalog"""
|
|
311
|
+
execute_command(ctx, "catalog.createKeyword", {"keyword": keyword})
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@catalog.command("remove-keyword")
|
|
315
|
+
@click.argument("photo_id")
|
|
316
|
+
@click.argument("keyword")
|
|
317
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
318
|
+
@json_input_options
|
|
319
|
+
@click.pass_context
|
|
320
|
+
def remove_keyword(ctx, photo_id, keyword, dry_run, **kwargs):
|
|
321
|
+
"""Remove keyword from a photo"""
|
|
322
|
+
execute_command(ctx, "catalog.removeKeyword", {"photoId": photo_id, "keyword": keyword})
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@catalog.command("set-view-filter")
|
|
326
|
+
@click.option("--filter", "filter_json", required=True, help="JSON filter descriptor")
|
|
327
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
328
|
+
@json_input_options
|
|
329
|
+
@click.pass_context
|
|
330
|
+
def set_view_filter(ctx, filter_json, dry_run, **kwargs):
|
|
331
|
+
"""Set view filter"""
|
|
332
|
+
try:
|
|
333
|
+
filter_data = json.loads(filter_json)
|
|
334
|
+
except json.JSONDecodeError as e:
|
|
335
|
+
click.echo(OutputFormatter.format_error(f"Invalid JSON for --filter: {e}"))
|
|
336
|
+
ctx.exit(1)
|
|
337
|
+
return
|
|
338
|
+
execute_command(ctx, "catalog.setViewFilter", {"filter": filter_data})
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@catalog.command("get-view-filter")
|
|
342
|
+
@json_input_options
|
|
343
|
+
@click.pass_context
|
|
344
|
+
def get_view_filter(ctx, **kwargs):
|
|
345
|
+
"""Get current view filter"""
|
|
346
|
+
execute_command(ctx, "catalog.getCurrentViewFilter", {})
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@catalog.command("remove-from-catalog")
|
|
350
|
+
@click.argument("photo_id")
|
|
351
|
+
@click.option(
|
|
352
|
+
"--confirm",
|
|
353
|
+
is_flag=True,
|
|
354
|
+
default=False,
|
|
355
|
+
help="Required confirmation flag (this operation is irreversible)",
|
|
356
|
+
)
|
|
357
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Preview without executing")
|
|
358
|
+
@json_input_options
|
|
359
|
+
@click.pass_context
|
|
360
|
+
def remove_from_catalog(ctx, photo_id, confirm, dry_run, **kwargs):
|
|
361
|
+
"""Remove photo from catalog (irreversible, requires --confirm)"""
|
|
362
|
+
if not confirm and not dry_run:
|
|
363
|
+
fmt = ctx.obj.get("output", "text") if ctx.obj else "text"
|
|
364
|
+
click.echo(
|
|
365
|
+
OutputFormatter.format_error(
|
|
366
|
+
"This operation is irreversible. Pass --confirm to proceed.",
|
|
367
|
+
fmt,
|
|
368
|
+
code="CONFIRMATION_REQUIRED",
|
|
369
|
+
suggestions=[
|
|
370
|
+
"Add --confirm flag: lr catalog remove-from-catalog PHOTO_ID --confirm",
|
|
371
|
+
"Use --dry-run first to preview: lr catalog remove-from-catalog PHOTO_ID --dry-run",
|
|
372
|
+
],
|
|
373
|
+
),
|
|
374
|
+
err=True,
|
|
375
|
+
)
|
|
376
|
+
ctx.exit(2)
|
|
377
|
+
return
|
|
378
|
+
execute_command(ctx, "catalog.removeFromCatalog", {"photoId": photo_id})
|