feat: per-photo EXIF editor (date taken + GPS coordinates)#3493
Open
karlitschek wants to merge 1 commit into
Open
feat: per-photo EXIF editor (date taken + GPS coordinates)#3493karlitschek wants to merge 1 commit into
karlitschek wants to merge 1 commit into
Conversation
Adds an "Edit metadata…" entry to the per-photo overflow menu.
Lets the user correct the two EXIF fields users actually edit in
practice: capture date (cameras with the wrong clock; scanned old
photos) and GPS location (phones without GPS; corrections).
The edits don't touch the original file. They live in a new
photos-app table and overlay at read time, so:
- the original on disk stays byte-identical
- each user's overrides are private (bob's edit on a shared photo
doesn't change what alice sees)
- a Reset button per field falls back to the EXIF / mtime value
Backend
- New `oc_photos_metadata_edits` table: composite PK
`(user_id, file_id)`, NULLable `taken_at` / `gps_lat` / `gps_lng`.
Lat/lng stored as DECIMAL(9,6) — six decimals = ~11 cm at the
equator, plenty for hand-corrected locations, no float-precision
drift. Index on `file_id` alone for the future delete listener.
- `PhotoMetadataEditMapper` upsert + per-file lookup + bulk lookup
for a page of fileIds. Direct DB access (composite PK is
incompatible with QBMapper).
- `MetadataEditController` exposes:
GET /api/v1/metadata/{fileId} → current overrides
PUT /api/v1/metadata/{fileId} → patch (omitted = unchanged,
null = clear).
Authorisation via `userFolder->getById($fileId)` so users can't
probe other users' libraries.
- `IndexController.composeItem` now layers the override on top of
the EXIF view: edited `taken_at` becomes both the response's
top-level `takenAt` (timeline sort key) AND the metadata bag's
`photos-original_date_time` (display key), so the photos UI
stays internally consistent. Edited GPS replaces the EXIF GPS
array entirely.
- On save the controller also pushes the resolved `taken_at` into
`oc_photos_index.taken_at` for that user's row, so the timeline
ORDER BY picks up the edit immediately without a reload.
Frontend
- `PhotoMetadataEditService.ts` thin REST wrapper. Patch shape:
omitted field = leave alone, `null` field = clear override.
- `MetadataEditDialog.vue` form-based dialog with a
`<input type="datetime-local">` for date and two NcTextField
inputs for lat/lng. Live validation (range checks fire as the
user types; Save stays disabled while values are out of range).
Per-field "Reset to original" buttons that clear the stored
override and re-seed from the EXIF defaults the caller passed.
- `PhotoActionsMenu` gets a new "Edit metadata…" entry next to
"View metadata", with the same icon-on-the-left layout as the
other actions. Mounted lazily so the API call only fires when
the user actually opens it.
Out of scope (follow-ups)
- Map picker for GPS (latitude/longitude inputs are functional but
not a great UX for non-technical users). Drop-in component
candidates: `vue-leaflet` (already a dep on the map view) wired
to a draggable marker.
- Title / description / orientation fields. Those are XMP, not
EXIF, and need a different storage path because NC indexes them
separately.
- Bulk edit (apply the same date offset to a multi-selection,
useful for scanned albums where every photo is wrong by the
same amount).
- Writing the overrides back to the file's actual EXIF (would
require shelling out to exiftool / ffmpeg). Current overlay
approach is non-destructive — that's a feature for now, but
power users may eventually want both.
Signed-off-by: Frank Karlitschek <frank@nextcloud.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Member
Author
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an "Edit metadata…" entry to the per-photo overflow menu. Lets the user correct the two EXIF fields users actually edit in practice: capture date (cameras with the wrong clock; scanned old photos) and GPS location (phones without GPS; corrections).
The edits don't touch the original file. They live in a new photos-app table and overlay at read time, so:
Backend
oc_photos_metadata_editstable: composite PK(user_id, file_id), NULLabletaken_at/gps_lat/gps_lng. Lat/lng stored as DECIMAL(9,6) — six decimals = ~11 cm at the equator, plenty for hand-corrected locations, no float-precision drift. Index onfile_idalone for the future delete listener.PhotoMetadataEditMapperupsert + per-file lookup + bulk lookup for a page of fileIds. Direct DB access (composite PK is incompatible with QBMapper).MetadataEditControllerexposes: GET /api/v1/metadata/{fileId} → current overrides PUT /api/v1/metadata/{fileId} → patch (omitted = unchanged, null = clear). Authorisation viauserFolder->getById($fileId)so users can't probe other users' libraries.IndexController.composeItemnow layers the override on top of the EXIF view: editedtaken_atbecomes both the response's top-leveltakenAt(timeline sort key) AND the metadata bag'sphotos-original_date_time(display key), so the photos UI stays internally consistent. Edited GPS replaces the EXIF GPS array entirely.taken_atintooc_photos_index.taken_atfor that user's row, so the timeline ORDER BY picks up the edit immediately without a reload.Frontend
PhotoMetadataEditService.tsthin REST wrapper. Patch shape: omitted field = leave alone,nullfield = clear override.MetadataEditDialog.vueform-based dialog with a<input type="datetime-local">for date and two NcTextField inputs for lat/lng. Live validation (range checks fire as the user types; Save stays disabled while values are out of range). Per-field "Reset to original" buttons that clear the stored override and re-seed from the EXIF defaults the caller passed.PhotoActionsMenugets a new "Edit metadata…" entry next to "View metadata", with the same icon-on-the-left layout as the other actions. Mounted lazily so the API call only fires when the user actually opens it.Out of scope (follow-ups)
vue-leaflet(already a dep on the map view) wired to a draggable marker.