fix(dates): resolve IFD0 ModifyDate as a last-resort capture date
Scans, edited exports and some downloads carry only EXIF DateTime (exiftool: IFD0:ModifyDate). The kamadak ingest path already takes it as the capture date, but the exiftool resolver didn't ask for it, so the date_utc drain resolved those rows to fs_time and then (rightly) refused to overwrite their tag-sourced legacy date. 147 prod rows sat on the legacy convention forever, re-running exiftool every tick. ModifyDate is now the lowest-priority tag, and only from IFD0 — XMP / QuickTime ModifyDate is an edit or mux time, not capture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d959fc77cf
commit
5c410b09d9
1 file changed
+65
@@ -322,6 +322,13 @@ pub fn resolve_dates_batch(
|
||||
/// timestamps. `ContentCreateDate` is Apple's iOS-set tag; it often
|
||||
/// reflects local capture time on iPhone exports better than the others.
|
||||
///
|
||||
/// `ModifyDate` comes last and only from `IFD0` (see
|
||||
/// [`MODIFY_DATE_GROUP`]): that's EXIF `DateTime`, which the kamadak
|
||||
/// ingest path already takes as the capture date when nothing better
|
||||
/// exists. Scans, edits and some downloads carry only that tag, and
|
||||
/// without it here the drain resolved them to fs_time and then refused
|
||||
/// to overwrite their tag-sourced legacy date — stuck forever.
|
||||
///
|
||||
/// Notably absent: `FileModifyDate` / `FileAccessDate` — those are
|
||||
/// filesystem-derived and the resolver covers them via the `fs_time`
|
||||
/// fallback. Letting exiftool pull them here would mask "no real EXIF
|
||||
@@ -333,8 +340,14 @@ const EXIFTOOL_DATE_TAGS: &[&str] = &[
|
||||
"MediaCreateDate",
|
||||
"TrackCreateDate",
|
||||
"ContentCreateDate",
|
||||
"ModifyDate",
|
||||
];
|
||||
|
||||
/// The only group whose `ModifyDate` counts as a date: `IFD0`, i.e. EXIF
|
||||
/// `DateTime`. XMP / QuickTime `ModifyDate` record when an editor or
|
||||
/// muxer last touched the file, which isn't a capture time.
|
||||
const MODIFY_DATE_GROUP: &str = "IFD0";
|
||||
|
||||
/// Offset + GPS evidence tags requested in the same exiftool pass. They
|
||||
/// feed the camera-offset cascade for naive wall-clock readings that come
|
||||
/// from photo-EXIF groups (ADR-0001).
|
||||
@@ -735,6 +748,9 @@ fn resolve_exiftool_payload(
|
||||
});
|
||||
|
||||
for cand in candidates {
|
||||
if cand.tag == "ModifyDate" && cand.group != MODIFY_DATE_GROUP {
|
||||
continue;
|
||||
}
|
||||
let Some(reading) = parse_exiftool_date_string(&cand.value) else {
|
||||
continue;
|
||||
};
|
||||
@@ -1063,6 +1079,55 @@ mod tests {
|
||||
assert_eq!(resolved.date_utc, Some(1_720_108_800));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ifd0_modify_date_is_a_last_resort() {
|
||||
// Scans / edits with only EXIF DateTime (exiftool: IFD0:ModifyDate)
|
||||
// resolve from it as a camera wall clock, like the kamadak path.
|
||||
let resolved = resolve_exiftool_payload(
|
||||
&tags(&[
|
||||
("IFD0", "ModifyDate", "2009:03:04 14:55:48"),
|
||||
("ExifIFD", "DateTimeOriginal", ""),
|
||||
]),
|
||||
&ExiftoolEvidence {
|
||||
offset_time: Some(-300),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resolved.source, DateSource::Exiftool);
|
||||
assert_eq!(resolved.date_utc, Some(1_236_196_548));
|
||||
assert_eq!(resolved.camera_offset_minutes, Some(-300));
|
||||
|
||||
// Any capture tag outranks it.
|
||||
let resolved = resolve_exiftool_payload(
|
||||
&tags(&[
|
||||
("IFD0", "ModifyDate", "2022:11:08 15:18:20"),
|
||||
("ExifIFD", "CreateDate", "2022:11:01 09:00:00"),
|
||||
]),
|
||||
&ExiftoolEvidence {
|
||||
offset_time: Some(-240),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resolved.date_utc, Some(1_667_307_600));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_exif_modify_date_is_ignored() {
|
||||
// XMP / QuickTime ModifyDate is an edit or mux time, not capture.
|
||||
for group in ["XMP-xmp", "QuickTime"] {
|
||||
assert!(
|
||||
resolve_exiftool_payload(
|
||||
&tags(&[(group, "ModifyDate", "2024:01:01 00:00:00")]),
|
||||
&ExiftoolEvidence::default(),
|
||||
)
|
||||
.is_none(),
|
||||
"{group}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_date_falls_through_to_next_tag() {
|
||||
// exiftool emits "0000:00:00 00:00:00" for missing-but-allocated
|
||||
|
||||
Reference in new issue
Block a user