fix(dates): year-gate the filename wall-clock patterns (#160)

match_filename_date only gated the unix-timestamp branch to years
1995..=now+1; the five wall patterns returned any NaiveDateTime. Counters
and IDs shaped like YYYYMMDD_HHMMSS (an app's VID_<counter>_<HHMMSS>,
Facebook's <8 digits>_<id>_o.jpg) became dates of record in years from
267 to 9985: 67 rows on prod, all via screenshot_compact.

Both branches now share plausible_filename_year; a wall miss falls
through to the next pattern and then the rest of the waterfall. The
filename audit no longer proposes these dates.

A data migration clears the five date columns on filename-sourced rows
dated outside [1995, 2028) UTC, so backfill_missing_date_taken
re-resolves them from EXIF / exiftool / fs time on the next ticks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-10-16 23:06:23 -04:00
1 parent f1779358b5
commit 36e4af9fa6
4 files changed
+211 -16

No files matched your search

+94
View File
@@ -474,6 +474,100 @@ mod tests {
assert!(!unidx.changed);
}
#[test]
fn audit_leaves_out_counter_and_id_names_with_implausible_years() {
// #160: these matched screenshot_compact as years 2229 / 8580 /
// 1053 and the audit proposed them as dates of record.
let tmp = tempdir().unwrap();
for path in [
"VID_22290524_080551_818.mp4",
"VID_185800302_235056_842.mp4",
"10530519_10152700326093436_5479865808523667420_o.jpg",
"Screenshot_2014-06-01-20-44-50.png",
] {
fs::write(tmp.path().join(path), b"fixture").unwrap();
}
let mut conn = migrated_conn();
let lib = library(1, "photos", tmp.path(), true);
let stored = stored_dates_for_library(&mut conn, 1).unwrap();
let rows = audit_library(&lib, &[], &stored);
let rels: Vec<&str> = rows.iter().map(|r| r.rel_path.as_str()).collect();
assert_eq!(rels, vec!["Screenshot_2014-06-01-20-44-50.png"]);
}
#[test]
fn migration_clears_implausible_filename_dates_for_re_resolution() {
// Migrate up to just before the #160 data fix, seed rows as the
// old matcher left them, then apply it.
const FIX: &str = "reresolve_implausible_filename_dates";
let mut conn = SqliteConnection::establish(":memory:").expect("in-memory sqlite");
for m in conn.pending_migrations(DB_MIGRATIONS).expect("pending") {
if m.name().to_string().contains(FIX) {
break;
}
conn.run_migration(&*m).expect("migration");
}
// 2229-05-24 08:05:51 and 1053-05-19 10:15:27 (wall-as-UTC), a
// real 2014 filename date, and an fs_time row that isn't ours to
// touch even though its date is odd.
seed_row(
&mut conn,
1,
"VID_22290524_080551_818.mp4",
Some(8_185_651_551),
Some("filename"),
);
seed_row(
&mut conn,
1,
"10530519_1015270_o.jpg",
Some(-28_925_732_673),
Some("filename"),
);
seed_row(
&mut conn,
1,
"Screenshot_2014-06-01-20-44-50.png",
Some(1_401_655_490),
Some("filename"),
);
seed_row(&mut conn, 1, "old_scan.jpg", Some(100), Some("fs_time"));
diesel::update(image_exif::table)
.set((
image_exif::date_utc.eq(image_exif::date_taken),
image_exif::date_taken_tz_source.eq("assumed_server"),
))
.execute(&mut conn)
.unwrap();
conn.run_pending_migrations(DB_MIGRATIONS)
.expect("fix migration");
type Dates = (
String,
Option<i64>,
Option<String>,
Option<i64>,
Option<String>,
);
let rows: Vec<Dates> = image_exif::table
.select((
image_exif::rel_path,
image_exif::date_taken,
image_exif::date_taken_source,
image_exif::date_utc,
image_exif::date_taken_tz_source,
))
.order(image_exif::rel_path)
.load(&mut conn)
.unwrap();
let cleared = |r: &Dates| r.1.is_none() && r.2.is_none() && r.3.is_none() && r.4.is_none();
for r in &rows {
let expect_cleared = r.0.starts_with("VID_") || r.0.starts_with("1053");
assert_eq!(cleared(r), expect_cleared, "{:?}", r);
}
}
#[test]
fn run_audit_skips_disabled_and_offline_libraries() {
let tmp = tempdir().unwrap();