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:
1 parent
044240037a
commit
080a75c336
24 files changed
+1614
-47
No files matched your search
+18
-1
@@ -459,12 +459,21 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
display_zone,
|
||||
server_zone,
|
||||
),
|
||||
// Versions are dropped below, against the request's own
|
||||
// library scope (this query is unscoped).
|
||||
true,
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("unified_search: query_by_exif failed: {e:?}");
|
||||
Vec::new()
|
||||
})
|
||||
};
|
||||
let hidden_versions = state
|
||||
.version_dao
|
||||
.lock()
|
||||
.expect("version dao")
|
||||
.hidden_version_rows(&library_ids)
|
||||
.unwrap_or_default();
|
||||
|
||||
candidate = rows
|
||||
.into_iter()
|
||||
@@ -473,6 +482,10 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
if !library_ids.is_empty() && !library_ids.contains(&row.library_id) {
|
||||
return false;
|
||||
}
|
||||
// A version stays behind its original.
|
||||
if hidden_versions.contains(&(row.library_id, row.file_path.clone())) {
|
||||
return false;
|
||||
}
|
||||
// Precise GPS distance (the EXIF query only did a coarse box).
|
||||
if let Some((lat, lon, radius_km)) = gps {
|
||||
match (row.gps_latitude, row.gps_longitude) {
|
||||
@@ -569,7 +582,7 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
);
|
||||
let total_matching = hits.len();
|
||||
let page = paginate(&hits, offset, limit);
|
||||
let results = resolve_hits(&exif_dao, &page);
|
||||
let results = resolve_hits(&exif_dao, &state.version_dao, &page);
|
||||
HttpResponse::Ok().json(UnifiedResponse {
|
||||
query: nl,
|
||||
interpreted: interpreted(&echo, resolved_place),
|
||||
@@ -593,6 +606,7 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
let results: Vec<SearchHit> = if offset >= total_matching {
|
||||
Vec::new()
|
||||
} else {
|
||||
let summaries = crate::versions::load_summaries(&state.version_dao);
|
||||
candidate[offset..end]
|
||||
.iter()
|
||||
.map(|r| SearchHit {
|
||||
@@ -600,6 +614,9 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
rel_path: r.file_path.clone(),
|
||||
content_hash: r.content_hash.clone().unwrap_or_default(),
|
||||
score: 0.0,
|
||||
versions: crate::versions::VersionFields::from_summary(
|
||||
summaries.get(&(r.library_id, r.file_path.clone())),
|
||||
),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user