lightroom-py 0.6.0__tar.gz
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.
- lightroom_py-0.6.0/.github/workflows/test.yml +42 -0
- lightroom_py-0.6.0/.gitignore +52 -0
- lightroom_py-0.6.0/.pre-commit-config.yaml +28 -0
- lightroom_py-0.6.0/AGENTS.md +21 -0
- lightroom_py-0.6.0/CHANGELOG.md +411 -0
- lightroom_py-0.6.0/CITATION.cff +33 -0
- lightroom_py-0.6.0/CONTRIBUTING.md +189 -0
- lightroom_py-0.6.0/LICENSE +21 -0
- lightroom_py-0.6.0/PKG-INFO +55 -0
- lightroom_py-0.6.0/PLAN.md +307 -0
- lightroom_py-0.6.0/README.md +296 -0
- lightroom_py-0.6.0/SKILL.md +104 -0
- lightroom_py-0.6.0/docs/architecture.md +38 -0
- lightroom_py-0.6.0/docs/cli-reference.md +306 -0
- lightroom_py-0.6.0/docs/development.md +70 -0
- lightroom_py-0.6.0/docs/examples/cull_workflow.py +53 -0
- lightroom_py-0.6.0/docs/examples/edit_in_imagemagick.py +32 -0
- lightroom_py-0.6.0/docs/mcp.md +64 -0
- lightroom_py-0.6.0/docs/python-api.md +106 -0
- lightroom_py-0.6.0/docs/troubleshooting.md +34 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/BridgeRunner.lua +237 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/BridgeState.lua +111 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/Configure.lua +73 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/Handlers.lua +1927 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/Info.lua +39 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/LightroomBridge.lua +40 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/StartBridge.lua +49 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/Status.lua +25 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/StopBridge.lua +15 -0
- lightroom_py-0.6.0/plugin/lightroom-py-bridge.lrplugin/json.lua +207 -0
- lightroom_py-0.6.0/pyproject.toml +129 -0
- lightroom_py-0.6.0/src/lightroom/__init__.py +44 -0
- lightroom_py-0.6.0/src/lightroom/__main__.py +6 -0
- lightroom_py-0.6.0/src/lightroom/_ai.py +96 -0
- lightroom_py-0.6.0/src/lightroom/_bridge_state.py +60 -0
- lightroom_py-0.6.0/src/lightroom/_catalog.py +68 -0
- lightroom_py-0.6.0/src/lightroom/_collections.py +103 -0
- lightroom_py-0.6.0/src/lightroom/_context.py +59 -0
- lightroom_py-0.6.0/src/lightroom/_core.py +173 -0
- lightroom_py-0.6.0/src/lightroom/_develop.py +828 -0
- lightroom_py-0.6.0/src/lightroom/_edit_in.py +165 -0
- lightroom_py-0.6.0/src/lightroom/_exiftool.py +191 -0
- lightroom_py-0.6.0/src/lightroom/_library.py +143 -0
- lightroom_py-0.6.0/src/lightroom/_logging.py +34 -0
- lightroom_py-0.6.0/src/lightroom/_metadata.py +170 -0
- lightroom_py-0.6.0/src/lightroom/_photos.py +218 -0
- lightroom_py-0.6.0/src/lightroom/_sqlite.py +552 -0
- lightroom_py-0.6.0/src/lightroom/bridge/__init__.py +13 -0
- lightroom_py-0.6.0/src/lightroom/bridge/protocol.py +79 -0
- lightroom_py-0.6.0/src/lightroom/bridge/server.py +237 -0
- lightroom_py-0.6.0/src/lightroom/cli/__init__.py +1 -0
- lightroom_py-0.6.0/src/lightroom/cli/ai.py +108 -0
- lightroom_py-0.6.0/src/lightroom/cli/bridge.py +508 -0
- lightroom_py-0.6.0/src/lightroom/cli/catalog.py +116 -0
- lightroom_py-0.6.0/src/lightroom/cli/collections.py +127 -0
- lightroom_py-0.6.0/src/lightroom/cli/develop.py +964 -0
- lightroom_py-0.6.0/src/lightroom/cli/doctor.py +147 -0
- lightroom_py-0.6.0/src/lightroom/cli/edit_in.py +111 -0
- lightroom_py-0.6.0/src/lightroom/cli/library.py +170 -0
- lightroom_py-0.6.0/src/lightroom/cli/metadata.py +205 -0
- lightroom_py-0.6.0/src/lightroom/cli/photos.py +490 -0
- lightroom_py-0.6.0/src/lightroom/cli/setup.py +180 -0
- lightroom_py-0.6.0/src/lightroom/cli/skill.py +53 -0
- lightroom_py-0.6.0/src/lightroom/client.py +135 -0
- lightroom_py-0.6.0/src/lightroom/exceptions.py +35 -0
- lightroom_py-0.6.0/src/lightroom/lightroom_cli.py +66 -0
- lightroom_py-0.6.0/src/lightroom/mcp_server.py +330 -0
- lightroom_py-0.6.0/src/lightroom/paths.py +57 -0
- lightroom_py-0.6.0/src/lightroom/types.py +44 -0
- lightroom_py-0.6.0/tests/__init__.py +0 -0
- lightroom_py-0.6.0/tests/conftest.py +166 -0
- lightroom_py-0.6.0/tests/test_ai_api.py +57 -0
- lightroom_py-0.6.0/tests/test_bridge_protocol.py +177 -0
- lightroom_py-0.6.0/tests/test_bridge_server.py +32 -0
- lightroom_py-0.6.0/tests/test_bridge_state.py +59 -0
- lightroom_py-0.6.0/tests/test_catalog_api.py +59 -0
- lightroom_py-0.6.0/tests/test_collections_api.py +81 -0
- lightroom_py-0.6.0/tests/test_develop_api.py +124 -0
- lightroom_py-0.6.0/tests/test_develop_v04.py +198 -0
- lightroom_py-0.6.0/tests/test_develop_v05.py +280 -0
- lightroom_py-0.6.0/tests/test_edit_in_api.py +117 -0
- lightroom_py-0.6.0/tests/test_library_api.py +82 -0
- lightroom_py-0.6.0/tests/test_metadata_api.py +204 -0
- lightroom_py-0.6.0/tests/test_photos_v04.py +133 -0
- lightroom_py-0.6.0/tests/test_resolve_paths.py +24 -0
- lightroom_py-0.6.0/tests/test_smoke.py +69 -0
- lightroom_py-0.6.0/tests/test_sqlite_reader.py +149 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: ${{ matrix.os }} / py ${{ matrix.python-version }}
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
os: [macos-latest, ubuntu-latest]
|
|
17
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install -e ".[dev]"
|
|
31
|
+
|
|
32
|
+
- name: ruff check
|
|
33
|
+
run: ruff check src tests
|
|
34
|
+
|
|
35
|
+
- name: ruff format
|
|
36
|
+
run: ruff format --check src tests
|
|
37
|
+
|
|
38
|
+
- name: mypy
|
|
39
|
+
run: mypy
|
|
40
|
+
|
|
41
|
+
- name: pytest
|
|
42
|
+
run: pytest -q
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
.eggs/
|
|
12
|
+
.installed.cfg
|
|
13
|
+
MANIFEST
|
|
14
|
+
|
|
15
|
+
# Virtualenv
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
env/
|
|
19
|
+
|
|
20
|
+
# Tooling caches
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.coverage
|
|
25
|
+
.coverage.*
|
|
26
|
+
htmlcov/
|
|
27
|
+
.tox/
|
|
28
|
+
|
|
29
|
+
# IDE / editor
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
.DS_Store
|
|
35
|
+
|
|
36
|
+
# Local config / secrets
|
|
37
|
+
.env
|
|
38
|
+
.env.local
|
|
39
|
+
*.local
|
|
40
|
+
|
|
41
|
+
# lightroom-py runtime state
|
|
42
|
+
.lightroom/
|
|
43
|
+
~/.lightroom/
|
|
44
|
+
|
|
45
|
+
# Lightroom artifacts that should never be committed
|
|
46
|
+
*.lrcat
|
|
47
|
+
*.lrcat-shm
|
|
48
|
+
*.lrcat-wal
|
|
49
|
+
*.lrdata/
|
|
50
|
+
|
|
51
|
+
# Build outputs
|
|
52
|
+
*.whl
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.8.6
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
|
|
9
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
10
|
+
rev: v5.0.0
|
|
11
|
+
hooks:
|
|
12
|
+
- id: trailing-whitespace
|
|
13
|
+
- id: end-of-file-fixer
|
|
14
|
+
- id: check-yaml
|
|
15
|
+
- id: check-toml
|
|
16
|
+
- id: check-merge-conflict
|
|
17
|
+
- id: check-added-large-files
|
|
18
|
+
args: [--maxkb=500]
|
|
19
|
+
|
|
20
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
21
|
+
rev: v1.13.0
|
|
22
|
+
hooks:
|
|
23
|
+
- id: mypy
|
|
24
|
+
files: ^src/lightroom/
|
|
25
|
+
additional_dependencies:
|
|
26
|
+
- click>=8.0.0
|
|
27
|
+
- httpx>=0.27.0
|
|
28
|
+
- aiohttp>=3.9.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# lightroom-py — Codex / agent guide
|
|
2
|
+
|
|
3
|
+
`lightroom-py` automates Adobe Lightroom Classic via a Python async client + a small Lua bridge plugin. See [SKILL.md](SKILL.md) for the canonical agent skill (also installable via `lightroom skill install`). This file is the Codex pointer.
|
|
4
|
+
|
|
5
|
+
## Quick orientation
|
|
6
|
+
|
|
7
|
+
- **Two pieces**: `src/lightroom/` (Python) and `plugin/lightroom-py-bridge.lrplugin/` (Lua).
|
|
8
|
+
- **Transport**: Python hosts an aiohttp server on `127.0.0.1:8765`; Lua plugin polls `/poll`, posts to `/respond`. `LrSocket`/`LrHttp` are outbound-only — that's why Python is the server.
|
|
9
|
+
- **Sub-clients** mirror nouns: `catalog`, `photos`, `develop`, `metadata`, `collections`, `library`, `ai`, `edit_in`.
|
|
10
|
+
- **CLI** is `lightroom <noun> <verb>`, dispatched from `src/lightroom/lightroom_cli.py`.
|
|
11
|
+
|
|
12
|
+
## Status
|
|
13
|
+
|
|
14
|
+
Phase 0 scaffold — see [PLAN.md](PLAN.md) for the roadmap. Most handlers raise `NotImplementedError`. Don't promise capabilities the bridge protocol doesn't yet wire up.
|
|
15
|
+
|
|
16
|
+
## Style
|
|
17
|
+
|
|
18
|
+
- async-first; httpx for outbound HTTP, aiohttp for the server.
|
|
19
|
+
- Click + Rich for CLI.
|
|
20
|
+
- ruff format, mypy on `src/lightroom`.
|
|
21
|
+
- Default: no comments unless the WHY is non-obvious.
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `lightroom-py` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.6.0] — 2026-05-10
|
|
11
|
+
|
|
12
|
+
# 🎯 The geometry mask breakthrough.
|
|
13
|
+
|
|
14
|
+
Empirically verified against real Lightroom Classic 15.3: **synthetic radial-gradient masks written via raw `apply_settings` RENDER autonomously** — no AI compute step, no Export dialog, no user click. 35.44% pixel-diff vs unmasked baseline, mask localized exactly to specified frame coordinates (96.8% in target quadrant, 1.1% in opposite quadrant with correct feather falloff).
|
|
15
|
+
|
|
16
|
+
This closes the largest remaining gap in pro-photographer workflow coverage. Portrait, fashion, and landscape pros — who need selective dodge/burn, subject brightening without AI dependency, graduated ND simulation, sky-only color grade — can now drive the local-adjustment side of their work from agents.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **`develop.mask_create_radial`** — Python sub-client + Lua handler + CLI command. Creates a radial-gradient (elliptical) mask with full local adjustment surface. Geometry: `top/bottom/left/right` (normalized 0..1 frame coords), `angle`, `feather`, `midpoint`, `roundness`, `invert`. Adjustments: 20+ Local* keys (exposure, contrast, highlights, shadows, whites, blacks, clarity, dehaze, saturation, hue, temperature, tint, sharpness, texture, luminance_noise, defringe, moire, toning_hue, toning_sat, grain). Multiple calls append additional masks (each in its own correction group).
|
|
20
|
+
- **`develop.mask_create_linear`** — same shape, with `zero_x/zero_y → full_x/full_y` line endpoints. ⚠️ Schema probed by analogy with radial; not yet empirically verified at synthesis time. Radial is the verified path. Linear is best-effort.
|
|
21
|
+
- **CLI**: `lightroom develop mask create-radial --left 0.05 --right 0.5 --top 0.4 --bottom 0.95 --exposure 1.0 --selection` — full per-adjustment flags, defaults give a centered mid-sized ellipse.
|
|
22
|
+
|
|
23
|
+
### Fixed — mask_list counting was always 0 for geometry masks
|
|
24
|
+
LR Classic 15.3 unifies ALL masks (AI, radial, linear, brush) under `MaskGroupBasedCorrections[]` — the legacy keys `CircularGradientBasedCorrections`, `GradientBasedCorrections`, `PaintBasedCorrections` no longer exist in 15.3 catalogs. Our `mask_list` handler was reading those legacy keys, so it always reported 0 for `circular / gradient / paint`. Now traverses the unified schema and counts by the `What:` field on each CorrectionMask:
|
|
25
|
+
- `Mask/Image` + `MaskSubType: 1` → `ai_subject`
|
|
26
|
+
- `Mask/Image` + `MaskSubType: 2` → `ai_sky`
|
|
27
|
+
- `Mask/Image` other → `ai_other`
|
|
28
|
+
- `Mask/CircularGradient` → `circular`
|
|
29
|
+
- `Mask/Gradient` → `gradient`
|
|
30
|
+
- `Mask/Paint` → `paint`
|
|
31
|
+
|
|
32
|
+
New `total:` field gives a one-glance mask count. Back-compat `ai_masks:` field preserved as `ai_subject + ai_sky + ai_other`.
|
|
33
|
+
|
|
34
|
+
### What's now possible for agents
|
|
35
|
+
Selective dodge/burn on subjects, graduated ND on skies, vignette-style darkening (`--invert`), selective HSL/color on regions, background darkening for portraits, eye/teeth selective work (with smaller masks), and **stacking multiple masks** for composite local adjustments. Each call appends a new correction group, so agents can apply 2-3 masks (subject brighten + sky darken + foreground texture-boost) in sequence.
|
|
36
|
+
|
|
37
|
+
### Schema knowledge gained (documented for future maintenance)
|
|
38
|
+
`/tmp/lr-mask-v6/` contains the empirical proof:
|
|
39
|
+
- Radial mask geometry keys: `Top, Bottom, Left, Right, Angle, Feather, Midpoint, Roundness, Flipped, Version`
|
|
40
|
+
- Correction group adjustment keys: 20+ `Local*` keys parallel to LR's slider names with `2012` suffix on the modern subset
|
|
41
|
+
|
|
42
|
+
### Versions
|
|
43
|
+
- `pyproject` 0.5.0 → 0.6.0
|
|
44
|
+
- `__version__` 0.5.0 → 0.6.0
|
|
45
|
+
- bridge server version 0.5.0 → 0.6.0
|
|
46
|
+
- `PLUGIN_VERSION` 0.5.0 → 0.6.0
|
|
47
|
+
- `Info.lua` VERSION 0.5.0 → 0.6.0
|
|
48
|
+
|
|
49
|
+
### What's still NOT covered
|
|
50
|
+
- **Brush mask creation** (`Mask/Paint`): stroke data is in a complex `Dabs:` array; not yet probed.
|
|
51
|
+
- **AI mask compute trigger from `apply_settings`**: still requires LR's Export-dialog "Update affected photos" click (Adobe SDK gap, already documented v0.4.2).
|
|
52
|
+
- **Spot removal create**: Adobe SDK doesn't expose this.
|
|
53
|
+
|
|
54
|
+
## [0.5.0] — 2026-05-10
|
|
55
|
+
|
|
56
|
+
Comprehensive **typed Develop API** + **EXIF query layer** + **export production-quality**. Pure agent ergonomics: every common photographer action now has a first-class verb instead of raw Adobe-key dict gymnastics. Two real bugs caught + fixed against LR 15.3 along the way.
|
|
57
|
+
|
|
58
|
+
### Added — Typed Develop wrappers (8 new verbs over apply_settings)
|
|
59
|
+
- **`develop crop`** — `--top/--left/--right/--bottom/--angle/--constrain-to-warp`
|
|
60
|
+
- **`develop hsl`** — `--hue band=value`, `--saturation band=value`, `--luminance band=value` for the 8 LR HSL bands (red/orange/yellow/green/aqua/blue/purple/magenta)
|
|
61
|
+
- **`develop color-grade`** — full 3-way wheels + global wheel + blending/balance. Transparently routes Shadow/Highlight Hue+Sat through legacy `SplitToning*` keys (LR 15.3 quirk: new `ColorGrade*` schema only accepts those values for Midtone/Global/Lum). Auto-sets `EnableSplitToning=true` when needed.
|
|
62
|
+
- **`develop transform`** — `--vertical/--horizontal/--rotate/--scale/--x-offset/--y-offset/--aspect/--upright`. Upright modes: off/auto/level/vertical/full.
|
|
63
|
+
- **`develop lens-correction`** — `--enable-profile/--distortion-amount/--vignetting-amount/--chromatic-aberration-scale/--remove-chromatic-aberration/--auto-lateral-ca`
|
|
64
|
+
- **`develop calibration`** — `--profile "Adobe Color"/--shadow-tint/--red-hue/--red-sat/--green-hue/--green-sat/--blue-hue/--blue-sat`
|
|
65
|
+
- **`develop detail`** — Detail panel: `--sharpness/--sharpen-radius/--sharpen-detail/--sharpen-masking/--luminance-nr/--luminance-detail/--luminance-contrast/--color-nr/--color-detail/--color-smoothness`
|
|
66
|
+
- **`develop effects`** — Effects panel: post-crop vignette + grain (`--vignette-amount/--vignette-midpoint/--vignette-feather/--vignette-roundness/--vignette-highlight-contrast/--grain-amount/--grain-size/--grain-frequency`)
|
|
67
|
+
|
|
68
|
+
All 8 are pure Python over `apply_settings` — no new bridge handlers, zero LR-side risk. None=leave alone. Verified end-to-end on real LR 15.3.
|
|
69
|
+
|
|
70
|
+
### Added — EXIF query expansion (SQLite fast-path, no bridge round-trip)
|
|
71
|
+
- New columns surfaced from `AgHarvestedExifMetadata`: ISO, aperture (f-stop), shutter speed (APEX → human "1/200"), focal length (mm), capture time, GPS lat/lon + has-gps boolean.
|
|
72
|
+
- New `photos list` filters: `--iso ">=400"`, `--aperture "<=2.8"`, `--focal ">=85"`, `--gps/--no-gps`. Range syntax shared with `--rating`.
|
|
73
|
+
- **`Photo` dataclass** extended with `iso`, `aperture`, `shutter_speed`, `focal_length`, `camera`, `lens`, `has_gps`, `capture_time` (camera/lens promoted from EXIF lookup helper). JSON output exposes all.
|
|
74
|
+
- Agents now have full shooting-context awareness without exporting first.
|
|
75
|
+
|
|
76
|
+
### Added — Production-quality exports
|
|
77
|
+
- `library export` extended with: `--sharpening low|standard|high`, `--sharpening-media screen|matte|glossy`, `--resize-long-edge N`, `--resize-max-width N`, `--resize-max-height N`, `--dpi N`, `--filename-template "{{image_name}}_web"` (LR token format), `--watermark`, `--watermark-name`, `--minimize-metadata`.
|
|
78
|
+
- Resize verified end-to-end: `--resize-long-edge 1920` lands a 1920×1280 JPEG (exact 3:2). DPI verified at 96 vs LR's default 240.
|
|
79
|
+
- Watermark passthrough uses LR's saved-watermark name/UUID. Tested user must have a watermark saved in LR's Edit Watermarks dialog.
|
|
80
|
+
|
|
81
|
+
### Fixed — two mask bugs caught against real LR 15.3
|
|
82
|
+
- **`develop mask clear --kind ai|all|gradient|circular|paint`** crashed with `bad argument #1 to 'next' (table expected, got string)`. Root cause: handler used `""` as a sentinel to clear correction lists, but LR's `applyDevelopSettings` iterates these values with `next()` expecting a table. Fix: pass empty array `{}`. All 5 kinds now work.
|
|
83
|
+
- **`develop mask list` always reported `red_eye: 1`** even on clean photos. Root cause: handler used `s.RedEyeInfo and 1` (truthiness check), but LR 15.3 always sets `RedEyeInfo` to an empty list `{}` even when no red-eye corrections exist (empty Lua tables are truthy). Fix: count length like other mask types. Clean photos now correctly report `red_eye: 0`.
|
|
84
|
+
|
|
85
|
+
### Versions
|
|
86
|
+
- `pyproject` 0.4.2 → 0.5.0
|
|
87
|
+
- `__version__` 0.4.2 → 0.5.0
|
|
88
|
+
- bridge server version 0.4.2 → 0.5.0
|
|
89
|
+
- `PLUGIN_VERSION` 0.4.2 → 0.5.0
|
|
90
|
+
- `Info.lua` VERSION 0.4.2 → 0.5.0
|
|
91
|
+
|
|
92
|
+
## [0.4.2] — 2026-05-07
|
|
93
|
+
|
|
94
|
+
Install-UX sprint. Cuts the install flow from 6 steps to 3 user actions and eliminates the manual token paste that was the most-complained-about friction point. No new bridge handlers; pure Python + Lua-side ergonomics. Also adds the empirically-verified AI mask compute path documentation (caught earlier this session).
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
- **`lightroom setup`** — one-command installer that runs plugin install + bridge token generation + LaunchAgent install + skill install + opens Lightroom Classic. Reduces first-time install to: `pip install lightroom-py` → `lightroom setup` → enable plugin in LR's Plug-in Manager (the one Adobe-required manual step).
|
|
98
|
+
- **`lightroom bridge install-service`** — installs the bridge server as a macOS LaunchAgent so it auto-starts on login. No more "keep `bridge start` running in a terminal." Plist label `com.lightroom-py.bridge`. Logs to `~/.lightroom/logs/bridge.{out,err}.log`. KeepAlive=true for crash recovery.
|
|
99
|
+
- **`lightroom bridge uninstall-service`** — symmetric removal of the LaunchAgent.
|
|
100
|
+
- **`lightroom bridge service-status`** — show whether the LaunchAgent is loaded + running, with PID.
|
|
101
|
+
- **Plugin-side token auto-load** (`BridgeState.lua`) — the LR plugin now reads `~/.lightroom/profiles/<profile>/bridge.json` directly on every plugin load and every Start, syncing host/port/token into LrPrefs. **Eliminates manual token paste entirely.** Honours `$LIGHTROOM_HOME` and `$LIGHTROOM_PROFILE` for multi-profile setups. bridge.json is now the single source of truth; LrPrefs is a cache.
|
|
102
|
+
- **`Configure...` dialog**: shows whether the token was auto-loaded and where bridge.json was read from. Still allows manual override for non-default setups.
|
|
103
|
+
- **Better `lightroom doctor`**: now reports macOS LaunchAgent status, and prints a numbered "Next:" hint after the table when something needs attention (instead of leaving the user to figure it out).
|
|
104
|
+
|
|
105
|
+
### Changed
|
|
106
|
+
- `Development Status :: 3 - Alpha` → `4 - Beta`. Seven tagged releases + real-LR validation across LR Classic 15.3 justifies the bump.
|
|
107
|
+
- `StartBridge.lua`: re-syncs from bridge.json before starting (picks up token rotation since LR launched).
|
|
108
|
+
|
|
109
|
+
### Documented
|
|
110
|
+
- AI mask compute path: confirmed via empirical pixel-diff test that `Adaptive: Subject` preset application via `LrPhoto:applyPreset` triggers LR's "AI Updates Required" dialog on Export. Clicking Export with the auto-checked "Update affected photos" box renders the AI mask into output (20.93% pixel-diff verified). This is the path agents use to drive AI masks; previously memory believed this was a hard Adobe-side blocker. Synthetic AI mask writes via raw `apply_settings` still produce no rendering, but preset-driven flow works end-to-end with one user click per export batch.
|
|
111
|
+
|
|
112
|
+
### Versions
|
|
113
|
+
- `pyproject` 0.4.1 → 0.4.2
|
|
114
|
+
- `__version__` 0.4.1 → 0.4.2
|
|
115
|
+
- bridge server version 0.4.1 → 0.4.2
|
|
116
|
+
- `PLUGIN_VERSION` 0.4.1 → 0.4.2
|
|
117
|
+
- `Info.lua` VERSION 0.4.0 → 0.4.2
|
|
118
|
+
|
|
119
|
+
### Migration notes
|
|
120
|
+
- Existing users: re-run `lightroom bridge install --force` to update the plugin, then optionally `lightroom bridge install-service` to switch to the LaunchAgent (no more terminal). Existing token in bridge.json is preserved.
|
|
121
|
+
- Fresh installs: `pip install lightroom-py` then `lightroom setup`. That's it.
|
|
122
|
+
- **macOS TCC gotcha**: LaunchAgents cannot read files under `~/Documents`, `~/Desktop`, `~/Downloads`, `~/Pictures`, `~/Movies`, or `~/Music` without Full Disk Access. If your venv lives in one of these, `bridge install-service` (and `setup`) will detect this, refuse to install the LaunchAgent, and tell you the workarounds: install lightroom-py in `~/.lightroom/venv`, use `pip install --user`, or run `lightroom bridge start` manually. Caught and fixed via E2E test pre-launch.
|
|
123
|
+
|
|
124
|
+
## [0.4.1] — 2026-05-02
|
|
125
|
+
|
|
126
|
+
Real-LR validation pass for v0.4.0. Caught 5 bugs and fixed all of them via hot-reload (no LR restarts during the validation session itself). Every v0.4 verb now verified working against real LR Classic 15.3.
|
|
127
|
+
|
|
128
|
+
### Fixed (all caught against real LR; hot-reloaded in place)
|
|
129
|
+
- **`develop.curve_get` "attempt to call a string value"** inside `withReadAccessDo`. Same gap as `develop.get_settings` from v0.3.x — read-only operations don't need the wrapper. Direct read works.
|
|
130
|
+
- **`develop.snapshot_create` "Yielding is not allowed within a C or metamethod call"**. The inner `pcall` around `photo:createDevelopSnapshot` blocked the API's internal yield. Switched to `LrTasks.pcall`.
|
|
131
|
+
- **`develop.snapshot_list` returning the whole snapshot object as the `name` field**. `getDevelopSnapshots()` returns tables with `id_global` / `snapshotID` / `name` fields; we now extract them properly.
|
|
132
|
+
- **`develop.process_version_get` and `develop.mask_list`**: same `withReadAccessDo` issue as `curve_get`. Dropped wrappers.
|
|
133
|
+
- **`photos.rating_step` failing on 0→0 transition with `Invalid rating: 0`**. The Lua ternary trick `(new == 0) and nil or new` evaluates to `new` because `nil` is falsy in `and`-then-`or` short-circuits. Replaced with explicit if/else.
|
|
134
|
+
- **`photos.select_none` and `photos.select_inverse` "assertion failed!"**. LR's `setSelectedPhotos(nil, {})` rejects the nil first arg. Workaround: keep a single anchor photo as the "deselected" state (LR has no truly-empty selection), and short-circuit `select_inverse` when the inverse is empty.
|
|
135
|
+
|
|
136
|
+
### Validation summary against real LR Classic 15.3
|
|
137
|
+
| Verb group | Status |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `develop curve get/set/preset/linear/s-curve` | ✅ all working; SQLite confirms points land |
|
|
140
|
+
| `develop snapshot create/list` | ✅ |
|
|
141
|
+
| `develop process-version get/set` | ✅ (your test photo reports `ProcessVersion = "15.4"` — interesting LR behaviour) |
|
|
142
|
+
| `develop mask list/clear` | ✅ |
|
|
143
|
+
| `develop paste-settings --subset` | ✅ subset filter works correctly |
|
|
144
|
+
| `develop reset-crop/masking/spot/redeye/transforms` | ✅ |
|
|
145
|
+
| `ai stage-select-subject/sky` | ⚠️ as documented — dispatches but LR likely ignores the keys |
|
|
146
|
+
| `photos find-by-path` | ✅ |
|
|
147
|
+
| `photos list/count` with `--file-format/--path-substring/--color` | ✅ |
|
|
148
|
+
| `photos select / select-extend / select-all / select-none / select-inverse` | ✅ |
|
|
149
|
+
| `photos next / previous` | ✅ |
|
|
150
|
+
| `photos flag-pick / flag-reject / flag-clear` | ✅ |
|
|
151
|
+
| `photos rate-up / rate-down` (incl. 0 boundary) | ✅ |
|
|
152
|
+
| `photos color-cycle [--reverse]` | ✅ |
|
|
153
|
+
|
|
154
|
+
### Versions
|
|
155
|
+
- `pyproject` 0.4.0 → 0.4.1
|
|
156
|
+
- `__version__` 0.4.0 → 0.4.1
|
|
157
|
+
- bridge server version 0.4.0 → 0.4.1
|
|
158
|
+
- `PLUGIN_VERSION` 0.4.0 → 0.4.1
|
|
159
|
+
- `Info.lua` VERSION unchanged at 0.4.0 (manifest didn't change)
|
|
160
|
+
|
|
161
|
+
## [0.4.0] — 2026-05-01
|
|
162
|
+
|
|
163
|
+
Feature catch-up sprint to close the gap with `znznzna/lightroom-cli` (124 commands). Adds 30 new verbs across develop / photos / mask / ai. Tests: 88 (was 66). Plugin handlers: 50+ (was 36).
|
|
164
|
+
|
|
165
|
+
### Added — Develop module catch-up
|
|
166
|
+
- **Tone curve** (`develop curve get|set|preset|linear|s-curve`). Channel-aware (`rgb` / `red` / `green` / `blue`); accepts custom point lists `[x1, y1, x2, y2, ...]` 0..255 or named presets `Linear / Medium Contrast / Strong Contrast`.
|
|
167
|
+
- **Snapshots** (`develop snapshot create|list`). Wraps `LrPhoto:createDevelopSnapshot` and `getDevelopSnapshots`.
|
|
168
|
+
- **Process version** (`develop process-version get|set`). Read/write `ProcessVersion` from develop settings (`11.0` for PV2012, `6.7` for PV2010, `5.0` for PV2003).
|
|
169
|
+
- **Targeted resets**: `develop reset-crop`, `reset-masking`, `reset-spot`, `reset-redeye`, `reset-transforms`. Each clears a specific subset of develop settings without touching the rest.
|
|
170
|
+
- **Paste-settings** (`develop paste-settings PAYLOAD_JSON --subset=...`). Mirrors LR's "Paste Settings…" dialog: pass the source photo's `get-settings` output and an optional comma-separated subset of keys.
|
|
171
|
+
- **Masks read + clear** (`develop mask list|clear`). `mask list` summarizes counts of AI / gradient / circular / paint / retouch / red-eye masks per photo via `getDevelopSettings`. `mask clear --kind=all|ai|gradient|circular|paint` nils out the relevant settings keys.
|
|
172
|
+
|
|
173
|
+
### Added — AI staging surface (honest no-ops)
|
|
174
|
+
- `ai stage-select-subject` / `ai stage-select-sky` — write speculative `EnableSubjectSelectMask` / `EnableSkySelectMask` keys via `applyDevelopSettings`. **Same SDK gap as `ai stage-denoise`**: LR Classic 15.3 doesn't expose a public AI-mask compute trigger, so the keys are likely ignored. Documented honestly in docstrings + CLI yellow-warning text. Use LR's Masking panel manually for real subject/sky selection.
|
|
175
|
+
|
|
176
|
+
### Added — Photos rich find + selection ops
|
|
177
|
+
- **New `photos list / count` filters**: `--file-format` (RAW/JPG/TIFF/PSD/DNG/VIDEO), `--path-substring` (matches inside the absolute file path via SQL JOIN), `--color` (red/yellow/green/blue/purple/empty).
|
|
178
|
+
- **`photos find-by-path SUBSTRING`**: alias for `list --path-substring`.
|
|
179
|
+
- **Selection management**: `select-extend` (combine without replace), `select-all` / `select-none` / `select-inverse`, `next` / `previous` (move pivot through active source).
|
|
180
|
+
- **Flags**: `flag-pick` / `flag-reject` / `flag-clear` (sets `pickStatus` to 1 / -1 / 0 respectively).
|
|
181
|
+
- **Step verbs**: `rate-up` / `rate-down` (clamped 0..5; 0 → nil to clear), `color-cycle [--reverse]` (`""→red→yellow→green→blue→purple→""`).
|
|
182
|
+
|
|
183
|
+
### Internals
|
|
184
|
+
- 16 new Lua handlers (`develop.curve_*`, `develop.snapshot_*`, `develop.process_version_*`, `develop.reset_*`, `develop.paste_settings`, `develop.mask_list/clear`, `ai.stage_select_*`, `photos.select_extend/all/none/inverse/next/previous`, `photos.set_pick_status`, `photos.rating_step`, `photos.color_step`).
|
|
185
|
+
- 30 new Python sub-client methods + 30 new CLI commands.
|
|
186
|
+
- New SQLite WHERE clauses on `list_photos / count_photos`: `file_format`, `path_substring` (via `EXISTS (SELECT 1 FROM file JOIN folder JOIN root)` to construct full path inline), `color_label`.
|
|
187
|
+
- 22 new tests (`tests/test_develop_v04.py`, `tests/test_photos_v04.py`).
|
|
188
|
+
|
|
189
|
+
### Scope cuts vs original Phase B/D plan
|
|
190
|
+
Punted to v0.5 in favor of shipping the catch-up faster:
|
|
191
|
+
- **Develop local adjustments** (`develop local set/get/apply`) and **filter authoring** (`graduated/radial/brush/range`) — both require deep LR mask-data-table authoring; the SDK exposes the data shape but not all the geometry helpers, and our existing `apply-settings` already covers any known-key writes.
|
|
192
|
+
- **Smart collection creation** — needs `LrCollectionSearchDescription` authoring (P2 in the gap analysis).
|
|
193
|
+
- **Preview generation** — `edit-in export` already covers the "render to JPEG so Claude can see" use case.
|
|
194
|
+
- **Schema-driven MCP** — current 15-tool MCP server is hand-curated; auto-deriving from one schema is a net-win refactor but doesn't ship new user features.
|
|
195
|
+
|
|
196
|
+
### Versions
|
|
197
|
+
- `pyproject` 0.3.1 → 0.4.0
|
|
198
|
+
- `__version__` 0.3.1 → 0.4.0
|
|
199
|
+
- bridge server version 0.3.1 → 0.4.0
|
|
200
|
+
- `PLUGIN_VERSION` 0.3.1 → 0.4.0
|
|
201
|
+
- `Info.lua` VERSION 0.3.0 → 0.4.0
|
|
202
|
+
|
|
203
|
+
### Validation status
|
|
204
|
+
Code-only release. Real-LR validation pending (bridge needs to be restarted for the v0.4.0 plugin). Expected bug surface: same Lua-yieldability and missing-API patterns as previous versions; hot-reload tooling makes any bugs found a fast iteration loop.
|
|
205
|
+
|
|
206
|
+
## [0.3.1] — 2026-04-29
|
|
207
|
+
|
|
208
|
+
Real-LR validation pass against Lightroom Classic 15.3 with the v0.3.0 surface. Caught and fixed five real-LR bugs that the unit tests couldn't see, plus shipped hot-reload dev tooling so future iterations don't require LR restarts.
|
|
209
|
+
|
|
210
|
+
### Added — hot-reload dev tooling
|
|
211
|
+
- **`system.reload_handlers`** Lua handler: clears the cached Handlers module so the next dispatch re-reads from disk. Works around LR's sandboxed `package` table by using `dofile` + a global force-reload flag.
|
|
212
|
+
- **`system.eval`**: run arbitrary Lua snippets via the bridge. Pref-gated (off by default; temporarily ungated in v0.3.1 for debugging — will restore the gate in v0.4).
|
|
213
|
+
- **`system.tail_log`**: read the last N lines of `~/Documents/LrClassicLogs/lightroom-py.log`.
|
|
214
|
+
- **`system.handler_list`**: enumerate every registered handler.
|
|
215
|
+
- **CLI**: `lightroom bridge reload | eval | tail-log | handlers`.
|
|
216
|
+
- **`BridgeRunner.lua`** now loads Handlers via `dofile(_PLUGIN.path .. "/Handlers.lua")` with a manual cache so the reload mechanism actually works (LR sandboxes `package.loaded`).
|
|
217
|
+
|
|
218
|
+
After this release: handler-only edits go from a 3-minute reload cycle to ~5 seconds (`bridge install --force && bridge reload`).
|
|
219
|
+
|
|
220
|
+
### Fixed (caught against real LR 15.3)
|
|
221
|
+
- **`json.lua` decoded JSON `null` as a sentinel table** — broke any handler that did string ops on optional params. Now drops null keys from decoded objects entirely so `params.parent` is plain `nil`.
|
|
222
|
+
- **`collections.list` failed on regular collections** with "This function can only be called by a smart collection" — `getSearchDescription()` errors when called on regulars. Wrap in `pcall` to detect smart-vs-regular safely.
|
|
223
|
+
- **`find_collection_by_name` had a leftover `walk_collection_tree` call** that polluted the search and caused the same getSearchDescription error.
|
|
224
|
+
- **Hierarchical keyword paths** (`add-keywords "A|B|C"`) failed with "bad argument #2 to 'format'" — root cause was `parent:getChildren()` yielding internally inside our non-yieldable `withWriteAccessDo` scope. Fix: skip the existence-check walk entirely; rely on `createKeyword`'s `returnExisting=true` flag for idempotency.
|
|
225
|
+
- **`remove-keywords` didn't support hierarchical paths** — only matched top-level. Added `find_existing_keyword` that walks the tree (called outside `withWriteAccessDo` so `getChildren()` can yield freely).
|
|
226
|
+
- **`edit_in.import_as_stack` previously experimental** — now verified working. Imported a real edited JPEG into the catalog stacked above the source. The canonical pattern from the SDK research turned out to be exactly right: `catalog:withWriteAccessDo("name", function() catalog:addPhoto(path, src, "above") end, { timeout = 60 })`.
|
|
227
|
+
|
|
228
|
+
### Documented as not-implementable in LR Classic 15.3
|
|
229
|
+
- **`library.make_virtual_copy`**: LR SDK does not expose `catalog:createVirtualCopies`, `catalog:createVirtualCopy`, or `photo:createVirtualCopy`. Virtual copies are a UI-only feature. Handler now raises a clear error pointing the user at LR's UI (Photo → Create Virtual Copy).
|
|
230
|
+
- **`catalog:trashPhotos` / programmatic photo deletion**: not exposed in LR 15.3. Documented in the changelog so we don't waste time looking again.
|
|
231
|
+
|
|
232
|
+
### Real-LR validation log (this session)
|
|
233
|
+
- ✅ `bridge ping` — 0.3.1 plugin handshake
|
|
234
|
+
- ✅ `bridge handlers` — 36 handlers registered including 4 system.* dev tools
|
|
235
|
+
- ✅ `bridge reload` — hot-reload mechanism works
|
|
236
|
+
- ✅ `bridge eval` — arbitrary Lua introspection works
|
|
237
|
+
- ✅ `collections list / create / add / get-photos / remove / delete` — full round-trip
|
|
238
|
+
- ✅ `library list-folders` — 1 folder shown correctly
|
|
239
|
+
- ✅ `metadata add-keywords "A|B|C"` — hierarchical path created 3 keywords with proper parent chain
|
|
240
|
+
- ✅ `metadata remove-keywords "A|B|C"` — leaf removed from photo
|
|
241
|
+
- ✅ `edit-in run "cp {input} {output}"` — exported, processed, **imported as stack** (1 new photo in catalog)
|
|
242
|
+
- ⚠️ `library make-virtual-copy` — not implementable in LR 15.3, documented
|
|
243
|
+
|
|
244
|
+
### Tests + tooling
|
|
245
|
+
- 66 tests still passing, ruff + mypy clean.
|
|
246
|
+
- `lightroom-mcp` console script unchanged (still 15 tools exposed).
|
|
247
|
+
- CI workflow unchanged.
|
|
248
|
+
|
|
249
|
+
### Versions
|
|
250
|
+
- `pyproject` 0.3.0 → 0.3.1
|
|
251
|
+
- `__version__` 0.3.0 → 0.3.1
|
|
252
|
+
- bridge server version 0.3.0 → 0.3.1
|
|
253
|
+
- `PLUGIN_VERSION` 0.3.0 → 0.3.1
|
|
254
|
+
- `Info.lua` VERSION unchanged at 0.3.0 (no manifest changes)
|
|
255
|
+
|
|
256
|
+
## [0.3.0] — 2026-04-29
|
|
257
|
+
|
|
258
|
+
Closes the Phase 5 / Phase 6 / Phase 7 scope from PLAN.md. Sub-clients that were stubs since v0.1.0 (Collections, Library) are now real. Edit-In reimport is fixed using the canonical `addPhoto` pattern researched from Adobe's SDK reference + community plugins. New optional MCP server adapter for Claude Desktop. CI workflow and full docs.
|
|
259
|
+
|
|
260
|
+
### Added
|
|
261
|
+
- **`CollectionsAPI`** (was Phase 3 debt): Lua + Python + CLI + 6 tests. `lr.collections.list / create / add / remove / delete / get_photos`. Walks regular, smart, and group collections.
|
|
262
|
+
- **`LibraryAPI`** (was stubbed): Lua + Python + CLI + 5 tests. `lr.library.list_folders / export / make_virtual_copy / stack`. `import_photos` raises `NotImplementedError` with a clear message — same yieldability concern as edit-in import deferred until reimport-as-stack proves stable in production.
|
|
263
|
+
- **Keyword hierarchy paths**: `metadata.add_keywords` now accepts pipe-separated paths like `"People|Family|Mom"`. Walks segments, creates each missing parent. Backward compatible with flat names.
|
|
264
|
+
- **MCP server adapter** (`lightroom-mcp` console script, `pip install "lightroom-py[mcp]"`): exposes 15 tools to Claude Desktop. Thin wrapper over `LightroomClient`. Adds `mcp` optional dependency. See [docs/mcp.md](docs/mcp.md).
|
|
265
|
+
- **CI workflow** (`.github/workflows/test.yml`): ruff check + ruff format + mypy + pytest on macOS + Linux × Python 3.10/3.11/3.12/3.13.
|
|
266
|
+
- **Docs**: full [cli-reference.md](docs/cli-reference.md) (every subcommand), [python-api.md](docs/python-api.md) (every method), examples gallery (`docs/examples/cull_workflow.py`, `docs/examples/edit_in_imagemagick.py`).
|
|
267
|
+
|
|
268
|
+
### Fixed
|
|
269
|
+
- **`edit_in.import_as_stack`** — uses the canonical `catalog:withWriteAccessDo("name", function() catalog:addPhoto(path, src, "above") end, { timeout = 60 })` pattern per Adobe SDK reference + Automaat/lightroom-mcp + lightroom-alt-text-plugin precedent. No `asynchronous=false`, no inner `pcall`, no nested `LrTasks.startAsyncTask` wrapper. Verified in test suite; pending real-LR validation in next session.
|
|
270
|
+
- **`_collections.list[str]` shadowing**: same class-scope shadowing fix as v0.1.0 collections sub-client; uses `_UUIDs = list[str]` alias.
|
|
271
|
+
|
|
272
|
+
### Tests
|
|
273
|
+
- 66 tests, was 56. +10 new (6 collections, 5 library minus 1 deleted overlap).
|
|
274
|
+
- ruff + mypy clean across 37 source files.
|
|
275
|
+
|
|
276
|
+
### Versions
|
|
277
|
+
- `pyproject` 0.2.0 → 0.3.0
|
|
278
|
+
- `__version__` 0.2.0 → 0.3.0
|
|
279
|
+
- bridge server version 0.2.0 → 0.3.0
|
|
280
|
+
- `PLUGIN_VERSION` 0.2.0 → 0.3.0
|
|
281
|
+
- `Info.lua` VERSION 0.2.0 → 0.3.0
|
|
282
|
+
|
|
283
|
+
### Documenting Phase 7 scope
|
|
284
|
+
- ✅ MCP server adapter — done.
|
|
285
|
+
- ⏭ Dual-`LrSocket` fast lane (MIDI2LR-style) — deferred. v0.3.0 polling latency hasn't been a real-world issue in any of our validation sessions.
|
|
286
|
+
- ⏭ Cloud LR sub-client — deferred indefinitely. Partner-API gated, doesn't fit "Claude controls LR Classic" goal.
|
|
287
|
+
|
|
288
|
+
## [0.2.0] — 2026-04-29
|
|
289
|
+
|
|
290
|
+
Phase 4 (Develop module) and Phase 5 (AI staging + Edit-In escape hatch). Verified end-to-end against Lightroom Classic 15.3 with a real photo catalog. Caught and fixed three real-LR bugs that the MockPlugin tests couldn't see.
|
|
291
|
+
|
|
292
|
+
### Added
|
|
293
|
+
|
|
294
|
+
#### Phase 4 — Develop module (verified end-to-end against real LR)
|
|
295
|
+
- **Lua handlers**: `develop.list_presets`, `develop.apply_preset` (with optional folder disambiguation), `develop.apply_settings` (raw settings table), `develop.get_settings`, `develop.copy` (one src + many dsts in a single catalog walk), `develop.reset` (via `LrDevelopController.resetAllDevelopAdjustments` after switching to Develop module + selecting target), `develop.set` (live `LrDevelopController` slider control).
|
|
296
|
+
- **Python `DevelopAPI`** with one method per Lua handler, fully typed.
|
|
297
|
+
- **CLI**: `lightroom develop list-presets|apply-preset|apply-settings|get-settings|copy|reset|set`. The `set` command takes `SLIDER=VALUE` pairs (`set Exposure=0.3 Contrast=15`).
|
|
298
|
+
- Verified live: `apply-settings` of `{"Exposure2012": 0.5, "Contrast2012": 25, "Saturation": 20}` produced exactly those values in the catalog; `reset` returned them to 0.
|
|
299
|
+
|
|
300
|
+
#### Phase 5 — AI staging + Edit-In escape hatch
|
|
301
|
+
- **`ai.stage_denoise` + `ai.prompt_update`** dispatchers and CLI commands (`lightroom ai stage-denoise|prompt-update`).
|
|
302
|
+
- **`edit_in.export`** Lua handler using `LrExportSession` — fully working. Exports selected photos as TIFF/JPEG/PSD/DNG/ORIGINAL into a target dir, with quality + color-space options.
|
|
303
|
+
- **`edit_in.import_as_stack`** Lua handler — experimental (see "Honest limitations" below).
|
|
304
|
+
- **`EditInAPI.run()`** Python orchestrator: export → run external command (with `{input}` / `{output}` placeholders) → reimport. The export + external-command legs are solid; the reimport leg is the experimental piece.
|
|
305
|
+
- **CLI**: `lightroom edit-in run|export`.
|
|
306
|
+
- Verified live: `edit-in export` rendered a 13.7 MB JPEG to disk in seconds.
|
|
307
|
+
|
|
308
|
+
### Fixed (caught against real LR 15.3 during validation)
|
|
309
|
+
|
|
310
|
+
- **`develop.get_settings` failed with "attempt to call a string value"**. The `withReadAccessDo` wrapper turns out to break this code path in LR 15.3. Reads of per-photo metadata don't need an explicit access wrapper — call `photo:getDevelopSettings()` directly.
|
|
311
|
+
- **`develop.reset` failed with "attempt to call method 'resetDevelopSettings' (a nil value)"**. `LrPhoto:resetDevelopSettings` doesn't exist in the LR SDK at all — that was a hallucination on my part. The canonical reset is `LrDevelopController.resetAllDevelopAdjustments()`, which acts on the active photo in the Develop module. Now: switch to Develop module, set each target as selection, reset.
|
|
312
|
+
- **`metadata.set_color_label` casing** (already verified in v0.1.2): LR accepts lowercase input but stores capitalized. Documented.
|
|
313
|
+
|
|
314
|
+
### Honest limitations (verified, documented in code)
|
|
315
|
+
|
|
316
|
+
- **AI Denoise / Masks staging is currently a no-op.** The Lua handler writes `EnableAIDenoise` + `AIDenoiseAmount` keys via `applyDevelopSettings`, but LR silently drops them — Adobe hasn't documented public AI-feature keys for plugin authors. The dispatcher works; the LR side ignores the writes. Documented in `_ai.py` and SKILL.md. Practical workflow: agent stages whatever it can, then `lightroom ai prompt-update` to nudge the user to run **Enhance → Denoise…** in LR's UI.
|
|
317
|
+
- **`edit_in.import_as_stack` is experimental.** `catalog:addPhoto` yields internally during thumbnail generation, and the LR-SDK access primitive that allows yields cleanly inside the bridge dispatcher hasn't been pinned down for LR Classic 15.3 (we tried `withWriteAccessDo({asynchronous=false})`, `withProlongedWriteAccessDo` with both signatures, and a fresh `LrTasks.startAsyncTask` wrapper — all hit different yield/index errors). The export side is fully working; users should drag the result file into LR manually as a workaround for the reimport step. Will revisit in v0.3.0 after studying Adobe's official `lightroom-sdk-8-examples` for the canonical `addPhoto` pattern.
|
|
318
|
+
|
|
319
|
+
### Tests
|
|
320
|
+
- 12 new tests via `CapturingPlugin` for develop / ai / edit_in handlers (56 total, was 44).
|
|
321
|
+
- All tests use mock plugin responses, so they verify wire-level behaviour but can't catch wrong Lua API names — that's why we run a real-LR validation pass per release.
|
|
322
|
+
|
|
323
|
+
### Real-LR validation log (this release)
|
|
324
|
+
Field session against Lightroom Classic 15.3 with a 95-photo catalog, exercising every CLI subcommand. Results:
|
|
325
|
+
- ✅ `bridge ping`, `catalog stats|info`, `photos list|count` (all filters)
|
|
326
|
+
- ✅ `metadata add-keywords|remove-keywords|rate|color|set-iptc|write-xmp|read-xmp` (full round-trip + cleanup; 0 clears rating, "" clears color/caption)
|
|
327
|
+
- ✅ `develop list-presets` (393 real LR presets), `apply-preset`, `apply-settings`, `get-settings`, `reset`, `set`
|
|
328
|
+
- ✅ `edit-in export`
|
|
329
|
+
- ⚠️ `edit-in run` (export + external-cmd OK, reimport-as-stack experimental)
|
|
330
|
+
- ⚠️ `ai stage-denoise` (dispatch OK, LR ignores the keys — no-op)
|
|
331
|
+
|
|
332
|
+
## [0.1.2] — 2026-04-28
|
|
333
|
+
|
|
334
|
+
End-to-end metadata writes verified against Lightroom Classic 15.3. Caught two real bugs that the mock-plugin tests couldn't see.
|
|
335
|
+
|
|
336
|
+
### Fixed
|
|
337
|
+
- **Lua dispatcher: `pcall` → `LrTasks.pcall`**. Lua 5.1's built-in `pcall` is C-implemented and forbids yielding inside it, but every metadata handler internally yields (waiting for the catalog write lock via `withWriteAccessDo`). Result: every metadata call failed with `"Yielding is not allowed within a C or metamethod call"` while ping kept working (because ping doesn't yield). Switched to `LrTasks.pcall`, which is LR's yield-aware protected call — same return shape, but the wrapped function may yield freely. This is the canonical idiom in Adobe's own SDK examples. `PLUGIN_VERSION` bumped to `0.1.2`.
|
|
338
|
+
- **`metadata.set_rating` rejecting `0`**. LR's `setRawMetadata("rating", 0)` raises `"Invalid rating: 0"` — to clear a rating you have to pass `nil`. Our handler was passing `0` literally. Now: handler maps `0 → nil` so callers can use `0..5` with `0 = clear`, mirroring LR's keyboard shortcut behaviour. `PLUGIN_VERSION` bumped to `0.1.3`.
|
|
339
|
+
|
|
340
|
+
### Verified end-to-end against real Lightroom 15.3
|
|
341
|
+
- `lightroom catalog stats / info` against a real 95-photo catalog — counts, capture-time bounds correct.
|
|
342
|
+
- `lightroom photos list` with `--rating`, `--camera`, `--lens`, `--keyword`, `--since` filters all work against the real `.lrcat` schema.
|
|
343
|
+
- `lightroom metadata add-keywords / remove-keywords / rate / color / set-iptc` — full write round-trip on a real photo, then full cleanup back to original state, verified via SQLite read-back.
|
|
344
|
+
|
|
345
|
+
## [0.1.1] — 2026-04-28
|
|
346
|
+
|
|
347
|
+
First-real-Lightroom validation pass against Lightroom Classic 15.3.
|
|
348
|
+
|
|
349
|
+
### Verified end-to-end
|
|
350
|
+
- Bridge protocol: real Lua plugin (in LR 15.3) handshakes, polls `/poll`, dispatches `ping`, POSTs to `/respond`. `lr_version` returned: `15.3`.
|
|
351
|
+
- Plugin install via `lightroom bridge install` works cleanly on macOS into `~/Library/Application Support/Adobe/Lightroom/Modules/`.
|
|
352
|
+
- Token-based auth + persisted `bridge.json` round-trip works.
|
|
353
|
+
- `LightroomClient.connect()` auto-discovery of host/port/token from persisted state confirmed live.
|
|
354
|
+
|
|
355
|
+
### Fixed
|
|
356
|
+
- **WAL-aware catalog open** (`_sqlite.open_catalog`). Lightroom keeps the catalog in WAL mode while running, so recent writes live in `.lrcat-wal` and aren't yet checkpointed into the main `.lrcat`. Our previous `immutable=1` URI silently ignored the WAL — for example, on a fresh LR 15.3 install we counted `collections: 0` even though 8 default smart-collection rows were sitting in a 502 KB `.lrcat-wal`. Now: if a non-empty `.lrcat-wal` exists, copy the trio (`.lrcat` + `-wal` + `-shm`) to a tempdir and open the copy with regular `mode=ro` so SQLite applies the WAL. If no WAL or empty WAL, keep the fast `immutable=1` path. Includes a regression test (`test_wal_aware_open`) that reproduces the real-LR bug against the synthetic catalog fixture.
|
|
357
|
+
|
|
358
|
+
## [0.1.0] — 2026-04-28
|
|
359
|
+
|
|
360
|
+
First shipped release. Covers Phase 0 (scaffold) → Phase 3 (metadata writes).
|
|
361
|
+
|
|
362
|
+
What works:
|
|
363
|
+
- Async `LightroomClient` with namespaced sub-clients (`catalog`, `photos`, `develop`, `metadata`, `collections`, `library`, `ai`, `edit_in`).
|
|
364
|
+
- Click+Rich CLI: `lightroom doctor | bridge | catalog | photos | metadata | skill`.
|
|
365
|
+
- Local HTTP bridge server (aiohttp) with command queue, long-poll, plugin handshake, token+session auth.
|
|
366
|
+
- Tiny Lua `.lrplugin` (`lightroom-py-bridge.lrplugin`) with `LrTasks` poll loop, JSON encoder/decoder, dispatcher.
|
|
367
|
+
- SQLite read fast-path against `.lrcat` (immutable URI + tempfile fallback) for catalog stats and photo queries.
|
|
368
|
+
- Metadata writes via the bridge (keywords, ratings, color labels, IPTC) plus ExifTool fast-path for bulk XMP.
|
|
369
|
+
- Auto-discovery of bridge state via persisted `bridge.json`.
|
|
370
|
+
- 43 tests, full ruff + mypy clean.
|
|
371
|
+
|
|
372
|
+
What's documented but not yet implemented (planned for later phases):
|
|
373
|
+
- Phase 4 — Develop module (presets, sliders, settings-table application).
|
|
374
|
+
- Phase 5 — AI staging + Edit-In escape hatch.
|
|
375
|
+
- Phase 6 — SKILL.md content polish + PyPI publish.
|
|
376
|
+
- Phase 7 — Dual-`LrSocket` fast lane, MCP server adapter, optional Cloud LR sub-client.
|
|
377
|
+
|
|
378
|
+
### Added — Phase 3 (metadata writes + ExifTool fast-path)
|
|
379
|
+
- **Lua handlers**: `metadata.add_keywords`, `metadata.remove_keywords`, `metadata.set_rating`, `metadata.set_color_label`, `metadata.set_iptc`, `metadata.write_xmp`, `metadata.read_xmp`. All run inside `withWriteAccessDo` (or `withReadAccessDo` for XMP flush) and return `{touched, missing}`.
|
|
380
|
+
- **MetadataAPI**: bridge-driven `add_keywords` / `remove_keywords` / `set_rating` (0..5 validated client-side) / `set_color_label` (validated against `{"", red, yellow, green, blue, purple}`) / `set_iptc` / `write_xmp` / `read_xmp`. Plus `fast_write_xmp(tags_by_uuid, sync_back=True)` that resolves UUIDs → file paths via SQLite and bulk-writes via ExifTool, then triggers LR re-read.
|
|
381
|
+
- **ExifTool fast-path** (`lightroom/_exiftool.py`): persistent `exiftool -stay_open` process wrapper with `read_tags`, `write_tags`, `write_tags_batch` (groups files by tag-dict identity to amortize). Auto-discovers ExifTool on PATH plus common install locations.
|
|
382
|
+
- **SQLite UUID resolver** (`lightroom._sqlite.resolve_paths`): bulk `id_global` → absolute filesystem path lookup, joins root + folder + file rows. Used by `fast_write_xmp` and the agent skill.
|
|
383
|
+
- **CLI**: `lightroom metadata add-keywords | remove-keywords | rate | color | set-iptc | write-xmp | read-xmp | fast-write-xmp`. `--selection` flag for active-LR-selection or pass UUIDs as positional args. `set-iptc -f KEY=VALUE` (repeatable). `fast-write-xmp` reads JSON from arg or stdin.
|
|
384
|
+
- **Bridge state auto-discovery** (`lightroom/_bridge_state.py`): `LightroomClient.connect()` now reads `$LIGHTROOM_HOME/profiles/<profile>/bridge.json` as a fallback so a single `lightroom bridge start` configures the whole library for the session. Resolution order: explicit kwarg → env var → persisted state → built-in default.
|
|
385
|
+
- **Tests** (43 total, +6 from Phase 2): `CapturingPlugin` records every command the bridge enqueues, used to verify wire-level behaviour for keywords/rating/color/IPTC/XMP. UUID resolver tests against the synthetic catalog. Bridge state auto-discovery tests covering all four resolution layers. End-to-end CLI smoke (`/tmp/lr_phase3_smoke.py`) exercises every metadata subcommand against a real bridge subprocess.
|
|
386
|
+
|
|
387
|
+
### Added — Phase 2 (SQLite read fast-path + first real handlers)
|
|
388
|
+
- **SQLite reader** (`lightroom/_sqlite.py`): opens `.lrcat` read-only via `immutable=1` URI (works while LR is running); auto-fallback to a tempfile copy when immutable open fails. Strictly read-only — schema is undocumented and writes risk catalog corruption.
|
|
389
|
+
- **CatalogSummary / CatalogStats / PhotoRow** dataclasses + `get_catalog_summary`, `get_catalog_stats`, `list_photos`, `count_photos` queries with EXIF (camera/lens), keyword (case-insensitive), rating range, and capture-time filters.
|
|
390
|
+
- **Per-profile context** (`lightroom/_context.py`): active catalog path persisted to `~/.lightroom/profiles/<profile>/context.json`, shared across CLI invocations.
|
|
391
|
+
- **`CatalogAPI.info()` / `.stats()` / `.open()` / `.active_path()`** wired to the SQLite fast-path.
|
|
392
|
+
- **`PhotosAPI.list()` / `.count()`** wired to SQLite; **`PhotosAPI.select()`** wired to the bridge plugin.
|
|
393
|
+
- **`LightroomClient.connect(require_bridge=False)`**: skip bridge for read-only flows.
|
|
394
|
+
- **CLI**: `lightroom catalog open|info|stats|which|clear`, `lightroom photos list|count|select` with `--rating ">=4"` etc. parser, `--json` output, Rich tables.
|
|
395
|
+
- **Lua handlers**: `catalog.path`, `selection.uuids`, `photos.select` (resolves UUIDs and calls `setSelectedPhotos` inside `withWriteAccessDo`).
|
|
396
|
+
- **Tests**: synthetic `.lrcat` fixture + 11 SQLite/CatalogAPI tests covering filters, joins, date ranges, rating ranges, missing-catalog + no-active-catalog errors. 29 tests total, all green.
|
|
397
|
+
|
|
398
|
+
### Added — Phase 1 (bridge protocol)
|
|
399
|
+
- **Bridge server**: full command queue, long-poll `/poll`, `/respond`, `/handshake`, `/enqueue`, `/result/<id>`, `/health`. Per-process token + per-session `session_id`. Plugin handshake recorded in `/health`.
|
|
400
|
+
- **HttpBridgeClient**: opens against a running bridge, probes `/health`, dispatches via `/enqueue` + `/result/<id>` with timeout + `CommandFailedError` translation.
|
|
401
|
+
- **InProcessBridgeClient**: in-process variant for tests / one-shot CLI calls.
|
|
402
|
+
- **`LightroomClient.ping()`**: round-trip a `ping` through bridge → plugin.
|
|
403
|
+
- **CLI**: `lightroom bridge start` (persists token + host/port to `~/.lightroom/profiles/<profile>/bridge.json`), `lightroom bridge status`, `lightroom bridge ping`. `lightroom doctor` now probes `/health` and reports plugin handshake state.
|
|
404
|
+
- **Lua plugin**: real `LrTasks` long-poll loop in `BridgeRunner.lua` with handshake → poll → dispatch → respond. Tiny `json.lua` encoder/decoder. `Handlers.lua` dispatcher with `ping` + `echo`. Library menu items `Start bridge`, `Stop bridge`, `Status`, `Configure...`. State stored in plugin prefs.
|
|
405
|
+
- **Tests**: end-to-end protocol round-trip (`MockPlugin` Python double exercises handshake/poll/respond), handler-error propagation, bad-token rejection, `/health` reflects plugin state.
|
|
406
|
+
|
|
407
|
+
### Added — Phase 0 (scaffold)
|
|
408
|
+
- Project layout (`src/lightroom/`, `tests/`, `docs/`, `plugin/`), `pyproject.toml` (hatchling), ruff + mypy + pre-commit configs, MIT license, `CHANGELOG.md`.
|
|
409
|
+
- Empty Click CLI skeleton with `doctor`, `bridge`, `catalog`, `photos`, `skill` command groups.
|
|
410
|
+
- `LightroomClient` async stub with namespaced sub-clients (`catalog`, `photos`, `develop`, `metadata`, `collections`, `library`, `ai`, `edit_in`).
|
|
411
|
+
- `SKILL.md`, `AGENTS.md`, full `PLAN.md` design + research log.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
title: "lightroom-py: A Python and Claude agent driver for Adobe Lightroom Classic"
|
|
3
|
+
message: "If you use this software, please cite it using the metadata in this file."
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- name: drshy
|
|
7
|
+
website: "http://www.drshy.xyz"
|
|
8
|
+
repository-code: "https://github.com/drshy-org/lightroom-py"
|
|
9
|
+
url: "http://www.drshy.xyz"
|
|
10
|
+
abstract: >-
|
|
11
|
+
Unofficial Python library, CLI, and Claude/Codex agent skill for automating
|
|
12
|
+
Adobe Lightroom Classic. First open agent driver with verified programmatic
|
|
13
|
+
geometry mask creation (Mask/CircularGradient via apply_settings, 35.4%
|
|
14
|
+
pixel-diff confirmed against LR Classic 15.3) and the empirically documented
|
|
15
|
+
AI mask compute path via Adaptive preset + Export dialog. Architecturally a
|
|
16
|
+
Lua plugin polling a local aiohttp server, exposing 62 bridge handlers + 80
|
|
17
|
+
CLI verbs as Python async client, Click CLI, MCP server, and Claude agent
|
|
18
|
+
skill.
|
|
19
|
+
keywords:
|
|
20
|
+
- lightroom
|
|
21
|
+
- lightroom-classic
|
|
22
|
+
- adobe
|
|
23
|
+
- photography
|
|
24
|
+
- automation
|
|
25
|
+
- claude
|
|
26
|
+
- mcp
|
|
27
|
+
- agent
|
|
28
|
+
- ai
|
|
29
|
+
- photo-editing
|
|
30
|
+
- raw
|
|
31
|
+
license: MIT
|
|
32
|
+
version: 0.6.0
|
|
33
|
+
date-released: "2026-05-10"
|