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:
Cameron
2026-04-17 15:35:14 -04:00
parent 00da97fe86
commit 08ac9a7695
5 changed files with 92 additions and 4 deletions

View File

@@ -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(),
),