feat: multi-library foundation (schema + libraries module)

Adds a `libraries` registry table and threads library_id through
per-instance metadata tables (image_exif, photo_insights,
entity_photo_links, video_preview_clips). File-path columns renamed to
rel_path to make the relative-to-root semantics explicit. Adds
content_hash + size_bytes on image_exif to support future hash-keyed
thumbnail/HLS dedup. Tags and favorites stay library-agnostic so they
share across libraries by rel_path.

Behavior is unchanged: a single primary library (id=1) is seeded from
BASE_PATH on first boot; all handlers and DAOs route through it as a
transitional shim until the API gains a library query param.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Cameron
2026-04-17 15:28:30 -04:00
parent 8bc948b297
commit 00da97fe86
17 changed files with 750 additions and 108 deletions

View File

@@ -84,6 +84,7 @@ impl PreviewDao for SqlitePreviewDao {
diesel::insert_or_ignore_into(video_preview_clips)
.values(InsertVideoPreviewClip {
library_id: 1,
file_path: file_path_val.to_string(),
status: status_val.to_string(),
created_at: now.clone(),
@@ -111,7 +112,7 @@ impl PreviewDao for SqlitePreviewDao {
let mut connection = self.connection.lock().expect("Unable to get PreviewDao");
let now = chrono::Utc::now().to_rfc3339();
diesel::update(video_preview_clips.filter(file_path.eq(file_path_val)))
diesel::update(video_preview_clips.filter(rel_path.eq(file_path_val)))
.set((
status.eq(status_val),
duration_seconds.eq(duration),
@@ -137,7 +138,7 @@ impl PreviewDao for SqlitePreviewDao {
let mut connection = self.connection.lock().expect("Unable to get PreviewDao");
match video_preview_clips
.filter(file_path.eq(file_path_val))
.filter(rel_path.eq(file_path_val))
.first::<VideoPreviewClip>(connection.deref_mut())
{
Ok(clip) => Ok(Some(clip)),
@@ -163,7 +164,7 @@ impl PreviewDao for SqlitePreviewDao {
let mut connection = self.connection.lock().expect("Unable to get PreviewDao");
video_preview_clips
.filter(file_path.eq_any(file_paths))
.filter(rel_path.eq_any(file_paths))
.load::<VideoPreviewClip>(connection.deref_mut())
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})