# 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 # low-level metadata primitive consumed by both this album module and the stats # aggregator, so it no longer belongs to album internals. The helpers below call # cached_photo_identify_output through that shared module. 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 '\n' printf '\n' fi printf '\n' \ "$key_html" \ "$value_html" (( ++exif_count )) fi done < <(cached_photo_identify_output "$photo" "$photo_path") if (( exif_count == 0 )); then printf '

No EXIF details available.

\n' return fi printf '\n' printf '
%s%s
\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]:-}" # Dedup the manufacturer prefix via the shared helper (task mn0) so this # tooltip and the stats leaderboard derive identical camera labels. camera=$(camera_label_from_make_model "$make" "$model") _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-%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 }