From 65af7d999e425934227c7f506f13d9ae768b3362 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Wed, 6 May 2026 20:43:18 -0400 Subject: [PATCH] memories: parse filename dates as UTC, not server local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `extract_date_from_filename` was calling `Local::from_local_datetime` on the parsed YYYY-MM-DD-HH-MM-SS components, then `.timestamp()` was shifting the result by the SERVER's TZ offset to produce real UTC seconds. That made filename-sourced timestamps disagree with EXIF- sourced timestamps by hours: kamadak-exif's `DateTimeOriginal` is a naive string parsed AS-IF-UTC (the project's load-bearing "naive local reinterpreted as UTC" convention), and Apollo's photo matcher re-anchors that naive value through the BROWSER's TZ when matching to the track. Anything stamped in server-local instead got double-shifted on its way through the matcher and through any `formatNaive*` display path on the client. Visible symptom in the Apollo DETAILS modal: a photo's CURRENT date read correctly (1:25 AM via exif) while FROM FILENAME read 4 hours ahead (5:25 AM in EDT) for the same `IMG_20160710_012515.jpg`. Switch to `Utc::from_utc_datetime` so `.timestamp()` returns the wall-clock-as-UTC unix seconds — same convention as the EXIF path. The /memories endpoint, the canonical-date waterfall (which feeds `image_exif.date_taken` for filename-only files), and Apollo's DETAILS modal `filename_date` field all now line up. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/memories.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/memories.rs b/src/memories.rs index 524e2a0..695d486 100644 --- a/src/memories.rs +++ b/src/memories.rs @@ -1,7 +1,6 @@ use actix_web::web::Data; use actix_web::{HttpRequest, HttpResponse, Responder, get, web}; -use chrono::LocalResult::{Ambiguous, Single}; -use chrono::{DateTime, FixedOffset, Local, LocalResult, NaiveDate, TimeZone}; +use chrono::{DateTime, FixedOffset, Local, NaiveDate, TimeZone, Utc}; use log::{debug, trace, warn}; use opentelemetry::KeyValue; use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer}; @@ -134,6 +133,15 @@ pub struct MemoriesResponse { } pub fn extract_date_from_filename(filename: &str) -> Option> { + // Filenames carry only digits — no timezone. We deliberately interpret + // them as UTC so `.timestamp()` returns the wall-clock-as-UTC unix + // seconds, matching the "naive local reinterpreted as UTC" convention + // image_exif.date_taken uses for kamadak-exif DateTimeOriginal (which + // is also naive). Anything else (Local::from_local_datetime, the + // previous behavior) shifted filename-sourced dates by the SERVER's + // TZ offset relative to UTC, making them disagree with EXIF-sourced + // dates by hours and double-shifting through Apollo's photo matcher + // (which re-anchors naive-as-UTC via the browser TZ). let build_date_from_ymd_capture = |captures: ®ex::Captures| -> Option> { let year = captures.get(1)?.as_str().parse::().ok()?; @@ -143,16 +151,8 @@ pub fn extract_date_from_filename(filename: &str) -> Option().ok()?; let sec = captures.get(6)?.as_str().parse::().ok()?; - match Local.from_local_datetime( - &NaiveDate::from_ymd_opt(year, month, day)?.and_hms_opt(hour, min, sec)?, - ) { - Single(dt) => Some(dt.fixed_offset()), - Ambiguous(early_dt, _) => Some(early_dt.fixed_offset()), - LocalResult::None => { - warn!("Weird local date: {:?}", filename); - None - } - } + let naive = NaiveDate::from_ymd_opt(year, month, day)?.and_hms_opt(hour, min, sec)?; + Some(Utc.from_utc_datetime(&naive).fixed_offset()) }; // 1. Screenshot format: Screenshot_2014-06-01-20-44-50.png