summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-18 16:50:03 +0300
committerPaul Buetow <paul@buetow.org>2026-07-18 16:50:03 +0300
commiteba54dbce3e8410bc2e9da69e7e4a26251573f87 (patch)
tree3db38ffba253afe2311356d2e17aabb4dcbf29a7 /src/lib
parent3a3ae4a4424136ad4a5be636c06fe036711558a2 (diff)
Fix CHRONOLOGICAL_ORDER to reject camera clock-reset EXIF dates
A real album (irregular.ninja/so.war.das) surfaced the bug: a Fujifilm X100V whose battery died reset its clock to 2000-01-01, and every EXIF timestamp on that roll was stamped with the bogus date instead of being omitted. Trusting it at face value sorted that whole roll to the front of the album, ahead of hundreds of correctly-dated 2020s photos. EXIF years before 2001 are now treated the same as a missing date tag. The no-date fallback also switches from source mtime to filename order, since mtime is not reliable either -- bulk-copying/rsyncing an incoming directory commonly rewrites every file's mtime to the transfer time, unrelated to capture order -- while sequential camera filenames track real shooting order even across a clock reset. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/album-photo-select.source.sh55
1 files changed, 33 insertions, 22 deletions
diff --git a/src/lib/album-photo-select.source.sh b/src/lib/album-photo-select.source.sh
index 283e17b..cae4f9f 100644
--- a/src/lib/album-photo-select.source.sh
+++ b/src/lib/album-photo-select.source.sh
@@ -12,7 +12,8 @@
# Main album display order, in precedence order (task 8v0):
# 1. CHRONOLOGICAL_ORDER=yes -> chronological_photo_files (EXIF date taken,
-# ascending, falling back to mtime for photos without one).
+# ascending, falling back to filename order for photos without a usable
+# one).
# 2. otherwise -> the historical maybe_shuffle path (seeded/random SHUFFLE, or
# plain filename sort when SHUFFLE=no).
# CHRONOLOGICAL_ORDER therefore takes precedence over SHUFFLE when both are
@@ -36,6 +37,16 @@ album_photo_files() {
| maybe_shuffle
}
+# Digital cameras were not commercially available before the 1990s, and a
+# long tail of camera bodies -- including the Fujifilm X100V that motivated
+# this constant -- silently default their clock to "2000-01-01" (or similar)
+# once its battery dies, then stamp every EXIF timestamp with that bogus date
+# instead of omitting it. An EXIF year below this is therefore treated as
+# unusable rather than real, so a whole clock-reset camera roll does not sort
+# to the very front of an otherwise correctly-dated album (see
+# chronological_sort_key_for_photo).
+readonly CHRONOLOGICAL_MIN_PLAUSIBLE_YEAR=2001
+
# Build the sort key chronological_photo_files uses to order one photo: a
# tab-separated "<group>\t<time>\t<photo>" line consumed by a plain lexicographic
# sort (see chronological_photo_files). EXIF reads always target the INCOMING_DIR
@@ -43,28 +54,27 @@ album_photo_files() {
# resized DIST_DIR copy, so ordering and tooltip/details agree about a photo's
# taken time and both share the same identify cache entry.
#
-# group 0 when photo_date_taken (album-metadata.source.sh) found a real EXIF
-# date, 1 otherwise. Group 0 always sorts before group 1, so photos
-# with a genuine timestamp are never displaced by an approximate
-# fallback for photos that lack one.
+# group 0 when photo_date_taken (album-metadata.source.sh) found a real,
+# plausible (CHRONOLOGICAL_MIN_PLAUSIBLE_YEAR or later) EXIF date, 1
+# otherwise. Group 0 always sorts before group 1, so photos with a
+# genuine timestamp are never displaced by an approximate fallback.
# time the EXIF date normalized from "YYYY:MM:DD HH:MM:SS" to a 14-digit
# "YYYYMMDDHHMMSS" string (colons/space just stripped -- the calendar
# substrings are untouched, so this stays safe even though the EXIF
# string is not `date -d`-parseable, see docs/stats-exif-audit.md) for
-# group 0, or the INCOMING_DIR file's mtime (compat.source.sh $STAT,
-# zero-padded so it sorts lexicographically) for group 1. Fixed width
-# within each group keeps a plain sort numerically correct.
-# photo final tiebreaker so photos sharing a timestamp (e.g. burst shots) or
-# missing both an EXIF date and a readable mtime still sort in a
-# stable, reproducible order across regenerations of the same
-# incoming set.
+# group 0, or the photo's own filename for group 1 (see the fallback
+# comment below for why filename, not mtime). Fixed width within
+# group 0 keeps a plain sort numerically correct.
+# photo final tiebreaker so photos sharing a timestamp (e.g. burst shots)
+# still sort in a stable, reproducible order across regenerations of
+# the same incoming set.
chronological_sort_key_for_photo() {
local -r photo="$1"; shift
local date_time
- local mtime
date_time=$(photo_date_taken "$photo" "$INCOMING_DIR/$photo")
- if [[ "$date_time" =~ ^([0-9]{4}):([0-9]{2}):([0-9]{2})\ ([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]]; then
+ if [[ "$date_time" =~ ^([0-9]{4}):([0-9]{2}):([0-9]{2})\ ([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]] \
+ && ((10#${BASH_REMATCH[1]} >= CHRONOLOGICAL_MIN_PLAUSIBLE_YEAR)); then
printf '0\t%s%s%s%s%s%s\t%s\n' \
"${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" \
"${BASH_REMATCH[4]}" "${BASH_REMATCH[5]}" "${BASH_REMATCH[6]}" \
@@ -72,14 +82,15 @@ chronological_sort_key_for_photo() {
return
fi
- # No usable EXIF date-taken (missing tag or a malformed value): fall back to
- # the source file's mtime so ordering still reflects "roughly when this
- # photo appeared" rather than an arbitrary readdir order, and stays fully
- # deterministic across runs. A missing/unreadable source file (should not
- # happen; INCOMING_DIR is validated before generation) reads as mtime 0 so
- # this never aborts the render.
- mtime=$("$STAT" -c '%Y' "$INCOMING_DIR/$photo" 2>/dev/null) || mtime=0
- printf '1\t%020d\t%s\n' "$mtime" "$photo"
+ # No usable EXIF date-taken (tag missing, malformed, or an implausible
+ # clock-reset default): fall back to plain filename order rather than
+ # source mtime. mtime looked like a reasonable fallback but is not a
+ # reliable proxy for capture order in practice -- copying/rsyncing an
+ # incoming directory commonly touches every file's mtime to the transfer
+ # time, unrelated to when it was actually shot -- whereas sequential
+ # camera filenames (e.g. Fujifilm's DSCFnnnn shutter-count naming) track
+ # real shooting order even across a clock reset.
+ printf '1\t%s\t%s\n' "$photo" "$photo"
}
# Chronological ordering for CHRONOLOGICAL_ORDER=yes: every photo in