summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-14 23:07:12 +0300
committerPaul Buetow <paul@buetow.org>2026-06-14 23:07:12 +0300
commitb7d5c1102c3679b67532f30f2613e4d2d61c59ba (patch)
tree573548dc8762aeb7c642fed804894d280425f84c
parent0f0662850ad2df3502241cd1bdfcb366dd68d717 (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.md13
-rwxr-xr-xbin/shuriken34
-rw-r--r--share/templates/default/header.tmpl8
-rw-r--r--src/lib/action.source.sh2
-rw-r--r--src/lib/album.source.sh17
-rw-r--r--src/lib/bootstrap.source.sh2
-rw-r--r--src/lib/config.print.source.sh1
-rw-r--r--src/lib/config.source.sh1
-rw-r--r--src/lib/config.validate.source.sh1
-rw-r--r--src/lib/template.source.sh7
-rw-r--r--src/shuriken.default.conf3
-rwxr-xr-xsrc/shuriken.sh3
-rwxr-xr-xtests/cli.sh118
13 files changed, 208 insertions, 2 deletions
diff --git a/README.md b/README.md
index b1211b6..b7dcf31 100644
--- a/README.md
+++ b/README.md
@@ -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>
-
&copy; 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 \