feat: add GET /libraries and library query param plumbing
New `/libraries` endpoint returns configured libraries so clients can discover them. `FilesRequest` and `MemoriesRequest` gain an optional `library` param (accepts name or numeric id). Unknown values are rejected with 400; absent values span all libraries. `/memories` now scopes its filesystem walk + EXIF query to the resolved library. `MemoryItem` carries `library_id` so union-mode clients can render a per-item source badge. Behavior is unchanged in single-library mode: omitting `library` still returns results from the primary library, which is the only one configured until a second row is added to the libraries table. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,9 @@ pub struct MemoriesRequest {
|
||||
pub span: Option<MemoriesSpan>,
|
||||
/// Client timezone offset in minutes from UTC (e.g., -480 for PST, 60 for CET)
|
||||
pub timezone_offset_minutes: Option<i32>,
|
||||
/// Optional library filter. Accepts a library id (e.g. "1") or name
|
||||
/// (e.g. "main"). When omitted, results span all libraries.
|
||||
pub library: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
@@ -114,6 +117,9 @@ pub struct MemoryItem {
|
||||
pub path: String,
|
||||
pub created: Option<i64>,
|
||||
pub modified: Option<i64>,
|
||||
/// Id of the library this memory belongs to. Allows clients to show a
|
||||
/// per-item source badge in union mode.
|
||||
pub library_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -417,6 +423,7 @@ fn collect_exif_memories(
|
||||
path: file_path.clone(),
|
||||
created,
|
||||
modified,
|
||||
library_id: crate::libraries::PRIMARY_LIBRARY_ID,
|
||||
},
|
||||
file_date,
|
||||
))
|
||||
@@ -478,6 +485,7 @@ fn collect_filesystem_memories(
|
||||
path: path_relative,
|
||||
created,
|
||||
modified,
|
||||
library_id: crate::libraries::PRIMARY_LIBRARY_ID,
|
||||
},
|
||||
file_date,
|
||||
))
|
||||
@@ -526,7 +534,23 @@ pub async fn list_memories(
|
||||
|
||||
debug!("Now: {:?}", now);
|
||||
|
||||
let base = Path::new(&app_state.base_path);
|
||||
// Resolve the optional library filter. Unknown values are a 400; None
|
||||
// means "all libraries" — currently equivalent to the primary library
|
||||
// while only one is configured.
|
||||
let library = match crate::libraries::resolve_library_param(
|
||||
&app_state,
|
||||
q.library.as_deref(),
|
||||
) {
|
||||
Ok(lib) => lib,
|
||||
Err(msg) => {
|
||||
warn!("Rejecting /memories request: {}", msg);
|
||||
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);
|
||||
|
||||
// Build the path excluder from base and env-configured exclusions
|
||||
let path_excluder = PathExcluder::new(base, &app_state.excluded_dirs);
|
||||
@@ -535,7 +559,7 @@ pub async fn list_memories(
|
||||
let exif_memories = collect_exif_memories(
|
||||
&exif_dao,
|
||||
&span_context,
|
||||
&app_state.base_path,
|
||||
&scoped_library.root_path,
|
||||
now,
|
||||
span_mode,
|
||||
years_back,
|
||||
@@ -546,12 +570,12 @@ pub async fn list_memories(
|
||||
// Build HashSet for deduplication
|
||||
let exif_paths: HashSet<PathBuf> = exif_memories
|
||||
.iter()
|
||||
.map(|(item, _)| PathBuf::from(&app_state.base_path).join(&item.path))
|
||||
.map(|(item, _)| PathBuf::from(&scoped_library.root_path).join(&item.path))
|
||||
.collect();
|
||||
|
||||
// Phase 2: File system scan (skip EXIF files)
|
||||
let fs_memories = collect_filesystem_memories(
|
||||
&app_state.base_path,
|
||||
&scoped_library.root_path,
|
||||
&path_excluder,
|
||||
&exif_paths,
|
||||
now,
|
||||
@@ -1098,6 +1122,7 @@ mod tests {
|
||||
path: "photo1.jpg".to_string(),
|
||||
created: Some(jan_15_2024_9am),
|
||||
modified: Some(jan_15_2024_9am),
|
||||
library_id: crate::libraries::PRIMARY_LIBRARY_ID,
|
||||
},
|
||||
NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
|
||||
),
|
||||
@@ -1106,6 +1131,7 @@ mod tests {
|
||||
path: "photo2.jpg".to_string(),
|
||||
created: Some(jan_15_2020_10am),
|
||||
modified: Some(jan_15_2020_10am),
|
||||
library_id: crate::libraries::PRIMARY_LIBRARY_ID,
|
||||
},
|
||||
NaiveDate::from_ymd_opt(2020, 1, 15).unwrap(),
|
||||
),
|
||||
@@ -1114,6 +1140,7 @@ mod tests {
|
||||
path: "photo3.jpg".to_string(),
|
||||
created: Some(jan_16_2021_8am),
|
||||
modified: Some(jan_16_2021_8am),
|
||||
library_id: crate::libraries::PRIMARY_LIBRARY_ID,
|
||||
},
|
||||
NaiveDate::from_ymd_opt(2021, 1, 16).unwrap(),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user