Batch EXIF endpoint: GET /photos/exif

Adds a single round-trip projection of `image_exif` for every photo whose
`date_taken` falls in `[date_from, date_to]`. Wraps the existing
`ExifDao::query_by_exif` DAO method which already handles the SQL filter
in one query against the covering index — the only missing piece was
HTTP plumbing.

Designed for window-scoped consumers like Apollo's photo-to-track
matcher, which currently does N+1 (one `/photos` listing + one
`/image/metadata` per photo). Because `/image/metadata` serializes on
`Data<Mutex<dyn ExifDao>>`, that pattern can take 10s+ for windows with
hundreds of photos. The new endpoint takes one mutex acquisition for
the whole batch.

Response shape:
  { photos: [
      { file_path, library_id, library_name,
        camera_model, width, height,
        gps_latitude, gps_longitude, date_taken } ],
    total: N }

Two notes on scope:
- Photos with NULL `date_taken` are excluded by `query_by_exif`'s
  semantics. Filename-extracted dates are not synthesized here; rare
  callers that need that fallback can still hit `/image/metadata`.
- GPS columns are stored as f32 in image_exif to keep row size small;
  the JSON shape widens to f64 so clients don't have to know about the
  on-disk precision.

Library names are pre-mapped from `app_state.libraries` once and
stamped on each row, avoiding an O(rows × libraries) linear scan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-04-27 16:38:53 -04:00
parent 9cf3af383d
commit c6f82ebaba
3 changed files with 119 additions and 1 deletions

View File

@@ -390,6 +390,36 @@ pub struct GpsPhotosResponse {
pub total: usize,
}
/// Single-row projection of `image_exif` rich enough to drive Apollo's
/// photo-to-track matcher (and any similar window-scoped consumer) without
/// a per-file `/image/metadata` round-trip. Returned by `/photos/exif`.
#[derive(Debug, Serialize)]
pub struct ExifSummary {
pub file_path: String,
pub library_id: i32,
pub library_name: Option<String>,
pub camera_model: Option<String>,
pub width: Option<i32>,
pub height: Option<i32>,
pub gps_latitude: Option<f64>,
pub gps_longitude: Option<f64>,
pub date_taken: Option<i64>,
}
#[derive(Debug, Serialize)]
pub struct ExifBatchResponse {
pub photos: Vec<ExifSummary>,
pub total: usize,
}
#[derive(Debug, Deserialize)]
pub struct ExifBatchRequest {
/// Lower bound (inclusive) for `image_exif.date_taken`, unix seconds.
pub date_from: Option<i64>,
/// Upper bound (inclusive). Same semantics as `date_to` on `/photos`.
pub date_to: Option<i64>,
}
#[derive(Deserialize)]
pub struct PreviewClipRequest {
pub path: String,