summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-24 10:45:28 +0300
committerPaul Buetow <paul@buetow.org>2026-06-24 10:45:28 +0300
commit6697431d3855d3030b35c2ce232c9ea0e9282ba7 (patch)
tree8e36cce1db02a40ee0954bd552ed43cb4ff2fe21 /src
parent6dfc0f1a9521cc4afe4941fc1328eec014b760a7 (diff)
Split album-metadata.source.sh into focused modules (6r0)
album-metadata.source.sh aggregated six unrelated concerns. Move each along its existing seam (pure mechanical move, no logic changes): - EXIF presentation (photo_exif_details_html, tooltip helpers, the _photo_exif_values_to wrapper) stays in album-metadata.source.sh, which is now EXIF-presentation only. - File counting (count_files, count_incoming_images, count_tree_files) -> image.source.sh, which already owns incoming_image_files; count_incoming_images is a direct wrapper of it. - Tarball naming (tarball_name_plan, generated_tarball_name) -> archive.source.sh, which already owns tarball()/resolve_tar_opts. - Generation metadata + JSON (_collect_generation_metadata, _generation_metadata_json, write_generation_metadata) -> new generation-metadata.source.sh. - Dry-run (dry_run, collect_dry_run_*, print_dry_run_plan) -> new dry-run.source.sh. - clear_exif_cache -> metadata-cache.source.sh, next to the cache primitive cached_photo_identify_output. LIB_SOURCES (Justfile + src/shuriken.sh): insert generation-metadata and dry-run right after album-metadata, before album-render/album. They depend on image, archive, template and metadata-cache (all earlier or runtime-only calls), and are consumed by the album coordinator and the dry-run CLI action, which come later. bin/shuriken regenerated via just build. File-header comments updated to reflect the new homes. just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/lib/album-metadata.source.sh308
-rw-r--r--src/lib/archive.source.sh28
-rw-r--r--src/lib/dry-run.source.sh142
-rw-r--r--src/lib/generation-metadata.source.sh120
-rw-r--r--src/lib/image.source.sh38
-rw-r--r--src/lib/metadata-cache.source.sh13
-rwxr-xr-xsrc/shuriken.sh4
7 files changed, 353 insertions, 300 deletions
diff --git a/src/lib/album-metadata.source.sh b/src/lib/album-metadata.source.sh
index 767ec4c..b96c817 100644
--- a/src/lib/album-metadata.source.sh
+++ b/src/lib/album-metadata.source.sh
@@ -1,3 +1,11 @@
+# Album EXIF presentation: turn a photo's parsed EXIF values into the per-photo
+# details table and hover tooltip the album pages render. This module now holds
+# ONLY the EXIF presentation concern -- the file counting, tarball naming,
+# generation-metadata/JSON, dry-run and cache-lifecycle helpers that used to live
+# here were split out into their own modules (task 6r0): counting -> image, tarball
+# naming -> archive, generation metadata -> generation-metadata, dry-run -> dry-run,
+# clear_exif_cache -> metadata-cache.
+#
# The EXIF identify cache primitive (cached_photo_identify_output and its
# private helpers photo_cache_signature / print_cached_photo_identify_output)
# was promoted to the shared metadata-cache.source.sh module (task pn0): it is a
@@ -145,303 +153,3 @@ photo_exif_tooltip_text() {
_photo_exif_values_to exif_values "$photo" "$photo_path"
_photo_exif_tooltip_text_from_values exif_values
}
-
-count_files() {
- local -r dir="$1"; shift
- local -r name="${1:-}"; shift || true
-
- if [ ! -d "$dir" ]; then
- printf '0\n'
- return
- fi
-
- if [ -n "$name" ]; then
- find "$dir" -maxdepth 1 -type f -name "$name" | wc -l
- else
- find "$dir" -maxdepth 1 -type f | wc -l
- fi
-}
-
-count_incoming_images() {
- incoming_image_files | wc -l
-}
-
-tarball_name_plan() {
- local base
-
- base=$(basename "$INCOMING_DIR")
- printf '%s-<timestamp>%s\n' "$base" "$TARBALL_SUFFIX"
-}
-
-generated_tarball_name() {
- local base
- local timestamp
-
- base=$(basename "$INCOMING_DIR")
- timestamp=$(current_timestamp_slug)
- printf '%s-%s%s\n' "$base" "$timestamp" "$TARBALL_SUFFIX"
-}
-
-count_tree_files() {
- local -r dir="$1"; shift
- local -r name="$1"; shift
-
- if [ ! -d "$dir" ]; then
- printf '0\n'
- return
- fi
-
- find "$dir" -type f -name "$name" | wc -l
-}
-
-_collect_generation_metadata() {
- local -r tarball_file="$1"; shift
-
- declare -gA _GENERATION_METADATA=()
- _GENERATION_METADATA["generator_name"]='shuriken'
- _GENERATION_METADATA["generator_version"]="$VERSION"
- _GENERATION_METADATA["generated_at"]=$(current_timestamp_iso)
- _GENERATION_METADATA["config_source"]="$SHURIKEN_CONFIG_SOURCE"
- _GENERATION_METADATA["template_name"]=$(basename "$TEMPLATE_DIR")
- _GENERATION_METADATA["template_directory"]="$TEMPLATE_DIR"
- _GENERATION_METADATA["source_incoming_dir"]="$INCOMING_DIR"
- _GENERATION_METADATA["source_image_count"]=$(count_incoming_images)
- _GENERATION_METADATA["generated_photo_count"]=$(count_files "$DIST_DIR/photos")
- _GENERATION_METADATA["generated_thumb_count"]=$(count_files "$DIST_DIR/thumbs")
- _GENERATION_METADATA["generated_html_count"]=$(
- count_tree_files "$DIST_DIR" '*.html'
- )
- _GENERATION_METADATA["tarball_included"]="$TARBALL_INCLUDE"
- _GENERATION_METADATA["tarball_file"]="$tarball_file"
- _GENERATION_METADATA["settings_title"]="$TITLE"
- _GENERATION_METADATA["settings_height"]="$HEIGHT"
- _GENERATION_METADATA["settings_thumbheight"]="$THUMBHEIGHT"
- _GENERATION_METADATA["settings_maxpreviews"]="$MAXPREVIEWS"
- _GENERATION_METADATA["settings_subdivide_percent"]="$THUMB_SUBDIVIDE_PERCENT"
- _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_shuffle"]="$SHUFFLE"
- _GENERATION_METADATA["settings_splash_page"]="$SPLASH_PAGE"
- _GENERATION_METADATA["settings_stats_page"]="$STATS_PAGE"
- _GENERATION_METADATA["settings_original_basepath"]="$ORIGINAL_BASEPATH"
-}
-
-_generation_metadata_json() {
- printf '{\n'
- printf ' "generator": {\n'
- printf ' "name": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["generator_name"]}")"
- printf ' "version": %s\n' \
- "$(_json_string "${_GENERATION_METADATA["generator_version"]}")"
- printf ' },\n'
- printf ' "generated_at": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["generated_at"]}")"
- printf ' "config_source": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["config_source"]}")"
- printf ' "template": {\n'
- printf ' "name": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["template_name"]}")"
- printf ' "directory": %s\n' \
- "$(_json_string "${_GENERATION_METADATA["template_directory"]}")"
- printf ' },\n'
- printf ' "source": {\n'
- printf ' "incoming_dir": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["source_incoming_dir"]}")"
- printf ' "image_count": %s\n' \
- "${_GENERATION_METADATA["source_image_count"]}"
- printf ' },\n'
- printf ' "generated": {\n'
- printf ' "photo_count": %s,\n' \
- "${_GENERATION_METADATA["generated_photo_count"]}"
- printf ' "thumb_count": %s,\n' \
- "${_GENERATION_METADATA["generated_thumb_count"]}"
- printf ' "html_count": %s\n' \
- "${_GENERATION_METADATA["generated_html_count"]}"
- printf ' },\n'
- printf ' "tarball": {\n'
- printf ' "included": %s,\n' \
- "$(_json_bool "${_GENERATION_METADATA["tarball_included"]}")"
- printf ' "file": %s\n' \
- "$(_json_string "${_GENERATION_METADATA["tarball_file"]}")"
- printf ' },\n'
- printf ' "settings": {\n'
- printf ' "title": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["settings_title"]}")"
- printf ' "height": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["settings_height"]}")"
- printf ' "thumbheight": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["settings_thumbheight"]}")"
- printf ' "maxpreviews": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["settings_maxpreviews"]}")"
- printf ' "subdivide_percent": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["settings_subdivide_percent"]}")"
- printf ' "feature_percent": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["settings_feature_percent"]}")"
- printf ' "image_jobs": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["settings_image_jobs"]}")"
- printf ' "random_seed": %s,\n' \
- "$(_json_string "${_GENERATION_METADATA["settings_random_seed"]}")"
- printf ' "shuffle": %s,\n' \
- "$(_json_bool "${_GENERATION_METADATA["settings_shuffle"]}")"
- printf ' "splash_page": %s,\n' \
- "$(_json_bool "${_GENERATION_METADATA["settings_splash_page"]}")"
- printf ' "stats_page": %s,\n' \
- "$(_json_bool "${_GENERATION_METADATA["settings_stats_page"]}")"
- printf ' "original_basepath": %s\n' \
- "$(_json_string "${_GENERATION_METADATA["settings_original_basepath"]}")"
- printf ' }\n'
- printf '}\n'
-}
-
-write_generation_metadata() {
- local -r tarball_file="$1"; shift
-
- _collect_generation_metadata "$tarball_file"
- {
- _generation_metadata_json
- } > "$DIST_DIR/shuriken.json"
-}
-
-# Empty the volatile EXIF cache (./cache/exif, parallel to ./dist) so a --force
-# run re-runs `identify` from scratch. Done once up front; the cache then
-# repopulates and is reused for the rest of the run (one identify per photo).
-clear_exif_cache() {
- local -r cache_dir="$(dirname "$DIST_DIR")/cache/exif"
-
- log_verbose "Force generation; clearing EXIF cache $cache_dir"
- rm -rf "$cache_dir"
-}
-
-dry_run() {
- # shellcheck disable=SC2034
- local -A dry_run_plan=()
-
- collect_dry_run_plan dry_run_plan
- print_dry_run_plan dry_run_plan
-}
-
-collect_dry_run_page_plan() {
- local -r plan_name="$1"; shift
- # shellcheck disable=SC2178
- local -n plan_ref="$plan_name"
- local -r image_count="$1"; shift
- local -i page_count=0
- local -i redirect_count=0
-
- plan_ref["html_index_count"]=1
- plan_ref["page_count"]=0
- plan_ref["redirect_count"]=0
- plan_ref["details_count"]=0
-
- if (( image_count > 0 )); then
- page_count=$(( (image_count + MAXPREVIEWS - 1) / MAXPREVIEWS ))
- redirect_count=$(( page_count * 4 + 2 ))
- plan_ref["details_count"]="$image_count"
- plan_ref["page_count"]="$page_count"
- plan_ref["redirect_count"]="$redirect_count"
- fi
-}
-
-collect_dry_run_plan() {
- local -r plan_name="$1"; shift
- # shellcheck disable=SC2178
- local -n plan_ref="$plan_name"
- local -i image_count=0
-
- image_count=$(count_incoming_images)
- plan_ref=()
- plan_ref["config_source"]="$SHURIKEN_CONFIG_SOURCE"
- plan_ref["incoming_dir"]="$INCOMING_DIR"
- plan_ref["dist_dir"]="$DIST_DIR"
- plan_ref["template_dir"]="$TEMPLATE_DIR"
- plan_ref["title"]="$TITLE"
- plan_ref["height"]="$HEIGHT"
- plan_ref["thumbheight"]="$THUMBHEIGHT"
- plan_ref["maxpreviews"]="$MAXPREVIEWS"
- plan_ref["subdivide_percent"]="$THUMB_SUBDIVIDE_PERCENT"
- plan_ref["feature_percent"]="$THUMB_FEATURE_PERCENT"
- plan_ref["image_jobs"]="$IMAGE_JOBS"
- plan_ref["random_seed"]="$RANDOM_SEED"
- plan_ref["shuffle"]="$SHUFFLE"
- plan_ref["splash_page"]="$SPLASH_PAGE"
- plan_ref["stats_page"]="$STATS_PAGE"
- plan_ref["image_count"]="$image_count"
- plan_ref["tarball_include"]="$TARBALL_INCLUDE"
- plan_ref["tarball_name_plan"]='not planned'
-
- if [ "$TARBALL_INCLUDE" = yes ]; then
- plan_ref["tarball_name_plan"]=$(tarball_name_plan)
- fi
-
- collect_dry_run_page_plan "$plan_name" "$image_count"
-}
-
-print_dry_run_plan() {
- local -r plan_name="$1"; shift
- # shellcheck disable=SC2178
- local -n plan_ref="$plan_name"
-
- printf 'Dry run: no files will be written.\n'
- printf 'Config source: %s\n' "${plan_ref["config_source"]}"
- printf 'Incoming directory: %s\n' "${plan_ref["incoming_dir"]}"
- printf 'Output directory: %s\n' "${plan_ref["dist_dir"]}"
- printf 'Template directory: %s\n' "${plan_ref["template_dir"]}"
- printf 'Title: %s\n' "${plan_ref["title"]}"
- printf 'Height: %s\n' "${plan_ref["height"]}"
- printf 'Thumb height: %s\n' "${plan_ref["thumbheight"]}"
- printf 'Max previews per page: %s\n' "${plan_ref["maxpreviews"]}"
- printf 'Subdivide percent: %s\n' "${plan_ref["subdivide_percent"]}"
- 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 'Shuffle: %s\n' "${plan_ref["shuffle"]}"
- printf 'Splash page: %s\n' "${plan_ref["splash_page"]}"
- printf 'Stats page: %s\n' "${plan_ref["stats_page"]}"
- printf 'Image count: %s\n' "${plan_ref["image_count"]}"
- printf 'Tarball setting: %s\n' "${plan_ref["tarball_include"]}"
- printf 'Tarball name plan: %s\n' "${plan_ref["tarball_name_plan"]}"
-
- printf 'Planned directories:\n'
- printf ' %s\n' "${plan_ref["dist_dir"]}"
- printf ' %s/photos\n' "${plan_ref["dist_dir"]}"
- printf ' %s/thumbs\n' "${plan_ref["dist_dir"]}"
- printf ' %s/blurs\n' "${plan_ref["dist_dir"]}"
-
- printf 'Planned generated files:\n'
- if [ "${plan_ref["splash_page"]}" = yes ]; then
- printf ' %s/index.html (%s splash page)\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["html_index_count"]}"
- else
- printf ' %s/index.html (%s album index redirect)\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["html_index_count"]}"
- fi
- printf ' %s/favicon.ico\n' "${plan_ref["dist_dir"]}"
- printf ' %s/shuriken.json\n' "${plan_ref["dist_dir"]}"
- printf ' %s/photos/* (%s image files)\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}"
- printf ' %s/thumbs/* (%s image files)\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}"
- printf ' %s/blurs/* (%s image files)\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}"
- printf ' %s/page-*.html (%s preview pages)\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["page_count"]}"
- printf ' %s/[page]-[image].html (%s view pages)\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}"
- printf ' %s/[page]-[image]-details.html (%s details pages)\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["details_count"]}"
- printf ' %s/[redirect].html (%s navigation redirects)\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["redirect_count"]}"
- if [ "${plan_ref["stats_page"]}" = yes ]; then
- # The exact filter mini-album set needs EXIF aggregation, which dry-run
- # does not perform, so list them as a wildcard under stats/.
- printf ' %s/stats/index.html (EXIF stats page)\n' \
- "${plan_ref["dist_dir"]}"
- printf ' %s/stats/*/ (filter mini-albums)\n' \
- "${plan_ref["dist_dir"]}"
- fi
- if [ "${plan_ref["tarball_include"]}" = yes ]; then
- printf ' %s/%s\n' \
- "${plan_ref["dist_dir"]}" "${plan_ref["tarball_name_plan"]}"
- fi
-}
diff --git a/src/lib/archive.source.sh b/src/lib/archive.source.sh
index b53a782..c27c65c 100644
--- a/src/lib/archive.source.sh
+++ b/src/lib/archive.source.sh
@@ -1,3 +1,31 @@
+# Tarball creation plus the naming helpers that decide what the archive file is
+# called. The naming helpers (tarball_name_plan / generated_tarball_name) were
+# moved here from album-metadata.source.sh (task 6r0): naming the archive is part
+# of this module's tarball concern, alongside tarball() which writes it. The
+# planned-name variant is consumed by the dry-run preview; the generated variant
+# (with a real timestamp slug) is consumed by the album coordinator when it
+# actually creates the archive. Behaviour and signatures are unchanged.
+
+# Print the tarball name as it will appear in the dry-run plan: the incoming
+# directory's basename, a literal "<timestamp>" placeholder, and the suffix.
+tarball_name_plan() {
+ local base
+
+ base=$(basename "$INCOMING_DIR")
+ printf '%s-<timestamp>%s\n' "$base" "$TARBALL_SUFFIX"
+}
+
+# Print the real tarball name used at generation time: the incoming directory's
+# basename, the current timestamp slug, and the configured suffix.
+generated_tarball_name() {
+ local base
+ local timestamp
+
+ base=$(basename "$INCOMING_DIR")
+ timestamp=$(current_timestamp_slug)
+ printf '%s-%s%s\n' "$base" "$timestamp" "$TARBALL_SUFFIX"
+}
+
tarball() {
local -r tarball_name="$1"; shift
local -r tarball_suffix="$TARBALL_SUFFIX"
diff --git a/src/lib/dry-run.source.sh b/src/lib/dry-run.source.sh
new file mode 100644
index 0000000..75a0655
--- /dev/null
+++ b/src/lib/dry-run.source.sh
@@ -0,0 +1,142 @@
+# Dry-run plan: describe what a generation run WOULD produce (directories, page
+# and redirect counts, the planned tarball name) without writing anything. Moved
+# out of album-metadata.source.sh (task 6r0): previewing a run is its own concern,
+# distinct from the EXIF presentation that stays in the album module. The plan
+# depends on count_incoming_images (image.source.sh) for the image tally and
+# tarball_name_plan (archive.source.sh) for the planned archive name; both are
+# sourced before this module and called at runtime, so source order documents the
+# dependency without affecting availability. dry_run is the CLI entry point wired
+# up from action.source.sh. Behaviour and signatures are unchanged by the move.
+
+dry_run() {
+ # shellcheck disable=SC2034
+ local -A dry_run_plan=()
+
+ collect_dry_run_plan dry_run_plan
+ print_dry_run_plan dry_run_plan
+}
+
+collect_dry_run_page_plan() {
+ local -r plan_name="$1"; shift
+ # shellcheck disable=SC2178
+ local -n plan_ref="$plan_name"
+ local -r image_count="$1"; shift
+ local -i page_count=0
+ local -i redirect_count=0
+
+ plan_ref["html_index_count"]=1
+ plan_ref["page_count"]=0
+ plan_ref["redirect_count"]=0
+ plan_ref["details_count"]=0
+
+ if (( image_count > 0 )); then
+ page_count=$(( (image_count + MAXPREVIEWS - 1) / MAXPREVIEWS ))
+ redirect_count=$(( page_count * 4 + 2 ))
+ plan_ref["details_count"]="$image_count"
+ plan_ref["page_count"]="$page_count"
+ plan_ref["redirect_count"]="$redirect_count"
+ fi
+}
+
+collect_dry_run_plan() {
+ local -r plan_name="$1"; shift
+ # shellcheck disable=SC2178
+ local -n plan_ref="$plan_name"
+ local -i image_count=0
+
+ image_count=$(count_incoming_images)
+ plan_ref=()
+ plan_ref["config_source"]="$SHURIKEN_CONFIG_SOURCE"
+ plan_ref["incoming_dir"]="$INCOMING_DIR"
+ plan_ref["dist_dir"]="$DIST_DIR"
+ plan_ref["template_dir"]="$TEMPLATE_DIR"
+ plan_ref["title"]="$TITLE"
+ plan_ref["height"]="$HEIGHT"
+ plan_ref["thumbheight"]="$THUMBHEIGHT"
+ plan_ref["maxpreviews"]="$MAXPREVIEWS"
+ plan_ref["subdivide_percent"]="$THUMB_SUBDIVIDE_PERCENT"
+ plan_ref["feature_percent"]="$THUMB_FEATURE_PERCENT"
+ plan_ref["image_jobs"]="$IMAGE_JOBS"
+ plan_ref["random_seed"]="$RANDOM_SEED"
+ plan_ref["shuffle"]="$SHUFFLE"
+ plan_ref["splash_page"]="$SPLASH_PAGE"
+ plan_ref["stats_page"]="$STATS_PAGE"
+ plan_ref["image_count"]="$image_count"
+ plan_ref["tarball_include"]="$TARBALL_INCLUDE"
+ plan_ref["tarball_name_plan"]='not planned'
+
+ if [ "$TARBALL_INCLUDE" = yes ]; then
+ plan_ref["tarball_name_plan"]=$(tarball_name_plan)
+ fi
+
+ collect_dry_run_page_plan "$plan_name" "$image_count"
+}
+
+print_dry_run_plan() {
+ local -r plan_name="$1"; shift
+ # shellcheck disable=SC2178
+ local -n plan_ref="$plan_name"
+
+ printf 'Dry run: no files will be written.\n'
+ printf 'Config source: %s\n' "${plan_ref["config_source"]}"
+ printf 'Incoming directory: %s\n' "${plan_ref["incoming_dir"]}"
+ printf 'Output directory: %s\n' "${plan_ref["dist_dir"]}"
+ printf 'Template directory: %s\n' "${plan_ref["template_dir"]}"
+ printf 'Title: %s\n' "${plan_ref["title"]}"
+ printf 'Height: %s\n' "${plan_ref["height"]}"
+ printf 'Thumb height: %s\n' "${plan_ref["thumbheight"]}"
+ printf 'Max previews per page: %s\n' "${plan_ref["maxpreviews"]}"
+ printf 'Subdivide percent: %s\n' "${plan_ref["subdivide_percent"]}"
+ 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 'Shuffle: %s\n' "${plan_ref["shuffle"]}"
+ printf 'Splash page: %s\n' "${plan_ref["splash_page"]}"
+ printf 'Stats page: %s\n' "${plan_ref["stats_page"]}"
+ printf 'Image count: %s\n' "${plan_ref["image_count"]}"
+ printf 'Tarball setting: %s\n' "${plan_ref["tarball_include"]}"
+ printf 'Tarball name plan: %s\n' "${plan_ref["tarball_name_plan"]}"
+
+ printf 'Planned directories:\n'
+ printf ' %s\n' "${plan_ref["dist_dir"]}"
+ printf ' %s/photos\n' "${plan_ref["dist_dir"]}"
+ printf ' %s/thumbs\n' "${plan_ref["dist_dir"]}"
+ printf ' %s/blurs\n' "${plan_ref["dist_dir"]}"
+
+ printf 'Planned generated files:\n'
+ if [ "${plan_ref["splash_page"]}" = yes ]; then
+ printf ' %s/index.html (%s splash page)\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["html_index_count"]}"
+ else
+ printf ' %s/index.html (%s album index redirect)\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["html_index_count"]}"
+ fi
+ printf ' %s/favicon.ico\n' "${plan_ref["dist_dir"]}"
+ printf ' %s/shuriken.json\n' "${plan_ref["dist_dir"]}"
+ printf ' %s/photos/* (%s image files)\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}"
+ printf ' %s/thumbs/* (%s image files)\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}"
+ printf ' %s/blurs/* (%s image files)\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}"
+ printf ' %s/page-*.html (%s preview pages)\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["page_count"]}"
+ printf ' %s/[page]-[image].html (%s view pages)\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}"
+ printf ' %s/[page]-[image]-details.html (%s details pages)\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["details_count"]}"
+ printf ' %s/[redirect].html (%s navigation redirects)\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["redirect_count"]}"
+ if [ "${plan_ref["stats_page"]}" = yes ]; then
+ # The exact filter mini-album set needs EXIF aggregation, which dry-run
+ # does not perform, so list them as a wildcard under stats/.
+ printf ' %s/stats/index.html (EXIF stats page)\n' \
+ "${plan_ref["dist_dir"]}"
+ printf ' %s/stats/*/ (filter mini-albums)\n' \
+ "${plan_ref["dist_dir"]}"
+ fi
+ if [ "${plan_ref["tarball_include"]}" = yes ]; then
+ printf ' %s/%s\n' \
+ "${plan_ref["dist_dir"]}" "${plan_ref["tarball_name_plan"]}"
+ fi
+}
diff --git a/src/lib/generation-metadata.source.sh b/src/lib/generation-metadata.source.sh
new file mode 100644
index 0000000..b75af1b
--- /dev/null
+++ b/src/lib/generation-metadata.source.sh
@@ -0,0 +1,120 @@
+# Generation metadata: collect a snapshot of the run (generator version, source
+# and generated file counts, effective settings) and serialise it to the
+# dist/shuriken.json sidecar. Moved out of album-metadata.source.sh (task 6r0):
+# describing the generation run is its own concern, distinct from the EXIF
+# presentation that stays in the album module. The collector depends on the file
+# counters (count_files / count_incoming_images / count_tree_files, now in
+# image.source.sh), current_timestamp_iso (template.source.sh) and the JSON
+# helpers (_json_string / _json_bool, template.source.sh); all are sourced before
+# this module, and these are runtime calls anyway, so source order documents the
+# dependency without affecting availability. Behaviour and signatures are
+# unchanged by the move.
+
+_collect_generation_metadata() {
+ local -r tarball_file="$1"; shift
+
+ declare -gA _GENERATION_METADATA=()
+ _GENERATION_METADATA["generator_name"]='shuriken'
+ _GENERATION_METADATA["generator_version"]="$VERSION"
+ _GENERATION_METADATA["generated_at"]=$(current_timestamp_iso)
+ _GENERATION_METADATA["config_source"]="$SHURIKEN_CONFIG_SOURCE"
+ _GENERATION_METADATA["template_name"]=$(basename "$TEMPLATE_DIR")
+ _GENERATION_METADATA["template_directory"]="$TEMPLATE_DIR"
+ _GENERATION_METADATA["source_incoming_dir"]="$INCOMING_DIR"
+ _GENERATION_METADATA["source_image_count"]=$(count_incoming_images)
+ _GENERATION_METADATA["generated_photo_count"]=$(count_files "$DIST_DIR/photos")
+ _GENERATION_METADATA["generated_thumb_count"]=$(count_files "$DIST_DIR/thumbs")
+ _GENERATION_METADATA["generated_html_count"]=$(
+ count_tree_files "$DIST_DIR" '*.html'
+ )
+ _GENERATION_METADATA["tarball_included"]="$TARBALL_INCLUDE"
+ _GENERATION_METADATA["tarball_file"]="$tarball_file"
+ _GENERATION_METADATA["settings_title"]="$TITLE"
+ _GENERATION_METADATA["settings_height"]="$HEIGHT"
+ _GENERATION_METADATA["settings_thumbheight"]="$THUMBHEIGHT"
+ _GENERATION_METADATA["settings_maxpreviews"]="$MAXPREVIEWS"
+ _GENERATION_METADATA["settings_subdivide_percent"]="$THUMB_SUBDIVIDE_PERCENT"
+ _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_shuffle"]="$SHUFFLE"
+ _GENERATION_METADATA["settings_splash_page"]="$SPLASH_PAGE"
+ _GENERATION_METADATA["settings_stats_page"]="$STATS_PAGE"
+ _GENERATION_METADATA["settings_original_basepath"]="$ORIGINAL_BASEPATH"
+}
+
+_generation_metadata_json() {
+ printf '{\n'
+ printf ' "generator": {\n'
+ printf ' "name": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["generator_name"]}")"
+ printf ' "version": %s\n' \
+ "$(_json_string "${_GENERATION_METADATA["generator_version"]}")"
+ printf ' },\n'
+ printf ' "generated_at": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["generated_at"]}")"
+ printf ' "config_source": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["config_source"]}")"
+ printf ' "template": {\n'
+ printf ' "name": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["template_name"]}")"
+ printf ' "directory": %s\n' \
+ "$(_json_string "${_GENERATION_METADATA["template_directory"]}")"
+ printf ' },\n'
+ printf ' "source": {\n'
+ printf ' "incoming_dir": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["source_incoming_dir"]}")"
+ printf ' "image_count": %s\n' \
+ "${_GENERATION_METADATA["source_image_count"]}"
+ printf ' },\n'
+ printf ' "generated": {\n'
+ printf ' "photo_count": %s,\n' \
+ "${_GENERATION_METADATA["generated_photo_count"]}"
+ printf ' "thumb_count": %s,\n' \
+ "${_GENERATION_METADATA["generated_thumb_count"]}"
+ printf ' "html_count": %s\n' \
+ "${_GENERATION_METADATA["generated_html_count"]}"
+ printf ' },\n'
+ printf ' "tarball": {\n'
+ printf ' "included": %s,\n' \
+ "$(_json_bool "${_GENERATION_METADATA["tarball_included"]}")"
+ printf ' "file": %s\n' \
+ "$(_json_string "${_GENERATION_METADATA["tarball_file"]}")"
+ printf ' },\n'
+ printf ' "settings": {\n'
+ printf ' "title": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_title"]}")"
+ printf ' "height": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_height"]}")"
+ printf ' "thumbheight": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_thumbheight"]}")"
+ printf ' "maxpreviews": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_maxpreviews"]}")"
+ printf ' "subdivide_percent": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_subdivide_percent"]}")"
+ printf ' "feature_percent": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_feature_percent"]}")"
+ printf ' "image_jobs": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_image_jobs"]}")"
+ printf ' "random_seed": %s,\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_random_seed"]}")"
+ printf ' "shuffle": %s,\n' \
+ "$(_json_bool "${_GENERATION_METADATA["settings_shuffle"]}")"
+ printf ' "splash_page": %s,\n' \
+ "$(_json_bool "${_GENERATION_METADATA["settings_splash_page"]}")"
+ printf ' "stats_page": %s,\n' \
+ "$(_json_bool "${_GENERATION_METADATA["settings_stats_page"]}")"
+ printf ' "original_basepath": %s\n' \
+ "$(_json_string "${_GENERATION_METADATA["settings_original_basepath"]}")"
+ printf ' }\n'
+ printf '}\n'
+}
+
+write_generation_metadata() {
+ local -r tarball_file="$1"; shift
+
+ _collect_generation_metadata "$tarball_file"
+ {
+ _generation_metadata_json
+ } > "$DIST_DIR/shuriken.json"
+}
diff --git a/src/lib/image.source.sh b/src/lib/image.source.sh
index f7cb715..2581228 100644
--- a/src/lib/image.source.sh
+++ b/src/lib/image.source.sh
@@ -53,6 +53,44 @@ incoming_image_files() {
| sort
}
+# File counters for the source tree and the generated dist. Moved here from
+# album-metadata.source.sh (task 6r0): counting incoming/generated files is part
+# of this module's file-enumeration concern -- count_incoming_images is a direct
+# wrapper of incoming_image_files above, and count_files/count_tree_files are the
+# generic siblings the generation-metadata and dry-run modules use to tally the
+# produced photos, thumbs and HTML pages. Behaviour and signatures are unchanged.
+count_files() {
+ local -r dir="$1"; shift
+ local -r name="${1:-}"; shift || true
+
+ if [ ! -d "$dir" ]; then
+ printf '0\n'
+ return
+ fi
+
+ if [ -n "$name" ]; then
+ find "$dir" -maxdepth 1 -type f -name "$name" | wc -l
+ else
+ find "$dir" -maxdepth 1 -type f | wc -l
+ fi
+}
+
+count_incoming_images() {
+ incoming_image_files | wc -l
+}
+
+count_tree_files() {
+ local -r dir="$1"; shift
+ local -r name="$1"; shift
+
+ if [ ! -d "$dir" ]; then
+ printf '0\n'
+ return
+ fi
+
+ find "$dir" -type f -name "$name" | wc -l
+}
+
warn_unsupported_incoming_files() {
local file
diff --git a/src/lib/metadata-cache.source.sh b/src/lib/metadata-cache.source.sh
index ba40ca9..7c6d2e3 100644
--- a/src/lib/metadata-cache.source.sh
+++ b/src/lib/metadata-cache.source.sh
@@ -140,3 +140,16 @@ photo_exif_values_to() {
fi
done
}
+
+# Empty the volatile EXIF cache (./cache/exif, parallel to ./dist) so a --force
+# run re-runs `identify` from scratch. Done once up front; the cache then
+# repopulates and is reused for the rest of the run (one identify per photo).
+# Moved here from album-metadata.source.sh (task 6r0): clearing the cache is the
+# lifecycle counterpart of cached_photo_identify_output above, so the cache's
+# creation and destruction now live in the same module.
+clear_exif_cache() {
+ local -r cache_dir="$(dirname "$DIST_DIR")/cache/exif"
+
+ log_verbose "Force generation; clearing EXIF cache $cache_dir"
+ rm -rf "$cache_dir"
+}
diff --git a/src/shuriken.sh b/src/shuriken.sh
index 418dfed..6f77e3c 100755
--- a/src/shuriken.sh
+++ b/src/shuriken.sh
@@ -115,6 +115,10 @@ source "$SHURIKEN_SOURCE_DIR/lib/random.source.sh"
source "$SHURIKEN_SOURCE_DIR/lib/image-pipeline.source.sh"
# shellcheck source=src/lib/album-metadata.source.sh
source "$SHURIKEN_SOURCE_DIR/lib/album-metadata.source.sh"
+# shellcheck source=src/lib/generation-metadata.source.sh
+source "$SHURIKEN_SOURCE_DIR/lib/generation-metadata.source.sh"
+# shellcheck source=src/lib/dry-run.source.sh
+source "$SHURIKEN_SOURCE_DIR/lib/dry-run.source.sh"
# shellcheck source=src/lib/album-render.source.sh
source "$SHURIKEN_SOURCE_DIR/lib/album-render.source.sh"
# shellcheck source=src/lib/album.source.sh