From 9cb923df9e2059c15de25a51499de167f11bd42e Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 29 Dec 2025 12:28:17 -0500 Subject: [PATCH] Fix memory date priority --- src/memories.rs | 259 +++++++++++++++++++++++++++--------------------- 1 file changed, 148 insertions(+), 111 deletions(-) diff --git a/src/memories.rs b/src/memories.rs index eccf737..c50439b 100644 --- a/src/memories.rs +++ b/src/memories.rs @@ -123,63 +123,6 @@ pub struct MemoriesResponse { pub items: Vec, } -fn get_file_date_info( - path: &Path, - client_timezone: &Option, -) -> Option<(NaiveDate, Option, Option)> { - // Read file metadata once - let meta = std::fs::metadata(path).ok()?; - - // Get created timestamp (tries filename first, then metadata) - let path_str = path.to_str()?; - let created = get_created_timestamp_with_fallback(path_str, &meta, client_timezone); - - // Get modified timestamp from metadata - let modified = meta.modified().ok().map(|t| { - let utc: DateTime = t.into(); - if let Some(tz) = client_timezone { - utc.with_timezone(tz).timestamp() - } else { - utc.timestamp() - } - }); - - // Try to get date from filename for the NaiveDate - if let Some(date_time) = path - .file_name() - .and_then(|filename| filename.to_str()) - .and_then(extract_date_from_filename) - { - // Convert to client timezone if specified - let date_in_timezone = if let Some(tz) = client_timezone { - date_time.with_timezone(tz) - } else { - date_time.with_timezone(&Local).fixed_offset() - }; - - debug!( - "File date from file {:?} > {:?} = {:?}", - path.file_name(), - date_time, - date_in_timezone - ); - return Some((date_in_timezone.date_naive(), created, modified)); - } - - // Fall back to metadata if no date in filename - let system_time = meta.created().ok().or_else(|| meta.modified().ok())?; - let dt_utc: DateTime = system_time.into(); - - let date_in_timezone = if let Some(tz) = client_timezone { - dt_utc.with_timezone(tz).date_naive() - } else { - dt_utc.with_timezone(&Local).date_naive() - }; - - trace!("Fallback metadata create date = {:?}", date_in_timezone); - Some((date_in_timezone, created, modified)) -} - /// Convert Unix timestamp to NaiveDate in client timezone fn timestamp_to_naive_date( timestamp: i64, @@ -196,37 +139,6 @@ fn timestamp_to_naive_date( Some(date) } -/// Get created timestamp, trying filename parsing first, then falling back to metadata -fn get_created_timestamp_with_fallback( - file_path: &str, - metadata: &std::fs::Metadata, - client_timezone: &Option, -) -> Option { - // Try to extract date from filename first - if let Some(filename_date) = Path::new(file_path) - .file_name() - .and_then(|f| f.to_str()) - .and_then(extract_date_from_filename) - { - let timestamp = if let Some(tz) = client_timezone { - filename_date.with_timezone(tz).timestamp() - } else { - filename_date.timestamp() - }; - return Some(timestamp); - } - - // Fall back to metadata - metadata.created().ok().map(|t| { - let utc: DateTime = t.into(); - if let Some(tz) = client_timezone { - utc.with_timezone(tz).timestamp() - } else { - utc.timestamp() - } - }) -} - pub fn extract_date_from_filename(filename: &str) -> Option> { let build_date_from_ymd_capture = |captures: ®ex::Captures| -> Option> { @@ -327,6 +239,99 @@ pub fn extract_date_from_filename(filename: &str) -> Option, + client_timezone: &Option, +) -> Option<(NaiveDate, Option, Option)> { + // Read file metadata once + let meta = std::fs::metadata(path).ok()?; + + // Priority 1: Try to extract date from filename + if let Some(filename_date) = path + .file_name() + .and_then(|f| f.to_str()) + .and_then(extract_date_from_filename) + { + // Convert to client timezone if specified + let date_in_timezone = if let Some(tz) = client_timezone { + filename_date.with_timezone(tz) + } else { + filename_date.with_timezone(&Local).fixed_offset() + }; + + let timestamp = if let Some(tz) = client_timezone { + filename_date.with_timezone(tz).timestamp() + } else { + filename_date.timestamp() + }; + + let modified = meta.modified().ok().map(|t| { + let utc: DateTime = t.into(); + if let Some(tz) = client_timezone { + utc.with_timezone(tz).timestamp() + } else { + utc.timestamp() + } + }); + + debug!( + "Memory date from filename {:?} > {:?} = {:?}", + path.file_name(), + filename_date, + date_in_timezone + ); + return Some((date_in_timezone.date_naive(), Some(timestamp), modified)); + } + + // Priority 2: Use EXIF date_taken if available + if let Some(exif_timestamp) = exif_date_taken { + let date = timestamp_to_naive_date(exif_timestamp, client_timezone)?; + + let modified = meta.modified().ok().map(|t| { + let utc: DateTime = t.into(); + if let Some(tz) = client_timezone { + utc.with_timezone(tz).timestamp() + } else { + utc.timestamp() + } + }); + + debug!("Memory date from EXIF {:?} = {:?}", path.file_name(), date); + return Some((date, Some(exif_timestamp), modified)); + } + + // Priority 3: Fall back to metadata + let system_time = meta.created().ok().or_else(|| meta.modified().ok())?; + let dt_utc: DateTime = system_time.into(); + + let date_in_timezone = if let Some(tz) = client_timezone { + dt_utc.with_timezone(tz).date_naive() + } else { + dt_utc.with_timezone(&Local).date_naive() + }; + + let created_timestamp = if let Some(tz) = client_timezone { + dt_utc.with_timezone(tz).timestamp() + } else { + dt_utc.timestamp() + }; + + let modified = meta.modified().ok().map(|t| { + let utc: DateTime = t.into(); + if let Some(tz) = client_timezone { + utc.with_timezone(tz).timestamp() + } else { + utc.timestamp() + } + }); + + trace!("Fallback metadata create date = {:?}", date_in_timezone); + Some((date_in_timezone, Some(created_timestamp), modified)) +} + /// Collect memories from EXIF database fn collect_exif_memories( exif_dao: &Data>>, @@ -371,27 +376,16 @@ fn collect_exif_memories( return None; } - // Convert timestamp to NaiveDate in client timezone - let file_date = timestamp_to_naive_date(*date_taken_ts, client_timezone)?; + // Get date with priority: filename → EXIF → metadata + // This ensures sorting and display use the same date source + let (file_date, created, modified) = + get_memory_date_with_priority(&full_path, Some(*date_taken_ts), client_timezone)?; // Check if matches memory criteria if !is_memories_match(file_path, file_date, now, span_mode, years_back) { return None; } - // Get file metadata for created/modified timestamps - let metadata = std::fs::metadata(&full_path).ok()?; - let created = - get_created_timestamp_with_fallback(file_path, &metadata, client_timezone); - let modified = metadata.modified().ok().map(|t| { - let utc: DateTime = t.into(); - if let Some(tz) = client_timezone { - utc.with_timezone(tz).timestamp() - } else { - utc.timestamp() - } - }); - Some(( MemoryItem { path: file_path.clone(), @@ -440,8 +434,9 @@ fn collect_filesystem_memories( entries .par_iter() .filter_map(|entry| { - // Use existing get_file_date_info() for filename/metadata fallback - let (file_date, created, modified) = get_file_date_info(entry.path(), client_timezone)?; + // Use unified date priority function (no EXIF for filesystem scan) + let (file_date, created, modified) = + get_memory_date_with_priority(entry.path(), None, client_timezone)?; if is_memories_match( entry.path().to_str().unwrap_or("Unknown"), @@ -793,15 +788,24 @@ mod tests { } #[test] - fn test_get_file_date_info_from_filename() { + fn test_memory_date_priority_filename() { let temp_dir = tempdir().unwrap(); let temp_file = temp_dir.path().join("Screenshot_2014-06-01-20-44-50.png"); File::create(&temp_file).unwrap(); - let (date, created, _) = - get_file_date_info(&temp_file, &Some(*Local::now().fixed_offset().offset())).unwrap(); + // Test that filename takes priority (even with EXIF data available) + let exif_date = DateTime::::from_timestamp(1609459200, 0) // 2021-01-01 + .unwrap() + .timestamp(); - // Check that date is from filename + let (date, created, _) = get_memory_date_with_priority( + &temp_file, + Some(exif_date), + &Some(*Local::now().fixed_offset().offset()), + ) + .unwrap(); + + // Check that date is from filename (2014), NOT EXIF (2021) assert_eq!(date.year(), 2014); assert_eq!(date.month(), 6); assert_eq!(date.day(), 1); @@ -820,12 +824,14 @@ mod tests { } #[test] - fn test_get_file_date_info_from_metadata() { + fn test_memory_date_priority_metadata_fallback() { let temp_dir = tempdir().unwrap(); let temp_file = temp_dir.path().join("regular_image.jpg"); File::create(&temp_file).unwrap(); - let (date, created, modified) = get_file_date_info(&temp_file, &None).unwrap(); + // Test metadata fallback when no filename date or EXIF + let (date, created, modified) = + get_memory_date_with_priority(&temp_file, None, &None).unwrap(); // Both date and timestamps should be from metadata (recent) let today = Local::now().date_naive(); @@ -844,6 +850,37 @@ mod tests { assert_eq!(dt_modified.year(), today.year()); } + #[test] + fn test_memory_date_priority_exif_over_metadata() { + let temp_dir = tempdir().unwrap(); + let temp_file = temp_dir.path().join("regular_image.jpg"); + File::create(&temp_file).unwrap(); + + // Test that EXIF takes priority over metadata (but not filename) + // EXIF date: June 15, 2020 12:00:00 UTC (safe from timezone edge cases) + let exif_date = DateTime::::from_timestamp(1592222400, 0) // 2020-06-15 12:00:00 UTC + .unwrap() + .timestamp(); + + let (date, created, modified) = + get_memory_date_with_priority(&temp_file, Some(exif_date), &None).unwrap(); + + // Date should be from EXIF (2020), not metadata (today) + assert_eq!(date.year(), 2020); + assert_eq!(date.month(), 6); + assert_eq!(date.day(), 15); + + // Created timestamp should also be from EXIF + assert!(created.is_some()); + assert_eq!(created.unwrap(), exif_date); + + // Modified should still be from metadata + assert!(modified.is_some()); + let today = Local::now().date_naive(); + let dt_modified = DateTime::::from_timestamp(modified.unwrap(), 0).unwrap(); + assert_eq!(dt_modified.year(), today.year()); + } + #[test] fn test_path_excluder_absolute_under_base() { let tmp = tempdir().unwrap();