summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-16 18:19:51 +0300
committerPaul Buetow <paul@buetow.org>2026-06-16 18:19:51 +0300
commitfe59a701586972793ea110b465f65d47b825fb8e (patch)
tree87ca3f64b695a0a8fa63d578be1aa4937bb59e01
parentcd7cf797bc633700c5ee7e986077f366c000f0b9 (diff)
in0 split album.source.sh into cohesive modules
Refactor the ~1442-line album.source.sh ([SRP] violation) into three cohesive modules, leaving album.source.sh a thin coordinator. Pure code move, no behavior change -- whole functions and their doc comments moved verbatim; the assembled bin/shuriken contains the same function set. - image-pipeline.source.sh: ImageMagick derivative generation (create_photo_derivatives, create_all_photo_derivatives) and photo asset prep (prepare_generation_photo_assets). - album-metadata.source.sh: EXIF identify caching, exif tooltip/details helpers, file counts, shuriken.json metadata, EXIF cache clearing and the dry-run plan. - album-render.source.sh: preview/view/details/splash/redirect page rendering, the parallel render-job orchestration and the ALBUM_VIEW_PAGE_BY_PHOTO global. - album.source.sh: thin coordinator (generate, refresh_splash, generate_stats_pages, site-asset/archive/html-clean helpers). Add the three modules to LIB_SOURCES (Justfile) and to the SHURIKEN_LIB_SOURCES markers in src/shuriken.sh, ordered after random and before album.source.sh / stats.source.sh. just build, just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--Justfile2
-rwxr-xr-xbin/shuriken1168
-rw-r--r--src/lib/album-metadata.source.sh493
-rw-r--r--src/lib/album-render.source.sh739
-rw-r--r--src/lib/album.source.sh1324
-rw-r--r--src/lib/image-pipeline.source.sh84
-rwxr-xr-xsrc/shuriken.sh6
7 files changed, 1918 insertions, 1898 deletions
diff --git a/Justfile b/Justfile
index f297802..de200c8 100644
--- a/Justfile
+++ b/Justfile
@@ -7,7 +7,7 @@ PREFIX := env_var_or_default("PREFIX", "/usr")
BINDIR := env_var_or_default("BINDIR", PREFIX + "/bin")
DATADIR := env_var_or_default("DATADIR", PREFIX + "/share")
SYSCONFDIR := env_var_or_default("SYSCONFDIR", "/etc/default")
-LIB_SOURCES := "src/lib/bootstrap.source.sh src/lib/imagemagick.source.sh src/lib/process.source.sh src/lib/archive.source.sh src/lib/template.source.sh src/lib/job-pool.source.sh src/lib/image.source.sh src/lib/random.source.sh src/lib/album.source.sh src/lib/stats.source.sh src/lib/config.source.sh src/lib/config.print.source.sh src/lib/config.sync.source.sh src/lib/config.staging.source.sh src/lib/config.validate.source.sh src/lib/config.cli.source.sh src/lib/action.source.sh"
+LIB_SOURCES := "src/lib/bootstrap.source.sh src/lib/imagemagick.source.sh src/lib/process.source.sh src/lib/archive.source.sh src/lib/template.source.sh src/lib/job-pool.source.sh src/lib/image.source.sh src/lib/random.source.sh src/lib/image-pipeline.source.sh src/lib/album-metadata.source.sh src/lib/album-render.source.sh src/lib/album.source.sh src/lib/stats.source.sh src/lib/config.source.sh src/lib/config.print.source.sh src/lib/config.sync.source.sh src/lib/config.staging.source.sh src/lib/config.validate.source.sh src/lib/config.cli.source.sh src/lib/action.source.sh"
default: build
diff --git a/bin/shuriken b/bin/shuriken
index f8aeaf2..729e693 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -1630,7 +1630,588 @@ maybe_shuffle() {
fi
}
-# Inlined from src/lib/album.source.sh
+# Inlined from src/lib/image-pipeline.source.sh
+create_photo_derivatives() {
+ local -r photos_dir="$1"; shift
+ local -r thumbs_dir="$1"; shift
+ local -r blurs_dir="$1"; shift
+ local -r photo="$1"; shift
+ local dirname
+ local height
+
+ if [[ -f "$DIST_DIR/$thumbs_dir/$photo" \
+ && -f "$DIST_DIR/$blurs_dir/$photo" ]]; then
+ log_verbose "Skipped existing thumb and blur" \
+ "$(_display_path "$DIST_DIR/$thumbs_dir/$photo") and" \
+ "$(_display_path "$DIST_DIR/$blurs_dir/$photo")"
+ return
+ fi
+
+ dirname="$DIST_DIR/$thumbs_dir"
+ mkdir -p "$dirname"
+ log_info "Creating thumb $(_display_path "$DIST_DIR/$thumbs_dir/$photo")"
+ # Double the height, as CSS scales images based on boxing too.
+ height=$(( THUMBHEIGHT * 2 ))
+ imagemagick \
+ "$DIST_DIR/$photos_dir/$photo" \
+ -geometry "x$height" \
+ "$DIST_DIR/$thumbs_dir/$photo"
+
+ dirname="$DIST_DIR/$blurs_dir"
+ mkdir -p "$dirname"
+ log_info "Creating blur $(_display_path "$DIST_DIR/$blurs_dir/$photo")"
+ imagemagick \
+ "$DIST_DIR/$thumbs_dir/$photo" \
+ -flip \
+ -blur 0x8 \
+ "$DIST_DIR/$blurs_dir/$photo"
+}
+
+create_all_photo_derivatives() {
+ local -r photos_dir="$1"; shift
+ local -r thumbs_dir="$1"; shift
+ local -r blurs_dir="$1"; shift
+ local -i failed=0
+ local -a image_job_pids=()
+ # Passed by name to wait_for_image_job_slot and wait_for_image_jobs.
+ # shellcheck disable=SC2034
+ local -A image_job_labels=()
+ # Passed by name to wait_for_image_job_slot and wait_for_image_jobs.
+ # shellcheck disable=SC2034
+ local -A image_job_statuses=()
+ local photo
+
+ while IFS= read -r photo; do
+ wait_for_image_job_slot \
+ image_job_pids \
+ image_job_statuses \
+ image_job_labels \
+ failed
+ create_photo_derivatives "$photos_dir" "$thumbs_dir" "$blurs_dir" \
+ "$photo" &
+ image_job_pids+=("$!")
+ # Read through a nameref in the job-pool helpers.
+ # shellcheck disable=SC2034
+ image_job_labels["$!"]="image derivative job for photo $photo"
+ done < <(
+ find "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \
+ | sort
+ )
+
+ wait_for_image_jobs \
+ image_job_pids \
+ image_job_statuses \
+ image_job_labels \
+ failed
+ if (( failed != 0 )); then
+ return 1
+ fi
+}
+
+prepare_generation_photo_assets() {
+ warn_unsupported_incoming_files
+ mkdir -p "$DIST_DIR/photos"
+ cleanphotos
+ scalephotos
+ create_all_photo_derivatives 'photos' 'thumbs' 'blurs'
+}
+
+# Inlined from src/lib/album-metadata.source.sh
+photo_cache_signature() {
+ local -r photo="$1"; shift
+ local -r photo_path="$1"; shift
+ local stat_output
+
+ stat_output=$(stat -c '%s:%Y' "$photo_path")
+ printf '%s:%s\n' "$photo" "$stat_output"
+}
+
+print_cached_photo_identify_output() {
+ local -r cache_file="$1"; shift
+ local line
+ local skipped_signature=no
+
+ while IFS= read -r line || [ -n "$line" ]; do
+ if [ "$skipped_signature" = no ]; then
+ skipped_signature=yes
+ continue
+ fi
+ printf '%s\n' "$line"
+ done < "$cache_file"
+}
+
+cached_photo_identify_output() {
+ local -r photo="$1"; shift
+ local -r photo_path="$1"; shift
+ local cache_dir
+ local cache_file
+ local cached_signature=''
+ local current_signature
+
+ # Persist the EXIF cache in a volatile ./cache directory parallel to ./dist
+ # (the staging dir is a sibling of the final dist, so dirname "$DIST_DIR" is
+ # the working dir in both staging and direct contexts). Keeping it outside
+ # dist means it survives a fresh/cleared dist and is never deployed, so an
+ # unchanged photo skips the slow `identify -verbose` on every regenerate.
+ cache_dir="$(dirname "$DIST_DIR")/cache/exif"
+ cache_file="$cache_dir/$photo.txt"
+ current_signature=$(photo_cache_signature "$photo" "$photo_path")
+
+ # Reuse the cache when its signature still matches the source file. --force
+ # is handled once up front by clear_exif_cache (which empties this directory),
+ # so the first call per photo then rebuilds it and the rest of the run reuses
+ # it -- exactly one identify per photo even under force.
+ if [ -f "$cache_file" ]; then
+ IFS= read -r cached_signature < "$cache_file" || true
+ if [ "$cached_signature" = "$current_signature" ]; then
+ print_cached_photo_identify_output "$cache_file"
+ return
+ fi
+ fi
+
+ mkdir -p "$cache_dir"
+ printf '%s\n' "$current_signature" > "$cache_file"
+ imagemagick_identify -verbose "$photo_path" >> "$cache_file" 2>/dev/null \
+ || true
+ print_cached_photo_identify_output "$cache_file"
+}
+
+photo_exif_details_html() {
+ local -r photo="$1"; shift
+ local -r photo_path="$1"; shift
+ local key
+ local key_html
+ local line
+ local value
+ local value_html
+ local -i exif_count=0
+
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then
+ key="exif:${BASH_REMATCH[1]}"
+ value="${BASH_REMATCH[2]}"
+ key_html=$(_html_escape "$key")
+ value_html=$(_html_escape "$value")
+
+ if (( exif_count == 0 )); then
+ printf '<table class="details">\n'
+ printf '<tbody>\n'
+ fi
+
+ printf '<tr><th>%s</th><td>%s</td></tr>\n' \
+ "$key_html" \
+ "$value_html"
+ (( ++exif_count ))
+ fi
+ done < <(cached_photo_identify_output "$photo" "$photo_path")
+
+ if (( exif_count == 0 )); then
+ printf '<p class="details-empty">No EXIF details available.</p>\n'
+ return
+ fi
+
+ printf '</tbody>\n'
+ printf '</table>\n'
+}
+
+_first_exif_value_to() {
+ local -n output_ref="$1"; shift
+ local -n exif_ref="$1"; shift
+ local key
+
+ output_ref=''
+ for key in "$@"; do
+ if [ -n "${exif_ref[$key]:-}" ]; then
+ # shellcheck disable=SC2034
+ output_ref="${exif_ref[$key]}"
+ return
+ fi
+ done
+}
+
+_photo_exif_values_to() {
+ local -n output_ref="$1"; shift
+ local -r photo="$1"; shift
+ local -r photo_path="$1"; shift
+ local line
+
+ output_ref=()
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then
+ # output_ref writes to the caller-provided associative array.
+ # shellcheck disable=SC2034
+ output_ref["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
+ fi
+ done < <(cached_photo_identify_output "$photo" "$photo_path")
+}
+
+_photo_exif_tooltip_text_from_values() {
+ local -r exif_name="$1"; shift
+ local -n values_ref="$exif_name"
+ local aperture
+ local camera
+ local date_time
+ local iso
+ local key
+ local make
+ local model
+ local separator=''
+ local shutter_speed
+ local -a tooltip_parts=()
+
+ make="${values_ref[Make]:-}"
+ model="${values_ref[Model]:-}"
+ camera="$make"
+ if [ -n "$model" ]; then
+ if [ -n "$make" ]; then
+ case "$model" in
+ "$make"|"$make "*)
+ camera="$model"
+ ;;
+ *)
+ camera="$make $model"
+ ;;
+ esac
+ else
+ camera="$model"
+ fi
+ fi
+
+ _first_exif_value_to aperture "$exif_name" FNumber ApertureValue
+ _first_exif_value_to iso "$exif_name" \
+ ISOSpeedRatings PhotographicSensitivity ISO
+ _first_exif_value_to shutter_speed "$exif_name" \
+ ExposureTime ShutterSpeedValue
+ _first_exif_value_to date_time "$exif_name" \
+ DateTimeOriginal DateTimeDigitized DateTime
+
+ if [ -n "$camera" ]; then
+ tooltip_parts+=("Camera: $camera")
+ fi
+ if [ -n "$aperture" ]; then
+ tooltip_parts+=("Aperture: $aperture")
+ fi
+ if [ -n "$iso" ]; then
+ tooltip_parts+=("ISO: $iso")
+ fi
+ if [ -n "$shutter_speed" ]; then
+ tooltip_parts+=("Shutter speed: $shutter_speed")
+ fi
+ if [ -n "$date_time" ]; then
+ tooltip_parts+=("Taken: $date_time")
+ fi
+
+ for key in "${tooltip_parts[@]}"; do
+ printf '%s%s' "$separator" "$key"
+ separator='; '
+ done
+ if (( ${#tooltip_parts[@]} > 0 )); then
+ printf '\n'
+ fi
+}
+
+photo_exif_tooltip_text() {
+ 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=()
+
+ _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_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 ' "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["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 '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
+}
+
+# Inlined from src/lib/album-render.source.sh
# Maps each album photo filename to its view-page basename ("<page>-<preview>")
# as assigned during render_album_pages. The stats filter mini-albums read this
# so each view page can link "Details" to the album's own details page for the
@@ -1860,210 +2441,6 @@ render_view_page() {
tarball_name "$tarball_name"
}
-photo_cache_signature() {
- local -r photo="$1"; shift
- local -r photo_path="$1"; shift
- local stat_output
-
- stat_output=$(stat -c '%s:%Y' "$photo_path")
- printf '%s:%s\n' "$photo" "$stat_output"
-}
-
-print_cached_photo_identify_output() {
- local -r cache_file="$1"; shift
- local line
- local skipped_signature=no
-
- while IFS= read -r line || [ -n "$line" ]; do
- if [ "$skipped_signature" = no ]; then
- skipped_signature=yes
- continue
- fi
- printf '%s\n' "$line"
- done < "$cache_file"
-}
-
-cached_photo_identify_output() {
- local -r photo="$1"; shift
- local -r photo_path="$1"; shift
- local cache_dir
- local cache_file
- local cached_signature=''
- local current_signature
-
- # Persist the EXIF cache in a volatile ./cache directory parallel to ./dist
- # (the staging dir is a sibling of the final dist, so dirname "$DIST_DIR" is
- # the working dir in both staging and direct contexts). Keeping it outside
- # dist means it survives a fresh/cleared dist and is never deployed, so an
- # unchanged photo skips the slow `identify -verbose` on every regenerate.
- cache_dir="$(dirname "$DIST_DIR")/cache/exif"
- cache_file="$cache_dir/$photo.txt"
- current_signature=$(photo_cache_signature "$photo" "$photo_path")
-
- # Reuse the cache when its signature still matches the source file. --force
- # is handled once up front by clear_exif_cache (which empties this directory),
- # so the first call per photo then rebuilds it and the rest of the run reuses
- # it -- exactly one identify per photo even under force.
- if [ -f "$cache_file" ]; then
- IFS= read -r cached_signature < "$cache_file" || true
- if [ "$cached_signature" = "$current_signature" ]; then
- print_cached_photo_identify_output "$cache_file"
- return
- fi
- fi
-
- mkdir -p "$cache_dir"
- printf '%s\n' "$current_signature" > "$cache_file"
- imagemagick_identify -verbose "$photo_path" >> "$cache_file" 2>/dev/null \
- || true
- print_cached_photo_identify_output "$cache_file"
-}
-
-photo_exif_details_html() {
- local -r photo="$1"; shift
- local -r photo_path="$1"; shift
- local key
- local key_html
- local line
- local value
- local value_html
- local -i exif_count=0
-
- while IFS= read -r line; do
- if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then
- key="exif:${BASH_REMATCH[1]}"
- value="${BASH_REMATCH[2]}"
- key_html=$(_html_escape "$key")
- value_html=$(_html_escape "$value")
-
- if (( exif_count == 0 )); then
- printf '<table class="details">\n'
- printf '<tbody>\n'
- fi
-
- printf '<tr><th>%s</th><td>%s</td></tr>\n' \
- "$key_html" \
- "$value_html"
- (( ++exif_count ))
- fi
- done < <(cached_photo_identify_output "$photo" "$photo_path")
-
- if (( exif_count == 0 )); then
- printf '<p class="details-empty">No EXIF details available.</p>\n'
- return
- fi
-
- printf '</tbody>\n'
- printf '</table>\n'
-}
-
-_first_exif_value_to() {
- local -n output_ref="$1"; shift
- local -n exif_ref="$1"; shift
- local key
-
- output_ref=''
- for key in "$@"; do
- if [ -n "${exif_ref[$key]:-}" ]; then
- # shellcheck disable=SC2034
- output_ref="${exif_ref[$key]}"
- return
- fi
- done
-}
-
-_photo_exif_values_to() {
- local -n output_ref="$1"; shift
- local -r photo="$1"; shift
- local -r photo_path="$1"; shift
- local line
-
- output_ref=()
- while IFS= read -r line; do
- if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then
- # output_ref writes to the caller-provided associative array.
- # shellcheck disable=SC2034
- output_ref["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
- fi
- done < <(cached_photo_identify_output "$photo" "$photo_path")
-}
-
-_photo_exif_tooltip_text_from_values() {
- local -r exif_name="$1"; shift
- local -n values_ref="$exif_name"
- local aperture
- local camera
- local date_time
- local iso
- local key
- local make
- local model
- local separator=''
- local shutter_speed
- local -a tooltip_parts=()
-
- make="${values_ref[Make]:-}"
- model="${values_ref[Model]:-}"
- camera="$make"
- if [ -n "$model" ]; then
- if [ -n "$make" ]; then
- case "$model" in
- "$make"|"$make "*)
- camera="$model"
- ;;
- *)
- camera="$make $model"
- ;;
- esac
- else
- camera="$model"
- fi
- fi
-
- _first_exif_value_to aperture "$exif_name" FNumber ApertureValue
- _first_exif_value_to iso "$exif_name" \
- ISOSpeedRatings PhotographicSensitivity ISO
- _first_exif_value_to shutter_speed "$exif_name" \
- ExposureTime ShutterSpeedValue
- _first_exif_value_to date_time "$exif_name" \
- DateTimeOriginal DateTimeDigitized DateTime
-
- if [ -n "$camera" ]; then
- tooltip_parts+=("Camera: $camera")
- fi
- if [ -n "$aperture" ]; then
- tooltip_parts+=("Aperture: $aperture")
- fi
- if [ -n "$iso" ]; then
- tooltip_parts+=("ISO: $iso")
- fi
- if [ -n "$shutter_speed" ]; then
- tooltip_parts+=("Shutter speed: $shutter_speed")
- fi
- if [ -n "$date_time" ]; then
- tooltip_parts+=("Taken: $date_time")
- fi
-
- for key in "${tooltip_parts[@]}"; do
- printf '%s%s' "$separator" "$key"
- separator='; '
- done
- if (( ${#tooltip_parts[@]} > 0 )); then
- printf '\n'
- fi
-}
-
-photo_exif_tooltip_text() {
- 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=()
-
- _photo_exif_values_to exif_values "$photo" "$photo_path"
- _photo_exif_tooltip_text_from_values exif_values
-}
-
render_details_page() {
local -r html_dir="$1"; shift
local -r photos_dir="$1"; shift
@@ -2107,83 +2484,6 @@ render_details_page() {
tarball_name "$tarball_name"
}
-create_photo_derivatives() {
- local -r photos_dir="$1"; shift
- local -r thumbs_dir="$1"; shift
- local -r blurs_dir="$1"; shift
- local -r photo="$1"; shift
- local dirname
- local height
-
- if [[ -f "$DIST_DIR/$thumbs_dir/$photo" \
- && -f "$DIST_DIR/$blurs_dir/$photo" ]]; then
- log_verbose "Skipped existing thumb and blur" \
- "$(_display_path "$DIST_DIR/$thumbs_dir/$photo") and" \
- "$(_display_path "$DIST_DIR/$blurs_dir/$photo")"
- return
- fi
-
- dirname="$DIST_DIR/$thumbs_dir"
- mkdir -p "$dirname"
- log_info "Creating thumb $(_display_path "$DIST_DIR/$thumbs_dir/$photo")"
- # Double the height, as CSS scales images based on boxing too.
- height=$(( THUMBHEIGHT * 2 ))
- imagemagick \
- "$DIST_DIR/$photos_dir/$photo" \
- -geometry "x$height" \
- "$DIST_DIR/$thumbs_dir/$photo"
-
- dirname="$DIST_DIR/$blurs_dir"
- mkdir -p "$dirname"
- log_info "Creating blur $(_display_path "$DIST_DIR/$blurs_dir/$photo")"
- imagemagick \
- "$DIST_DIR/$thumbs_dir/$photo" \
- -flip \
- -blur 0x8 \
- "$DIST_DIR/$blurs_dir/$photo"
-}
-
-create_all_photo_derivatives() {
- local -r photos_dir="$1"; shift
- local -r thumbs_dir="$1"; shift
- local -r blurs_dir="$1"; shift
- local -i failed=0
- local -a image_job_pids=()
- # Passed by name to wait_for_image_job_slot and wait_for_image_jobs.
- # shellcheck disable=SC2034
- local -A image_job_labels=()
- # Passed by name to wait_for_image_job_slot and wait_for_image_jobs.
- # shellcheck disable=SC2034
- local -A image_job_statuses=()
- local photo
-
- while IFS= read -r photo; do
- wait_for_image_job_slot \
- image_job_pids \
- image_job_statuses \
- image_job_labels \
- failed
- create_photo_derivatives "$photos_dir" "$thumbs_dir" "$blurs_dir" \
- "$photo" &
- image_job_pids+=("$!")
- # Read through a nameref in the job-pool helpers.
- # shellcheck disable=SC2034
- image_job_labels["$!"]="image derivative job for photo $photo"
- done < <(
- find "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \
- | sort
- )
-
- wait_for_image_jobs \
- image_job_pids \
- image_job_statuses \
- image_job_labels \
- failed
- if (( failed != 0 )); then
- return 1
- fi
-}
-
render_photo_view_and_details() {
local -r photos_dir="$1"; shift
local -r blurs_dir="$1"; shift
@@ -2652,164 +2952,13 @@ randomphoto() {
printf '%s\n' "${photos[index]}"
}
-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"]="$