From 3a3ae4a4424136ad4a5be636c06fe036711558a2 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 18 Jul 2026 16:22:22 +0300 Subject: Add CHRONOLOGICAL_ORDER config option to order albums by EXIF date taken Adds a new yes/no config setting (default no, preserving current behavior) that orders the main album's photos by EXIF date taken (ascending) instead of the default filename/shuffle order. Reuses the existing EXIF cache and tag fallback chain (DateTimeOriginal -> DateTimeDigitized -> DateTime) already used for tooltips/details/stats, so ordering never disagrees with what those features show. Photos with no usable EXIF date fall back to their source file's mtime, staying fully deterministic and crash-free. CHRONOLOGICAL_ORDER takes precedence over SHUFFLE when both are enabled, documented in album-photo-select.source.sh and docs/configuration.md. Wired through the config registry (CONFIG_SPECS), validation, CLI flags (--chronological/--no-chronological), --print-config, --dry-run, --verbose logging, and shuriken.json generation metadata. Adds unit and end-to-end tests covering default-off behavior, EXIF-date ordering with shuffle precedence, and mtime fallback for EXIF-less photos. Co-Authored-By: Claude Sonnet 5 --- README.md | 8 +- bin/shuriken | 125 ++++++++++++++++++++-- docs/configuration.md | 35 ++++-- docs/generation.md | 12 ++- docs/usage.md | 2 + src/lib/action.source.sh | 1 + src/lib/album-metadata.source.sh | 23 ++++ src/lib/album-photo-select.source.sh | 90 ++++++++++++++-- src/lib/bootstrap.source.sh | 2 + src/lib/config.spec.source.sh | 1 + src/lib/config.validate.source.sh | 1 + src/lib/dry-run.source.sh | 2 + src/lib/generation-metadata.source.sh | 3 + src/shuriken.default.conf | 11 +- src/shuriken.sh | 2 + tests/cli.sh | 196 ++++++++++++++++++++++++++++++++++ 16 files changed, 491 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 055e3e3..c8c71b6 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,8 @@ on Linux the default tools already are GNU, on macOS/FreeBSD install the Common per-run overrides (see the full reference table in [docs/usage.md](docs/usage.md)): `--incoming`, `--dist`, `--template`, `--title`, `--height`, `--thumbheight`, -`--maxpreviews`, `--image-jobs`, `--random-seed`, `--shuffle`/`--no-shuffle`, +`--maxpreviews`, `--image-jobs`, `--random-seed`, +`--chronological`/`--no-chronological`, `--shuffle`/`--no-shuffle`, `--splash`/`--no-splash`, `--details`/`--no-details`, `--stats`/`--no-stats`, `--tarball`/`--no-tarball`, `--favicon`, `--source-url`, `--sync-destination`, `--sync-delete`/`--no-sync-delete`, `--quiet`, `--verbose`. @@ -120,6 +121,11 @@ Feature toggles at a glance: * **Reproducible builds**: set `RANDOM_SEED` (or `--random-seed VALUE`) to make splash/background picks, animation classes, timestamps, and shuffle order repeatable. +* **Chronological order** (`CHRONOLOGICAL_ORDER=no`, the default): set to + `yes` (or pass `--chronological`) to order the main album's photos by EXIF + date taken instead of filename/shuffle order, falling back to source mtime + for photos with no usable EXIF date. Takes precedence over `SHUFFLE` when + both are enabled. ## Documentation diff --git a/bin/shuriken b/bin/shuriken index 5b8f974..fd7eda1 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -51,6 +51,8 @@ declare -Ar CLI_OPTION_SPEC=( [--feature]='kind=value config=THUMB_FEATURE_PERCENT' [--image-jobs]='kind=value config=IMAGE_JOBS' [--random-seed]='kind=value config=RANDOM_SEED' + [--chronological]='kind=flag value=yes config=CHRONOLOGICAL_ORDER' + [--no-chronological]='kind=flag value=no config=CHRONOLOGICAL_ORDER' [--shuffle]='kind=flag value=yes config=SHUFFLE' [--no-shuffle]='kind=flag value=no config=SHUFFLE' [--splash]='kind=flag value=yes config=SPLASH_PAGE' @@ -294,6 +296,8 @@ usage() { --feature PERCENT --image-jobs N --random-seed VALUE + --chronological + --no-chronological --splash --no-splash --details @@ -2573,6 +2577,29 @@ photo_exif_tooltip_text() { _photo_exif_tooltip_text_from_values exif_values } +# A photo's EXIF date-taken, in the raw "YYYY:MM:DD HH:MM:SS" EXIF format. +# Consumed by CHRONOLOGICAL_ORDER (album-photo-select.source.sh, +# chronological_sort_key_for_photo) so ordering by "date taken" uses exactly the +# same tag fallback chain (DateTimeOriginal -> DateTimeDigitized -> DateTime) as +# the tooltip's "Taken:" field above -- one photo can't disagree with itself +# about when it was taken depending on which feature asks. Empty when none of +# the three tags is present (e.g. screenshots, downloaded images); callers +# needing a value in that case supply their own deterministic fallback rather +# than guessing a date here. +photo_date_taken() { + local -r photo="$1"; shift + local -r photo_path="$1"; shift + # exif_values is populated and read through nameref helpers. + # shellcheck disable=SC2034 + local -A exif_values=() + local date_time + + _photo_exif_values_to exif_values "$photo" "$photo_path" + _first_exif_value_to date_time exif_values \ + DateTimeOriginal DateTimeDigitized DateTime + printf '%s\n' "$date_time" +} + # Inlined from src/lib/generation-metadata.source.sh # Generation metadata: collect a snapshot of the run (generator version, source # and generated file counts, effective settings) and serialise it to the @@ -2618,6 +2645,7 @@ _collect_generation_metadata() { _GENERATION_METADATA["settings_feature_percent"]="$THUMB_FEATURE_PERCENT" _GENERATION_METADATA["settings_image_jobs"]="$IMAGE_JOBS" _GENERATION_METADATA["settings_random_seed"]="$RANDOM_SEED" + _GENERATION_METADATA["settings_chronological_order"]="$CHRONOLOGICAL_ORDER" _GENERATION_METADATA["settings_shuffle"]="$SHUFFLE" _GENERATION_METADATA["settings_splash_page"]="$SPLASH_PAGE" _GENERATION_METADATA["settings_details_page"]="$DETAILS_PAGE" @@ -2688,6 +2716,8 @@ _generation_metadata_json_settings() { "$(json_string "${_GENERATION_METADATA["settings_image_jobs"]}")" printf ' "random_seed": %s,\n' \ "$(json_string "${_GENERATION_METADATA["settings_random_seed"]}")" + printf ' "chronological_order": %s,\n' \ + "$(json_bool "${_GENERATION_METADATA["settings_chronological_order"]}")" printf ' "shuffle": %s,\n' \ "$(json_bool "${_GENERATION_METADATA["settings_shuffle"]}")" printf ' "splash_page": %s,\n' \ @@ -2810,6 +2840,7 @@ collect_dry_run_plan() { plan_ref["feature_percent"]="$THUMB_FEATURE_PERCENT" plan_ref["image_jobs"]="$IMAGE_JOBS" plan_ref["random_seed"]="$RANDOM_SEED" + plan_ref["chronological_order"]="$CHRONOLOGICAL_ORDER" plan_ref["shuffle"]="$SHUFFLE" plan_ref["splash_page"]="$SPLASH_PAGE" plan_ref["details_page"]="$DETAILS_PAGE" @@ -2845,6 +2876,7 @@ _print_dry_run_settings() { printf 'Feature percent: %s\n' "${plan_ref["feature_percent"]}" printf 'Image jobs: %s\n' "${plan_ref["image_jobs"]}" printf 'Random seed: %s\n' "${plan_ref["random_seed"]}" + printf 'Chronological order: %s\n' "${plan_ref["chronological_order"]}" printf 'Shuffle: %s\n' "${plan_ref["shuffle"]}" printf 'Splash page: %s\n' "${plan_ref["splash_page"]}" printf 'Details page: %s\n' "${plan_ref["details_page"]}" @@ -3498,24 +3530,102 @@ build_preview_thumbnail() { # album-render.source.sh (task ar0) so the "which photos are in this album, in # what order, and which one do we pick for a background/splash" concern lives # apart from the page orchestration, the tile-layout deciders and the thumbnail -# HTML. This is selection POLICY (shuffle/sort, splash-requires-a-blur, seeded -# random pick) and changes for different reasons than the rendering plumbing. +# HTML. This is selection POLICY (shuffle/sort/chronological, splash-requires-a- +# blur, seeded random pick) and changes for different reasons than the +# rendering plumbing. # # These helpers are called by the orchestrator (album-render.source.sh) and by # the per-page render jobs at runtime; all libs are sourced before any code runs, # so availability does not depend on source order. -# Unlike the other photo listings this one keeps its own find rather than using -# list_photos (photo-list.source.sh): it pipes through maybe_shuffle, not sort, -# because the album's display order is the configurable (seeded) shuffle, not a -# plain sort. Uses $FIND (compat.source.sh) since -printf is a GNU-only action. +# 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). +# 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 +# set: a chronological album is meant to read as a timeline, so an enabled +# shuffle must not silently re-scramble it. This is deliberately a config-level +# choice rather than an error, so flipping SHUFFLE on/off (e.g. via the CLI +# flags) while experimenting does not require also touching +# CHRONOLOGICAL_ORDER. Unlike the other photo listings this one keeps its own +# find rather than using list_photos (photo-list.source.sh): both order modes +# need the raw filename list before applying their own ordering, not a plain +# sort. Uses $FIND (compat.source.sh) since -printf is a GNU-only action. album_photo_files() { local -r photos_dir="$1"; shift + if [ "$CHRONOLOGICAL_ORDER" = yes ]; then + chronological_photo_files "$photos_dir" + return + fi + "$FIND" "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \ | maybe_shuffle } +# Build the sort key chronological_photo_files uses to order one photo: a +# tab-separated "\t