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

+12 -1
View File
@@ -13,7 +13,7 @@ use crate::database::{
LocationHistoryDao, PrecomputedReelDao, SearchHistoryDao, SqliteCalendarEventDao,
SqliteDailySummaryDao, SqliteExifDao, SqliteInsightDao, SqliteInsightGenerationJobDao,
SqliteKnowledgeDao, SqliteLocationHistoryDao, SqlitePrecomputedReelDao, SqliteSearchHistoryDao,
SqliteUserAiPrefsDao, UserAiPrefsDao, connect,
SqliteUserAiPrefsDao, SqliteVersionDao, UserAiPrefsDao, VersionDao, connect,
};
use crate::database::{PreviewDao, SqlitePreviewDao};
use crate::faces;
@@ -98,6 +98,10 @@ pub struct AppState {
/// client; read by the nightly pre-generation scheduler.
#[allow(dead_code)]
pub user_ai_prefs_dao: Arc<Mutex<Box<dyn UserAiPrefsDao>>>,
/// Photo-version links and the global display choice (#191). Read by
/// the listings that annotate a photo with its versions and by the
/// `/image/versions` endpoints.
pub version_dao: Arc<Mutex<Box<dyn VersionDao>>>,
}
impl AppState {
@@ -142,6 +146,7 @@ impl AppState {
insight_job_handles: Arc<Mutex<HashMap<i32, tokio::task::AbortHandle>>>,
precomputed_reel_dao: Arc<Mutex<Box<dyn PrecomputedReelDao>>>,
user_ai_prefs_dao: Arc<Mutex<Box<dyn UserAiPrefsDao>>>,
version_dao: Arc<Mutex<Box<dyn VersionDao>>>,
) -> Self {
assert!(
!libraries_vec.is_empty(),
@@ -202,6 +207,7 @@ impl AppState {
insight_job_handles,
precomputed_reel_dao,
user_ai_prefs_dao,
version_dao,
}
}
@@ -290,6 +296,9 @@ impl Default for AppState {
let user_ai_prefs_dao: Arc<Mutex<Box<dyn UserAiPrefsDao>>> =
Arc::new(Mutex::new(Box::new(SqliteUserAiPrefsDao::new())));
let version_dao: Arc<Mutex<Box<dyn VersionDao>>> =
Arc::new(Mutex::new(Box::new(SqliteVersionDao::new())));
// Load base path and ensure the primary library row reflects it.
let base_path = env::var("BASE_PATH").expect("BASE_PATH was not set in the env");
let mut seed_conn = connect();
@@ -374,6 +383,7 @@ impl Default for AppState {
insight_job_handles,
precomputed_reel_dao,
user_ai_prefs_dao,
version_dao,
)
}
}
@@ -552,6 +562,7 @@ impl AppState {
Arc::new(Mutex::new(HashMap::new())), // placeholder for test
Arc::new(Mutex::new(Box::new(SqlitePrecomputedReelDao::new()))), // placeholder for test
Arc::new(Mutex::new(Box::new(SqliteUserAiPrefsDao::new()))), // placeholder for test
Arc::new(Mutex::new(Box::new(SqliteVersionDao::new()))), // placeholder for test
)
}
}