diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-14 23:07:12 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-14 23:07:12 +0300 |
| commit | b7d5c1102c3679b67532f30f2613e4d2d61c59ba (patch) | |
| tree | 573548dc8762aeb7c642fed804894d280425f84c | |
| parent | 0f0662850ad2df3502241cd1bdfcb366dd68d717 (diff) | |
rm0 wire stats feature into generate with STATS_PAGE toggle
Final integration of the stats-site feature:
- Add STATS_PAGE config (default yes) mirroring SPLASH_PAGE through every
layer: shuriken.default.conf, apply_config_defaults, CLI_OPTION_SPEC
(--stats/--no-stats), the CLI_CONFIG_OVERRIDE_TARGETS allowlist (without
which the flag was silently ignored), usage text, yes/no validation,
print_config, the action config list + effective-setting log, the
generation metadata, and the dry-run plan.
- generate() now calls a gated generate_stats_pages helper after the album
pages (and before archiving, so the stats/camera pages are tarballed):
collect_photo_exif_stats + render_stats_page . . + render_camera_pages . .
- Emit "stats_page" in shuriken.json next to "splash_page".
- Gate the header Stats nav link behind STATS_PAGE via a new
render_stats_page_html config_html field, so it is hidden (no 404) when
stats are disabled.
- Document STATS_PAGE and --stats/--no-stats in the README.
- Tests: full --generate produces stats.html + camera-<slug>.html + the nav
link and stats_page=true; --no-stats suppresses all three and sets
stats_page=false; print-config and metadata expectations updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rw-r--r-- | README.md | 13 | ||||
| -rwxr-xr-x | bin/shuriken | 34 | ||||
| -rw-r--r-- | share/templates/default/header.tmpl | 8 | ||||
| -rw-r--r-- | src/lib/action.source.sh | 2 | ||||
| -rw-r--r-- | src/lib/album.source.sh | 17 | ||||
| -rw-r--r-- | src/lib/bootstrap.source.sh | 2 | ||||
| -rw-r--r-- | src/lib/config.print.source.sh | 1 | ||||
| -rw-r--r-- | src/lib/config.source.sh | 1 | ||||
| -rw-r--r-- | src/lib/config.validate.source.sh | 1 | ||||
| -rw-r--r-- | src/lib/template.source.sh | 7 | ||||
| -rw-r--r-- | src/shuriken.default.conf | 3 | ||||
| -rwxr-xr-x | src/shuriken.sh | 3 | ||||
| -rwxr-xr-x | tests/cli.sh | 118 |
13 files changed, 208 insertions, 2 deletions
@@ -76,7 +76,8 @@ If the file is missing, run `shuriken --init` first. The config file is a Bash file with assignments such as `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, -`TARBALL_INCLUDE`, `TAR_TIMEOUT`, `SYNC_DELETE`, and `SYNC_DESTINATIONS`. +`STATS_PAGE`, `TARBALL_INCLUDE`, `TAR_TIMEOUT`, `SYNC_DELETE`, and +`SYNC_DESTINATIONS`. Before generating, `shuriken` validates the loaded config and command-line overrides. It checks required values, positive integer settings, `yes`/`no` @@ -96,7 +97,7 @@ tarball filename uses `<timestamp>` as a placeholder so the output is stable. `--print-config` writes stable shell-style assignments to stdout in this order: `CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`, -`RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, `TARBALL_INCLUDE`, +`RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, `STATS_PAGE`, `TARBALL_INCLUDE`, `TARBALL_SUFFIX`, `TAR_TIMEOUT`, `TAR_OPTS`, `SYNC_DELETE`, `SYNC_DESTINATIONS`, and `ORIGINAL_BASEPATH`. Scalar values use Bash `%q` quoting and `TAR_OPTS` and `SYNC_DESTINATIONS` are normalized to Bash array @@ -135,6 +136,8 @@ The following long options override config values: | `--no-shuffle` | `SHUFFLE=no` | | `--splash` | `SPLASH_PAGE=yes` | | `--no-splash` | `SPLASH_PAGE=no` | +| `--stats` | `STATS_PAGE=yes` | +| `--no-stats` | `STATS_PAGE=no` | | `--tarball` | `TARBALL_INCLUDE=yes` | | `--no-tarball` | `TARBALL_INCLUDE=no` | | `--sync-delete` | `SYNC_DELETE=yes` | @@ -147,6 +150,12 @@ By default, the generated root `index.html` is a no-JavaScript splash page using a randomly selected album photo. Set `SPLASH_PAGE=no` or pass `--no-splash` to restore the top-level redirect to `page-1.html`. +By default, `shuriken` also generates `stats.html`, a no-JavaScript page with +EXIF-derived insights (camera leaderboard, shooting dates, exposure, dimension, +and format breakdowns) plus one `camera-<slug>.html` per camera linked from the +leaderboard, and shows a `Stats` link in the page header bar. Set `STATS_PAGE=no` +or pass `--no-stats` to skip the stats and per-camera pages and hide the link. + To quickly pick a new random splash photo for an already generated album, run `shuriken --refresh-splash`. This rewrites only `DIST_DIR/index.html` using the existing `photos` and `blurs` output, so it avoids reprocessing images and diff --git a/bin/shuriken b/bin/shuriken index ec8e52b..40f35c2 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -40,6 +40,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=( RANDOM_SEED SHUFFLE SPLASH_PAGE + STATS_PAGE SYNC_DELETE TARBALL_INCLUDE ) @@ -58,6 +59,8 @@ declare -Ar CLI_OPTION_SPEC=( [--no-shuffle]='kind=flag value=no config=SHUFFLE' [--splash]='kind=flag value=yes config=SPLASH_PAGE' [--no-splash]='kind=flag value=no config=SPLASH_PAGE' + [--stats]='kind=flag value=yes config=STATS_PAGE' + [--no-stats]='kind=flag value=no config=STATS_PAGE' [--tarball]='kind=flag value=yes config=TARBALL_INCLUDE' [--no-tarball]='kind=flag value=no config=TARBALL_INCLUDE' [--force]='kind=flag value=yes target=SHURIKEN_FORCE_GENERATE' @@ -107,6 +110,8 @@ usage() { --random-seed VALUE --splash --no-splash + --stats + --no-stats --shuffle --no-shuffle --tarball @@ -629,6 +634,7 @@ declare -ra TEMPLATE_RENDER_FIELD_SPECS=( 'render_redirect_page_html|context_html|redirect_page|redirect_page|redirect' 'render_show_header_bar|context_raw|show_header_bar|show_header_bar|header' 'render_stats_body_html|context_raw|stats_body|stats_body|stats' + 'render_stats_page_html|config_html|STATS_PAGE||' 'render_tarball_include|tarball_include|||' 'render_tarball_name_html|context_html|tarball_name|tarball_name|footer' 'render_thumbheight_html|config_html|THUMBHEIGHT||' @@ -940,6 +946,12 @@ prepare_template_render_vars() { ORIGINAL_BASEPATH) context_value="$ORIGINAL_BASEPATH" ;; + STATS_PAGE) + # Always defaulted by apply_config_defaults; degrade to + # "no" (link hidden) if somehow unset so the header bar + # never references a stats page that was not generated. + context_value="${STATS_PAGE:-no}" + ;; THUMBHEIGHT) # Refresh-only configs do not require generation # sizing fields; render absent values as empty. @@ -2429,6 +2441,7 @@ _collect_generation_metadata() { _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" } @@ -2487,6 +2500,8 @@ _generation_metadata_json() { "$(_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' @@ -2542,6 +2557,16 @@ create_generation_archive() { fi } +# Aggregate EXIF stats and render the stats page plus the per-camera pages into +# the dist root (html_dir and backhref are '.', matching render_album_pages). +# Run after the album pages so the per-photo identify cache is already warm. +generate_stats_pages() { + log_verbose 'Stats page enabled; collecting EXIF stats' + collect_photo_exif_stats + render_stats_page . . + render_camera_pages . . +} + generate() { local tarball_name='' @@ -2558,6 +2583,9 @@ generate() { prepare_generation_site_assets clear_rendered_html render_album_pages 'photos' '.' 'thumbs' 'blurs' '.' "$tarball_name" + if [ "$STATS_PAGE" = yes ]; then + generate_stats_pages + fi create_generation_archive "$tarball_name" write_generation_metadata "$tarball_name" } @@ -2660,6 +2688,7 @@ collect_dry_run_plan() { 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' @@ -3742,6 +3771,7 @@ apply_config_defaults() { RANDOM_SEED="${RANDOM_SEED:-}" SHUFFLE="${SHUFFLE:-no}" SPLASH_PAGE="${SPLASH_PAGE:-yes}" + STATS_PAGE="${STATS_PAGE:-yes}" SYNC_DELETE="${SYNC_DELETE:-yes}" TARBALL_INCLUDE="${TARBALL_INCLUDE:-no}" TARBALL_SUFFIX="${TARBALL_SUFFIX:-.tar}" @@ -3793,6 +3823,7 @@ print_config() { print_shell_assignment RANDOM_SEED "$RANDOM_SEED" print_shell_assignment SHUFFLE "$SHUFFLE" print_shell_assignment SPLASH_PAGE "$SPLASH_PAGE" + print_shell_assignment STATS_PAGE "$STATS_PAGE" print_shell_assignment TARBALL_INCLUDE "$TARBALL_INCLUDE" print_shell_assignment TARBALL_SUFFIX "$TARBALL_SUFFIX" print_shell_assignment TAR_TIMEOUT "$TAR_TIMEOUT" @@ -4248,6 +4279,7 @@ validate_common_config() { validate_positive_integer_config_var TAR_TIMEOUT || return validate_yes_no_config_var SHUFFLE || return validate_yes_no_config_var SPLASH_PAGE || return + validate_yes_no_config_var STATS_PAGE || return validate_yes_no_config_var TARBALL_INCLUDE || return } @@ -4517,6 +4549,7 @@ run_action_body_context() { RANDOM_SEED SHUFFLE SPLASH_PAGE + STATS_PAGE TARBALL_INCLUDE TARBALL_SUFFIX TAR_OPTS @@ -4622,6 +4655,7 @@ log_configured_action() { log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT}s" log_verbose "Effective tar timeout: ${TAR_TIMEOUT}s" log_verbose "Effective splash page setting: $SPLASH_PAGE" + log_verbose "Effective stats page setting: $STATS_PAGE" log_verbose "Effective tarball setting: $TARBALL_INCLUDE" log_verbose "Effective sync delete setting: $SYNC_DELETE" log_verbose "Effective force generation setting: $SHURIKEN_FORCE_GENERATE" diff --git a/share/templates/default/header.tmpl b/share/templates/default/header.tmpl index e0acc6f..6a163f9 100644 --- a/share/templates/default/header.tmpl +++ b/share/templates/default/header.tmpl @@ -276,8 +276,16 @@ cat <<END Site generated at ${render_current_date_text} with <a href="https://codeberg.org/snonux/shuriken.sh">codeberg.org/snonux/shuriken.sh</a> - © by Paul Buetow +END +# The Stats nav link is only shown when the stats page was actually generated +# (STATS_PAGE=yes); otherwise it would point at a missing stats.html. +if [ "${render_stats_page_html}" = 'yes' ]; then +cat <<END - <a href="${render_backhref_html}/stats.html">Stats</a> +END +fi +cat <<END </div> END fi diff --git a/src/lib/action.source.sh b/src/lib/action.source.sh index 987fd7a..e2649d7 100644 --- a/src/lib/action.source.sh +++ b/src/lib/action.source.sh @@ -53,6 +53,7 @@ run_action_body_context() { RANDOM_SEED SHUFFLE SPLASH_PAGE + STATS_PAGE TARBALL_INCLUDE TARBALL_SUFFIX TAR_OPTS @@ -158,6 +159,7 @@ log_configured_action() { log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT}s" log_verbose "Effective tar timeout: ${TAR_TIMEOUT}s" log_verbose "Effective splash page setting: $SPLASH_PAGE" + log_verbose "Effective stats page setting: $STATS_PAGE" log_verbose "Effective tarball setting: $TARBALL_INCLUDE" log_verbose "Effective sync delete setting: $SYNC_DELETE" log_verbose "Effective force generation setting: $SHURIKEN_FORCE_GENERATE" diff --git a/src/lib/album.source.sh b/src/lib/album.source.sh index 2036591..c9812a2 100644 --- a/src/lib/album.source.sh +++ b/src/lib/album.source.sh @@ -927,6 +927,7 @@ _collect_generation_metadata() { _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" } @@ -985,6 +986,8 @@ _generation_metadata_json() { "$(_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' @@ -1040,6 +1043,16 @@ create_generation_archive() { fi } +# Aggregate EXIF stats and render the stats page plus the per-camera pages into +# the dist root (html_dir and backhref are '.', matching render_album_pages). +# Run after the album pages so the per-photo identify cache is already warm. +generate_stats_pages() { + log_verbose 'Stats page enabled; collecting EXIF stats' + collect_photo_exif_stats + render_stats_page . . + render_camera_pages . . +} + generate() { local tarball_name='' @@ -1056,6 +1069,9 @@ generate() { prepare_generation_site_assets clear_rendered_html render_album_pages 'photos' '.' 'thumbs' 'blurs' '.' "$tarball_name" + if [ "$STATS_PAGE" = yes ]; then + generate_stats_pages + fi create_generation_archive "$tarball_name" write_generation_metadata "$tarball_name" } @@ -1158,6 +1174,7 @@ collect_dry_run_plan() { 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' diff --git a/src/lib/bootstrap.source.sh b/src/lib/bootstrap.source.sh index 67ba090..a047f5d 100644 --- a/src/lib/bootstrap.source.sh +++ b/src/lib/bootstrap.source.sh @@ -23,6 +23,8 @@ usage() { --random-seed VALUE --splash --no-splash + --stats + --no-stats --shuffle --no-shuffle --tarball diff --git a/src/lib/config.print.source.sh b/src/lib/config.print.source.sh index bad29ef..7b91b60 100644 --- a/src/lib/config.print.source.sh +++ b/src/lib/config.print.source.sh @@ -36,6 +36,7 @@ print_config() { print_shell_assignment RANDOM_SEED "$RANDOM_SEED" print_shell_assignment SHUFFLE "$SHUFFLE" print_shell_assignment SPLASH_PAGE "$SPLASH_PAGE" + print_shell_assignment STATS_PAGE "$STATS_PAGE" print_shell_assignment TARBALL_INCLUDE "$TARBALL_INCLUDE" print_shell_assignment TARBALL_SUFFIX "$TARBALL_SUFFIX" print_shell_assignment TAR_TIMEOUT "$TAR_TIMEOUT" diff --git a/src/lib/config.source.sh b/src/lib/config.source.sh index 348f167..50c331a 100644 --- a/src/lib/config.source.sh +++ b/src/lib/config.source.sh @@ -37,6 +37,7 @@ apply_config_defaults() { RANDOM_SEED="${RANDOM_SEED:-}" SHUFFLE="${SHUFFLE:-no}" SPLASH_PAGE="${SPLASH_PAGE:-yes}" + STATS_PAGE="${STATS_PAGE:-yes}" SYNC_DELETE="${SYNC_DELETE:-yes}" TARBALL_INCLUDE="${TARBALL_INCLUDE:-no}" TARBALL_SUFFIX="${TARBALL_SUFFIX:-.tar}" diff --git a/src/lib/config.validate.source.sh b/src/lib/config.validate.source.sh index b858144..bd2fccf 100644 --- a/src/lib/config.validate.source.sh +++ b/src/lib/config.validate.source.sh @@ -193,6 +193,7 @@ validate_common_config() { validate_positive_integer_config_var TAR_TIMEOUT || return validate_yes_no_config_var SHUFFLE || return validate_yes_no_config_var SPLASH_PAGE || return + validate_yes_no_config_var STATS_PAGE || return validate_yes_no_config_var TARBALL_INCLUDE || return } diff --git a/src/lib/template.source.sh b/src/lib/template.source.sh index 9fd3ad4..8e5b93e 100644 --- a/src/lib/template.source.sh +++ b/src/lib/template.source.sh @@ -167,6 +167,7 @@ declare -ra TEMPLATE_RENDER_FIELD_SPECS=( 'render_redirect_page_html|context_html|redirect_page|redirect_page|redirect' 'render_show_header_bar|context_raw|show_header_bar|show_header_bar|header' 'render_stats_body_html|context_raw|stats_body|stats_body|stats' + 'render_stats_page_html|config_html|STATS_PAGE||' 'render_tarball_include|tarball_include|||' 'render_tarball_name_html|context_html|tarball_name|tarball_name|footer' 'render_thumbheight_html|config_html|THUMBHEIGHT||' @@ -478,6 +479,12 @@ prepare_template_render_vars() { ORIGINAL_BASEPATH) context_value="$ORIGINAL_BASEPATH" ;; + STATS_PAGE) + # Always defaulted by apply_config_defaults; degrade to + # "no" (link hidden) if somehow unset so the header bar + # never references a stats page that was not generated. + context_value="${STATS_PAGE:-no}" + ;; THUMBHEIGHT) # Refresh-only configs do not require generation # sizing fields; render absent values as empty. diff --git a/src/shuriken.default.conf b/src/shuriken.default.conf index 6c3ea47..c0fe41a 100644 --- a/src/shuriken.default.conf +++ b/src/shuriken.default.conf @@ -15,6 +15,9 @@ IMAGEMAGICK_TIMEOUT=60 # SHUFFLE=yes # Generate a splash landing page at index.html. SPLASH_PAGE=yes +# Generate an EXIF stats page (stats.html) plus per-camera pages, and show the +# Stats navigation link in page headers. Can be yes or no. +STATS_PAGE=yes # Set to any non-empty value for repeatable background, animation, timestamp, # and shuffled preview choices. # RANDOM_SEED=album-build-1 diff --git a/src/shuriken.sh b/src/shuriken.sh index 482484c..b6a6cc5 100755 --- a/src/shuriken.sh +++ b/src/shuriken.sh @@ -40,6 +40,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=( RANDOM_SEED SHUFFLE SPLASH_PAGE + STATS_PAGE SYNC_DELETE TARBALL_INCLUDE ) @@ -58,6 +59,8 @@ declare -Ar CLI_OPTION_SPEC=( [--no-shuffle]='kind=flag value=no config=SHUFFLE' [--splash]='kind=flag value=yes config=SPLASH_PAGE' [--no-splash]='kind=flag value=no config=SPLASH_PAGE' + [--stats]='kind=flag value=yes config=STATS_PAGE' + [--no-stats]='kind=flag value=no config=STATS_PAGE' [--tarball]='kind=flag value=yes config=TARBALL_INCLUDE' [--no-tarball]='kind=flag value=no config=TARBALL_INCLUDE' [--force]='kind=flag value=yes target=SHURIKEN_FORCE_GENERATE' diff --git a/tests/cli.sh b/tests/cli.sh index 50a61f3..6f56dad 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -142,6 +142,7 @@ assert metadata["settings"]["maxpreviews"] == maxpreviews assert metadata["settings"]["image_jobs"] == "3" assert metadata["settings"]["shuffle"] is False assert isinstance(metadata["settings"]["splash_page"], bool) +assert isinstance(metadata["settings"]["stats_page"], bool) assert "original_basepath" in metadata["settings"] PY } @@ -1308,6 +1309,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED='' SHUFFLE=no SPLASH_PAGE=yes +STATS_PAGE=yes TARBALL_INCLUDE=yes TARBALL_SUFFIX=.tar TAR_TIMEOUT=120 @@ -1354,6 +1356,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED='' SHUFFLE=no SPLASH_PAGE=yes +STATS_PAGE=yes TARBALL_INCLUDE=no TARBALL_SUFFIX=.tar TAR_TIMEOUT=120 @@ -1493,6 +1496,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED='' SHUFFLE=yes SPLASH_PAGE=yes +STATS_PAGE=yes TARBALL_INCLUDE=no TARBALL_SUFFIX=.tar TAR_TIMEOUT=120 @@ -1534,6 +1538,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED='' SHUFFLE=no SPLASH_PAGE=yes +STATS_PAGE=yes TARBALL_INCLUDE=no TARBALL_SUFFIX=.tar TAR_TIMEOUT=120 @@ -1600,6 +1605,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED=cli-seed SHUFFLE=yes SPLASH_PAGE=no +STATS_PAGE=yes TARBALL_INCLUDE=yes TARBALL_SUFFIX=.tar TAR_TIMEOUT=120 @@ -3005,6 +3011,112 @@ test_generate_cli_no_splash_overrides_config() { test::teardown } +# Synthetic `identify -verbose` output with EXIF the stats aggregation can parse, +# so a full --generate produces a real camera leaderboard + per-camera page. +test::stats_identify_output() { + printf '%s\n' \ + ' Format: JPEG (Joint Photographic Experts Group JFIF format)' \ + ' Geometry: 160x90+0+0' \ + ' exif:Make: Canon' \ + ' exif:Model: EOS R5' \ + ' exif:FNumber: 28/10' \ + ' exif:ExposureTime: 1/250' \ + ' exif:PhotographicSensitivity: 400' \ + ' exif:FocalLength: 50/1' \ + ' exif:DateTimeOriginal: 2023:07:15 14:30:00' +} + +test_generate_stats_pages_created_and_nav_linked() { + local config_file + local fake_bin + local -i nav_links + + test::setup + fake_bin="$TEST_TMPDIR/bin" + config_file="$TEST_TMPDIR/shuriken.conf" + + test::install_fake_imagemagick "$fake_bin" + PATH="$fake_bin:$PATH" \ + test::generate_fixture_images "$TEST_TMPDIR/incoming" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'Stats album' 40 + + ( + cd "$TEST_TMPDIR" + PATH="$fake_bin:$PATH" \ + TEST_IMAGEMAGICK_IDENTIFY_OUTPUT="$(test::stats_identify_output)" \ + "$TEST_SHURIKEN" --generate --random-seed stats-seed + ) + + # Stats page and the per-camera page (Canon EOS R5 -> canon-eos-r5) exist. + test::assert_file_exists "$TEST_TMPDIR/dist/stats.html" + test::assert_file_exists "$TEST_TMPDIR/dist/camera-canon-eos-r5.html" + test::assert_contains 'Canon EOS R5' "$(<"$TEST_TMPDIR/dist/stats.html")" + test::assert_not_contains '<script' "$(<"$TEST_TMPDIR/dist/stats.html")" + + # The header bar links to the stats page on at least one generated page. + nav_links=$(grep -lF 'stats.html">Stats' "$TEST_TMPDIR"/dist/*.html | wc -l) + test "$nav_links" -gt 0 + + python3 - "$TEST_TMPDIR/dist/shuriken.json" <<'PY' +import json +import pathlib +import sys + +metadata = json.loads(pathlib.Path(sys.argv[1]).read_text()) +assert metadata["settings"]["stats_page"] is True +PY + + test::teardown +} + +test_generate_no_stats_suppresses_pages_and_nav() { + local config_file + local fake_bin + + test::setup + fake_bin="$TEST_TMPDIR/bin" + config_file="$TEST_TMPDIR/shuriken.conf" + + test::install_fake_imagemagick "$fake_bin" + PATH="$fake_bin:$PATH" \ + test::generate_fixture_images "$TEST_TMPDIR/incoming" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'No stats album' 40 + + ( + cd "$TEST_TMPDIR" + PATH="$fake_bin:$PATH" \ + TEST_IMAGEMAGICK_IDENTIFY_OUTPUT="$(test::stats_identify_output)" \ + "$TEST_SHURIKEN" --generate --no-stats --random-seed stats-seed + ) + + # No stats page, no per-camera pages. + test::assert_path_absent "$TEST_TMPDIR/dist/stats.html" + if compgen -G "$TEST_TMPDIR/dist/camera-*.html" >/dev/null; then + printf 'FAIL: --no-stats still produced camera pages\n' >&2 + exit 1 + fi + # No stats nav link anywhere. + if grep -RF 'stats.html">Stats' "$TEST_TMPDIR"/dist/*.html; then + printf 'FAIL: --no-stats still rendered the Stats nav link\n' >&2 + exit 1 + fi + + python3 - "$TEST_TMPDIR/dist/shuriken.json" <<'PY' +import json +import pathlib +import sys + +metadata = json.loads(pathlib.Path(sys.argv[1]).read_text()) +assert metadata["settings"]["stats_page"] is False +PY + + test::teardown +} + test_refresh_splash_rewrites_only_index_from_existing_assets() { local after_index local after_metadata @@ -5464,6 +5576,12 @@ main() { '--generate --no-splash keeps root index redirect' \ test_generate_cli_no_splash_overrides_config test::run_case \ + '--generate creates stats and per-camera pages with nav link' \ + test_generate_stats_pages_created_and_nav_linked + test::run_case \ + '--generate --no-stats suppresses stats pages and nav link' \ + test_generate_no_stats_suppresses_pages_and_nav + test::run_case \ '--refresh-splash rewrites only root index from existing assets' \ test_refresh_splash_rewrites_only_index_from_existing_assets test::run_case \ |
