feat: union /photos and /memories across libraries
When `library` is omitted, both endpoints now walk every configured library root, interleave the results, and tag each row with its source library via the parallel `photo_libraries` / per-row `library_id` arrays. Previously the handlers fell back to the primary library, silently hiding the rest. Threads a parallel `file_libraries: Vec<i32>` through the sort/paginate helpers so library attribution survives sorting and pagination. Directory names are de-duplicated across libraries. `get_all_with_date_taken` grows an optional library filter so memories can scope its EXIF query per-library during the union walk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ use walkdir::WalkDir;
|
||||
use crate::data::Claims;
|
||||
use crate::database::ExifDao;
|
||||
use crate::files::is_image_or_video;
|
||||
use crate::libraries::Library;
|
||||
use crate::otel::{extract_context_from_request, global_tracer};
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -378,7 +379,7 @@ fn collect_exif_memories(
|
||||
) -> Vec<(MemoryItem, NaiveDate)> {
|
||||
// Query database for all files with date_taken
|
||||
let exif_records = match exif_dao.lock() {
|
||||
Ok(mut dao) => match dao.get_all_with_date_taken(context) {
|
||||
Ok(mut dao) => match dao.get_all_with_date_taken(context, Some(library_id)) {
|
||||
Ok(records) => records,
|
||||
Err(e) => {
|
||||
warn!("Failed to query EXIF database: {:?}", e);
|
||||
@@ -546,48 +547,50 @@ pub async fn list_memories(
|
||||
return HttpResponse::BadRequest().body(msg);
|
||||
}
|
||||
};
|
||||
// For Phase 3 the walker still operates against a single library's root.
|
||||
// Multi-library union support for the filesystem walk comes in Phase 4.
|
||||
let scoped_library = library.unwrap_or_else(|| app_state.primary_library());
|
||||
let base = Path::new(&scoped_library.root_path);
|
||||
// When `library` is `Some`, scope to that one library; otherwise union
|
||||
// across every configured library and let the results interleave.
|
||||
let libraries_to_scan: Vec<&Library> = match library {
|
||||
Some(lib) => vec![lib],
|
||||
None => app_state.libraries.iter().collect(),
|
||||
};
|
||||
|
||||
// Build the path excluder from base and env-configured exclusions
|
||||
let path_excluder = PathExcluder::new(base, &app_state.excluded_dirs);
|
||||
let mut memories_with_dates: Vec<(MemoryItem, NaiveDate)> = Vec::new();
|
||||
|
||||
// Phase 1: Query EXIF database
|
||||
let exif_memories = collect_exif_memories(
|
||||
&exif_dao,
|
||||
&span_context,
|
||||
&scoped_library.root_path,
|
||||
scoped_library.id,
|
||||
now,
|
||||
span_mode,
|
||||
years_back,
|
||||
&client_timezone,
|
||||
&path_excluder,
|
||||
);
|
||||
for lib in &libraries_to_scan {
|
||||
let base = Path::new(&lib.root_path);
|
||||
let path_excluder = PathExcluder::new(base, &app_state.excluded_dirs);
|
||||
|
||||
// Build HashSet for deduplication
|
||||
let exif_paths: HashSet<PathBuf> = exif_memories
|
||||
.iter()
|
||||
.map(|(item, _)| PathBuf::from(&scoped_library.root_path).join(&item.path))
|
||||
.collect();
|
||||
let exif_memories = collect_exif_memories(
|
||||
&exif_dao,
|
||||
&span_context,
|
||||
&lib.root_path,
|
||||
lib.id,
|
||||
now,
|
||||
span_mode,
|
||||
years_back,
|
||||
&client_timezone,
|
||||
&path_excluder,
|
||||
);
|
||||
|
||||
// Phase 2: File system scan (skip EXIF files)
|
||||
let fs_memories = collect_filesystem_memories(
|
||||
&scoped_library.root_path,
|
||||
scoped_library.id,
|
||||
&path_excluder,
|
||||
&exif_paths,
|
||||
now,
|
||||
span_mode,
|
||||
years_back,
|
||||
&client_timezone,
|
||||
);
|
||||
let exif_paths: HashSet<PathBuf> = exif_memories
|
||||
.iter()
|
||||
.map(|(item, _)| PathBuf::from(&lib.root_path).join(&item.path))
|
||||
.collect();
|
||||
|
||||
// Phase 3: Merge and sort
|
||||
let mut memories_with_dates = exif_memories;
|
||||
memories_with_dates.extend(fs_memories);
|
||||
let fs_memories = collect_filesystem_memories(
|
||||
&lib.root_path,
|
||||
lib.id,
|
||||
&path_excluder,
|
||||
&exif_paths,
|
||||
now,
|
||||
span_mode,
|
||||
years_back,
|
||||
&client_timezone,
|
||||
);
|
||||
|
||||
memories_with_dates.extend(exif_memories);
|
||||
memories_with_dates.extend(fs_memories);
|
||||
}
|
||||
|
||||
match span_mode {
|
||||
// Sort by absolute time for a more 'overview'
|
||||
|
||||
Reference in New Issue
Block a user