From 2d1429173329027615b22d182ba0fd16f519d982 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Wed, 6 May 2026 16:00:14 -0400 Subject: [PATCH] ingest: stamp canonical date_taken on every InsertImageExif MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires `date_resolver::resolve_date_taken` into the three call sites that build `InsertImageExif`: - `process_new_files` (file watcher) — every newly-registered file gets the resolver's verdict so videos and EXIF-stripped images land with a real date instead of NULL. - Upload handler — same waterfall on the post-multipart-write path. - GPS-write handler — re-runs the waterfall after exiftool writes GPS and re-reads the EXIF, in case a previously fs_time-sourced row now has a real EXIF date to upgrade to. This is a behavior change vs. the pre-rewrite `/memories` request-time priority: EXIF now beats filename when both are present. A photo named `Screenshot_2014-06-01.png` whose EXIF `DateTime` is 2021 now appears under 2021. The reverse case (no EXIF, parseable filename) is unchanged and continues to surface the filename date with `date_taken_source = 'filename'`. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main.rs | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 84af187..f7ea49f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -504,6 +504,11 @@ async fn set_image_gps( }; let now = Utc::now().timestamp(); let normalized_path = body.path.replace('\\', "/"); + // Re-run the canonical-date waterfall on every GPS write — exiftool + // writing GPS doesn't change the capture date, but if the row was + // previously sourced from `fs_time` the re-read may have given us a + // real EXIF date this time, and we want to upgrade the source. + let resolved_date = date_resolver::resolve_date_taken(&full_path, extracted.date_taken); let insert_exif = InsertImageExif { library_id: resolved_library.id, file_path: normalized_path.clone(), @@ -520,7 +525,7 @@ async fn set_image_gps( aperture: extracted.aperture.map(|v| v as f32), shutter_speed: extracted.shutter_speed, iso: extracted.iso, - date_taken: extracted.date_taken, + date_taken: resolved_date.map(|r| r.timestamp), // Created_time is preserved by update_exif (it doesn't touch the // column); pass any int — it's ignored in the UPDATE statement. created_time: now, @@ -538,8 +543,7 @@ async fn set_image_gps( // with a usable signal; failure just leaves prior values in place. phash_64: perceptual_hash::compute(&full_path).map(|h| h.phash_64), dhash_64: perceptual_hash::compute(&full_path).map(|h| h.dhash_64), - // Replaced in a follow-up commit with the canonical-date resolver's output. - date_taken_source: None, + date_taken_source: resolved_date.map(|r| r.source.as_str().to_string()), }; let updated = { @@ -752,6 +756,10 @@ async fn upload_image( } }; let perceptual = perceptual_hash::compute(&uploaded_path); + let resolved_date = date_resolver::resolve_date_taken( + &uploaded_path, + exif_data.date_taken, + ); let insert_exif = InsertImageExif { library_id: target_library.id, file_path: relative_path.clone(), @@ -768,15 +776,15 @@ async fn upload_image( aperture: exif_data.aperture.map(|v| v as f32), shutter_speed: exif_data.shutter_speed, iso: exif_data.iso, - date_taken: exif_data.date_taken, + date_taken: resolved_date.map(|r| r.timestamp), created_time: timestamp, last_modified: timestamp, content_hash, size_bytes, phash_64: perceptual.map(|h| h.phash_64), dhash_64: perceptual.map(|h| h.dhash_64), - // Replaced in a follow-up commit with the canonical-date resolver's output. - date_taken_source: None, + date_taken_source: resolved_date + .map(|r| r.source.as_str().to_string()), }; if let Ok(mut dao) = exif_dao.lock() { @@ -2382,6 +2390,16 @@ fn process_new_files( None }; + // Canonical date_taken via the waterfall — kamadak-exif (already + // computed above) → exiftool fallback for videos / MakerNote / + // QuickTime → filename regex → earliest_fs_time. Source is + // recorded so the per-tick backfill drain can re-run weak + // resolutions later. + let resolved_date = date_resolver::resolve_date_taken( + &file_path, + exif_fields.as_ref().and_then(|e| e.date_taken), + ); + let insert_exif = InsertImageExif { library_id: library.id, file_path: relative_path.clone(), @@ -2408,15 +2426,14 @@ fn process_new_files( .and_then(|e| e.aperture.map(|v| v as f32)), shutter_speed: exif_fields.as_ref().and_then(|e| e.shutter_speed.clone()), iso: exif_fields.as_ref().and_then(|e| e.iso), - date_taken: exif_fields.as_ref().and_then(|e| e.date_taken), + date_taken: resolved_date.map(|r| r.timestamp), created_time: timestamp, last_modified: timestamp, content_hash, size_bytes, phash_64: perceptual.map(|h| h.phash_64), dhash_64: perceptual.map(|h| h.dhash_64), - // Replaced in a follow-up commit with the canonical-date resolver's output. - date_taken_source: None, + date_taken_source: resolved_date.map(|r| r.source.as_str().to_string()), }; let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");