feat(versions): link at ingest, hide in listings, version endpoints

XMP (PNG iTXt DerivedFrom, native chunk reader) then file-name convention
links at ingest; unlinked name candidates are retried each tick. Versions
are hidden from /photos (SQL for the recursive listing, hidden-row set for
the walk and tag branches), /memories, reels, /photos/exif, the GPS summary,
unified search and the media gauges while their original is reachable in the
view; include_versions=true lists them. /photos gains the parallel arrays
photo_display_paths / photo_version_counts, memories / search hits /
/photos/exif rows gain display_path / version_count, all skipped when empty.
GET /image/versions and PUT /image/versions/display (library optional).
Tags and favorites on a version path bind to the original; faces, CLIP and
duplicate grouping skip version hashes; insight generation redirects to the
original. Orphan GC and the back-ref refresh cover the new tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-10-30 11:28:34 -04:00
1 parent 044240037a
commit 080a75c336
24 files changed
+1614 -47

No files matched your search

+26
View File
@@ -108,6 +108,19 @@ pub struct PhotosResponse {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub photo_libraries: Vec<i32>,
/// Photo versions (#191), parallel to `photos`. `photos[i]` is always
/// the *original's* path (it anchors tags, sort order and dates).
/// `photo_display_paths[i]` is the file to show instead when the global
/// display choice isn't the original, else `null`; skipped when no photo
/// on the page has one.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub photo_display_paths: Vec<Option<String>>,
/// How many versions stand behind `photos[i]`; skipped when the page has
/// none, so responses without versions are unchanged.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub photo_version_counts: Vec<u32>,
// Pagination metadata (only present when limit is set)
#[serde(skip_serializing_if = "Option::is_none")]
pub total_count: Option<i64>,
@@ -180,6 +193,13 @@ pub struct FilesRequest {
/// inside a group.
#[serde(default)]
pub include_duplicates: Option<bool>,
/// When true, list photo versions (#191) as photos of their own instead
/// of hiding them behind their original. Default false. Counterpart of
/// `include_duplicates`; a version whose original is not reachable in
/// the request's view is listed either way.
#[serde(default)]
pub include_versions: Option<bool>,
}
#[derive(Copy, Clone, Deserialize, PartialEq, Debug)]
@@ -495,6 +515,11 @@ pub struct ExifSummary {
/// Capture wall clock to display (naive ISO-8601; see
/// `MetadataResponse::wall_clock`).
pub wall_clock: Option<String>,
/// `display_path` / `version_count` (#191); absent for a photo without
/// versions. `file_path` is always the original's (versions are hidden
/// from this listing while their original is reachable).
#[serde(flatten)]
pub versions: crate::versions::VersionFields,
}
impl ExifSummary {
@@ -525,6 +550,7 @@ impl ExifSummary {
date_utc: r.date_utc,
camera_offset_minutes: r.camera_offset_minutes,
date_taken_tz_source: r.date_taken_tz_source,
versions: Default::default(),
}
}
}