summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-15 18:48:49 +0300
committerPaul Buetow <paul@buetow.org>2026-06-15 23:50:41 +0300
commite7aeb1f998aef664d3fc6cc21dc8c3b6b7579d01 (patch)
tree08ba293284c3ad5293fb2252407f83b5ddfebeb0
parent66a39be5271393b95cddae1bd61206eea8427934 (diff)
Move the stats site into a stats/ subdir to keep the album root small
The filter mini-albums put thousands of HTML files directly in the album root. Reorganise so only the main album lives in DIST_DIR and all stats content goes under stats/: stats/index.html - the stats overview (was stats.html) stats/<pagebase>/index.html - each filter gallery (was <pagebase>.html) stats/<pagebase>/<index>.html - each filter view page (was <pagebase>--N.html) Each mini-album is its own directory, so no single directory holds more than one filter's pages. Links updated accordingly: the overview links to <pagebase>/, the gallery thumbnails link to sibling <index>.html, view prev/next/gallery are same-dir, and assets/album-details/back-to-stats resolve via the ../.. backhref. The header Stats link and camera.tmpl back link point at stats/index.html. Mini-albums still reuse the shared photos/thumbs/blurs assets and render in parallel. Tests and the dry-run plan updated for the new layout; README documents the stats/ subdirectory structure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--README.md18
-rwxr-xr-xbin/shuriken104
-rw-r--r--share/templates/default/camera.tmpl2
-rw-r--r--share/templates/default/header.tmpl2
-rw-r--r--src/lib/album.source.sh15
-rw-r--r--src/lib/stats.source.sh89
-rwxr-xr-xtests/cli.sh166
7 files changed, 215 insertions, 181 deletions
diff --git a/README.md b/README.md
index 6095e57..219d051 100644
--- a/README.md
+++ b/README.md
@@ -163,11 +163,21 @@ 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
+By default, `shuriken` also generates a no-JavaScript stats site 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.
+format, and decoded-enum breakdowns), reachable from the `Stats` link in the page
+header bar. Every row on the stats overview is clickable: each bucket (each
+camera, ISO, year, aperture, orientation, …) is its own filter "mini-album" — a
+gallery of just the matching photos with view pages whose previous/next cycle
+within that filter.
+
+To keep the album root uncluttered, all of this lives under a `stats/`
+subdirectory: the overview is `stats/index.html` and each mini-album is its own
+directory `stats/<filter>/` (gallery `index.html` plus numbered view pages). Only
+the main album sits in `DIST_DIR` itself. The mini-album pages reuse the album's
+shared `photos/`, `thumbs/`, and `blurs/` assets (only the HTML is per-filter)
+and are rendered in parallel honouring `IMAGE_JOBS`. Set `STATS_PAGE=no` or pass
+`--no-stats` to skip the whole `stats/` tree 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
diff --git a/bin/shuriken b/bin/shuriken
index 89d7f94..3f210b9 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -2606,8 +2606,10 @@ clear_exif_cache() {
generate_stats_pages() {
log_verbose 'Stats page enabled; collecting EXIF stats'
collect_photo_exif_stats
- render_stats_page . .
- render_filter_pages . .
+ # Keep the album root uncluttered: the stats overview is stats/index.html and
+ # every filter mini-album lives under stats/<pagebase>/ (see render_filter_pages).
+ render_stats_page stats .. index
+ render_filter_pages
}
generate() {
@@ -2800,10 +2802,11 @@ print_dry_run_plan() {
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 camera-page count needs EXIF aggregation, which dry-run
- # does not perform, so list them as a wildcard.
- printf ' %s/stats.html (EXIF stats page)\n' "${plan_ref["dist_dir"]}"
- printf ' %s/camera-*.html (per-camera pages)\n' \
+ # 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
@@ -3517,7 +3520,9 @@ _stats_filter_link() {
pagebase="${STATS_FILTER_PAGEBASE[$catkey]:-}"
if [ -n "$pagebase" ]; then
- printf '<a href="%s.html">%s</a>' "$pagebase" "$label_html"
+ # The stats overview lives at stats/index.html and each mini-album at
+ # stats/<pagebase>/index.html, so link relative to the overview.
+ printf '<a href="%s/index.html">%s</a>' "$pagebase" "$label_html"
else
printf '%s' "$label_html"
fi
@@ -3802,13 +3807,20 @@ render_stats_page() {
declare -gr STATS_PHOTOS_DIR='photos'
declare -gr STATS_THUMBS_DIR='thumbs'
declare -gr STATS_BLURS_DIR='blurs'
+# Everything stats-related lives under this dist subdirectory so the album root
+# only holds the main album: the stats overview is stats/index.html and each
+# filter mini-album is stats/<pagebase>/ (gallery index.html + view pages
+# <index>.html). This keeps the file count in any single directory bounded.
+declare -gr STATS_DIR='stats'
+# Relative path from a filter mini-album page (dist/stats/<pagebase>/...) back to
+# the album root (dist/), used for shared assets and album links.
+declare -gr STATS_FILTER_BACKHREF='../..'
# Emit one thumbnail anchor for a filter gallery: a thumb image (from thumbs/)
# linking to this filter's view page for that photo (<pagebase>--<index>.html).
# The photo filename is HTML-escaped; the pagebase and index are filename-safe.
_stats_filter_thumbnail() {
local -r backhref_html="$1"; shift
- local -r pagebase="$1"; shift
local -ri index="$1"; shift
local -r photo="$1"; shift
local photo_html
@@ -3817,8 +3829,9 @@ _stats_filter_thumbnail() {
photo_html=$(_html_escape "$photo")
# Same seeded animation the album thumbnail uses for this photo.
animation_class=$(random_animation_css_class slow "$photo")
- printf ' <a name="%s" href="%s/%s--%d.html">' \
- "$photo_html" "$backhref_html" "$pagebase" "$index"
+ # The view page sits in the same directory as this gallery, so link to it by
+ # bare index; the thumbnail image lives at the album root via backhref.
+ printf ' <a name="%s" href="%d.html">' "$photo_html" "$index"
printf '<img class="thumb %s" src="%s/%s/%s" /></a>\n' \
"$animation_class" "$backhref_html" "$STATS_THUMBS_DIR" "$photo_html"
}
@@ -3827,7 +3840,6 @@ _stats_filter_thumbnail() {
# list, preserving aggregation (encounter) order for deterministic output.
_stats_build_filter_thumbs() {
local -r backhref_html="$1"; shift
- local -r pagebase="$1"; shift
local -r photos="$1"; shift
local photo
local -i index=0
@@ -3835,7 +3847,7 @@ _stats_build_filter_thumbs() {
while IFS= read -r photo; do
if [ -n "$photo" ]; then
(( ++index ))
- _stats_filter_thumbnail "$backhref_html" "$pagebase" "$index" "$photo"
+ _stats_filter_thumbnail "$backhref_html" "$index" "$photo"
fi
done <<< "$photos"
}
@@ -3843,36 +3855,35 @@ _stats_build_filter_thumbs() {
# Render a filter gallery page (<pagebase>.html): header + camera.tmpl (heading +
# pre-built thumbnail grid) + footer. camera.tmpl is reused for every filter; the
# heading is the bucket's title (camera name, "ISO 400", "Year 2023", ...).
+# Each filter mini-album lives in its own directory stats/<pagebase>/, so the
+# gallery is index.html and the view pages are <index>.html. backhref is fixed
+# (../.. back to the album root). camera.tmpl/cameraview.tmpl are reused.
_stats_render_filter_gallery() {
- local -r html_dir="$1"; shift
- local -r backhref="$1"; shift
- local -r backhref_html="$1"; shift
local -r pagebase="$1"; shift
local thumbs
local background_image
- local -r page="$pagebase.html"
+ local -r html_dir="$STATS_DIR/$pagebase"
+ local -r backhref="$STATS_FILTER_BACKHREF"
+ local -r backhref_html="$STATS_FILTER_BACKHREF"
local -r title="${STATS_FILTER_TITLE[$pagebase]:-}"
thumbs=$(_stats_build_filter_thumbs \
- "$backhref_html" "$pagebase" "${STATS_FILTER_PHOTOS[$pagebase]}")
- background_image=$(_stats_random_background "$page")
- template header "$page" \
+ "$backhref_html" "${STATS_FILTER_PHOTOS[$pagebase]}")
+ background_image=$(_stats_random_background "$pagebase/index")
+ template header index.html \
html_dir "$html_dir" backhref "$backhref" \
blurs_dir "$STATS_BLURS_DIR" background_image "$background_image" \
show_header_bar 'yes'
- template camera "$page" \
+ template camera index.html \
html_dir "$html_dir" backhref "$backhref" \
camera_name "$title" camera_thumbs "$thumbs"
- template footer "$page" \
+ template footer index.html \
html_dir "$html_dir" backhref "$backhref" tarball_name ''
}
# Render one filter view page (<pagebase>--<index>.html): header + cameraview
# body + footer, with prev/next cycling within the filter.
_stats_render_filter_view_page() {
- local -r html_dir="$1"; shift
- local -r backhref="$1"; shift
- local -r backhref_html="$1"; shift
local -r pagebase="$1"; shift
local -r photo="$1"; shift
local -ri index="$1"; shift
@@ -3880,11 +3891,13 @@ _stats_render_filter_view_page() {
local -ri next="$1"; shift
local body
local background_image
- local -r page="$pagebase--$index.html"
+ local -r html_dir="$STATS_DIR/$pagebase"
+ local -r backhref="$STATS_FILTER_BACKHREF"
+ local -r page="$index.html"
body=$(_stats_build_filterview_body \
- "$backhref_html" "$pagebase" "$photo" "$prev" "$next")
- background_image=$(_stats_random_background "$page")
+ "$STATS_FILTER_BACKHREF" "$photo" "$prev" "$next")
+ background_image=$(_stats_random_background "$pagebase/$index")
template header "$page" \
html_dir "$html_dir" backhref "$backhref" \
blurs_dir "$STATS_BLURS_DIR" background_image "$background_image" \
@@ -3901,7 +3914,6 @@ _stats_render_filter_view_page() {
# image link.
_stats_build_filterview_body() {
local -r backhref_html="$1"; shift
- local -r pagebase="$1"; shift
local -r photo="$1"; shift
local -ri prev="$1"; shift
local -ri next="$1"; shift
@@ -3923,15 +3935,17 @@ _stats_build_filterview_body() {
details_link=$(printf ' <a href="%s/%s-details.html">Details</a> |' \
"$backhref_html" "$view_page")
fi
- _stats_print_filterview_body "$backhref_html" "$pagebase" "$photo_html" \
+ _stats_print_filterview_body "$backhref_html" "$photo_html" \
"$animation_class" "$tooltip_attr" "$details_link" "$prev" "$next"
}
# Emit the filter view page markup. Split out so _stats_build_filterview_body
# stays focused on assembling the pieces.
+# The prev/next/gallery links are same-directory (this view page lives in the
+# filter's own stats/<pagebase>/ dir); the image, details, and direct links go
+# back to the album root via backhref.
_stats_print_filterview_body() {
local -r backhref_html="$1"; shift
- local -r pagebase="$1"; shift
local -r photo_html="$1"; shift
local -r animation_class="$1"; shift
local -r tooltip_attr="$1"; shift
@@ -3941,14 +3955,14 @@ _stats_print_filterview_body() {
cat <<END
<div class='view'>
- <a href="$backhref_html/$pagebase--$next.html">
+ <a href="$next.html">
<img class='view $animation_class' border='0' src='$backhref_html/$STATS_PHOTOS_DIR/$photo_html'$tooltip_attr />
</a>
<div class="navigator">
- <a href="$backhref_html/$pagebase--$prev.html" class="arrow">&lArr;</a>
- <a href="$backhref_html/$pagebase.html">Gallery</a> |$details_link
+ <a href="$prev.html" class="arrow">&lArr;</a>
+ <a href="index.html">Gallery</a> |$details_link
<a href="$backhref_html/$STATS_PHOTOS_DIR/$photo_html">Direct link</a>
- <a href="$backhref_html/$pagebase--$next.html" class="arrow">&rArr;</a>
+ <a href="$next.html" class="arrow">&rArr;</a>
</div>
</div>
END
@@ -3958,9 +3972,6 @@ END
# shared render job pool, waiting for a free slot (<= IMAGE_JOBS) before each
# background render so parallelism follows the configured IMAGE_JOBS.
_stats_enqueue_filter_album() {
- local -r html_dir="$1"; shift
- local -r backhref="$1"; shift
- local -r backhref_html="$1"; shift
local -r pagebase="$1"; shift
local -r pids_name="$1"; shift
local -r statuses_name="$1"; shift
@@ -3981,8 +3992,7 @@ _stats_enqueue_filter_album() {
wait_for_template_render_job_slot \
"$pids_name" "$statuses_name" "$labels_name" "$failed_name"
- _stats_render_filter_gallery \
- "$html_dir" "$backhref" "$backhref_html" "$pagebase" &
+ _stats_render_filter_gallery "$pagebase" &
pids_ref+=("$!")
labels_ref["$!"]="filter gallery $pagebase"
@@ -3990,11 +4000,10 @@ _stats_enqueue_filter_album() {
wait_for_template_render_job_slot \
"$pids_name" "$statuses_name" "$labels_name" "$failed_name"
_stats_render_filter_view_page \
- "$html_dir" "$backhref" "$backhref_html" "$pagebase" \
- "${photo_list[i - 1]}" "$i" \
+ "$pagebase" "${photo_list[i - 1]}" "$i" \
"$(( i == 1 ? n : i - 1 ))" "$(( i == n ? 1 : i + 1 ))" &
pids_ref+=("$!")
- labels_ref["$!"]="filter view $pagebase--$i"
+ labels_ref["$!"]="filter view $pagebase/$i"
done
}
@@ -4003,10 +4012,11 @@ _stats_enqueue_filter_album() {
# dist-relative output dir ("." for a top-level album) and backhref the relative
# path back to the album root ("."), the same values render_stats_page uses.
# Pagebases are walked in LC_ALL=C order for reproducible enqueue order.
+# Render every filter mini-album under dist/stats/<pagebase>/ in parallel. Each
+# mini-album's location and backhref are fixed by the layout (see STATS_DIR /
+# STATS_FILTER_BACKHREF), so no path arguments are needed. Call
+# collect_photo_exif_stats first to fill STATS_FILTER_PHOTOS.
render_filter_pages() {
- local -r html_dir="$1"; shift
- local -r backhref="$1"; shift
- local backhref_html
local pagebase
# Render job pool, throttled to IMAGE_JOBS by the job-pool helpers.
local -a render_job_pids=()
@@ -4019,13 +4029,11 @@ render_filter_pages() {
if (( ${#STATS_FILTER_PHOTOS[@]} == 0 )); then
return
fi
- backhref_html=$(_html_escape "$backhref")
# Load the background photo list once now so every forked render job inherits
# the cached array rather than rescanning the photos directory per page.
_stats_load_background_photos
while IFS= read -r pagebase; do
- _stats_enqueue_filter_album \
- "$html_dir" "$backhref" "$backhref_html" "$pagebase" \
+ _stats_enqueue_filter_album "$pagebase" \
render_job_pids render_job_statuses render_job_labels render_failed
done < <(printf '%s\n' "${!STATS_FILTER_PHOTOS[@]}" | LC_ALL=C sort)
wait_for_template_render_jobs \
diff --git a/share/templates/default/camera.tmpl b/share/templates/default/camera.tmpl
index 982e32f..3b9d2f6 100644
--- a/share/templates/default/camera.tmpl
+++ b/share/templates/default/camera.tmpl
@@ -17,7 +17,7 @@ cat <<END
<div class="view stats camera">
<h1 class="stats-title">${render_title_html} &mdash; ${render_camera_name_html}</h1>
<div class="stats-back navigator">
- <a href="${render_backhref_html}/stats.html">Back to stats</a>
+ <a href="${render_backhref_html}/stats/index.html">Back to stats</a>
</div>
</div>
<div class="camera-thumbs">
diff --git a/share/templates/default/header.tmpl b/share/templates/default/header.tmpl
index 411d106..9a412fb 100644
--- a/share/templates/default/header.tmpl
+++ b/share/templates/default/header.tmpl
@@ -308,7 +308,7 @@ END
if [ "${render_stats_page_html}" = 'yes' ]; then
cat <<END
-
- <a href="${render_backhref_html}/stats.html">Stats</a>
+ <a href="${render_backhref_html}/stats/index.html">Stats</a>
END
fi
cat <<END
diff --git a/src/lib/album.source.sh b/src/lib/album.source.sh
index 17502ff..532c218 100644
--- a/src/lib/album.source.sh
+++ b/src/lib/album.source.sh
@@ -1091,8 +1091,10 @@ clear_exif_cache() {
generate_stats_pages() {
log_verbose 'Stats page enabled; collecting EXIF stats'
collect_photo_exif_stats
- render_stats_page . .
- render_filter_pages . .
+ # Keep the album root uncluttered: the stats overview is stats/index.html and
+ # every filter mini-album lives under stats/<pagebase>/ (see render_filter_pages).
+ render_stats_page stats .. index
+ render_filter_pages
}
generate() {
@@ -1285,10 +1287,11 @@ print_dry_run_plan() {
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 camera-page count needs EXIF aggregation, which dry-run
- # does not perform, so list them as a wildcard.
- printf ' %s/stats.html (EXIF stats page)\n' "${plan_ref["dist_dir"]}"
- printf ' %s/camera-*.html (per-camera pages)\n' \
+ # 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
diff --git a/src/lib/stats.source.sh b/src/lib/stats.source.sh
index 2844973..afc2d3b 100644
--- a/src/lib/stats.source.sh
+++ b/src/lib/stats.source.sh
@@ -702,7 +702,9 @@ _stats_filter_link() {
pagebase="${STATS_FILTER_PAGEBASE[$catkey]:-}"
if [ -n "$pagebase" ]; then
- printf '<a href="%s.html">%s</a>' "$pagebase" "$label_html"
+ # The stats overview lives at stats/index.html and each mini-album at
+ # stats/<pagebase>/index.html, so link relative to the overview.
+ printf '<a href="%s/index.html">%s</a>' "$pagebase" "$label_html"
else
printf '%s' "$label_html"
fi
@@ -987,13 +989,20 @@ render_stats_page() {
declare -gr STATS_PHOTOS_DIR='photos'
declare -gr STATS_THUMBS_DIR='thumbs'
declare -gr STATS_BLURS_DIR='blurs'
+# Everything stats-related lives under this dist subdirectory so the album root
+# only holds the main album: the stats overview is stats/index.html and each
+# filter mini-album is stats/<pagebase>/ (gallery index.html + view pages
+# <index>.html). This keeps the file count in any single directory bounded.
+declare -gr STATS_DIR='stats'
+# Relative path from a filter mini-album page (dist/stats/<pagebase>/...) back to
+# the album root (dist/), used for shared assets and album links.
+declare -gr STATS_FILTER_BACKHREF='../..'
# Emit one thumbnail anchor for a filter gallery: a thumb image (from thumbs/)
# linking to this filter's view page for that photo (<pagebase>--<index>.html).
# The photo filename is HTML-escaped; the pagebase and index are filename-safe.
_stats_filter_thumbnail() {
local -r backhref_html="$1"; shift
- local -r pagebase="$1"; shift
local -ri index="$1"; shift
local -r photo="$1"; shift
local photo_html
@@ -1002,8 +1011,9 @@ _stats_filter_thumbnail() {
photo_html=$(_html_escape "$photo")
# Same seeded animation the album thumbnail uses for this photo.
animation_class=$(random_animation_css_class slow "$photo")
- printf ' <a name="%s" href="%s/%s--%d.html">' \
- "$photo_html" "$backhref_html" "$pagebase" "$index"
+ # The view page sits in the same directory as this gallery, so link to it by
+ # bare index; the thumbnail image lives at the album root via backhref.
+ printf ' <a name="%s" href="%d.html">' "$photo_html" "$index"
printf '<img class="thumb %s" src="%s/%s/%s" /></a>\n' \
"$animation_class" "$backhref_html" "$STATS_THUMBS_DIR" "$photo_html"
}
@@ -1012,7 +1022,6 @@ _stats_filter_thumbnail() {
# list, preserving aggregation (encounter) order for deterministic output.
_stats_build_filter_thumbs() {
local -r backhref_html="$1"; shift
- local -r pagebase="$1"; shift
local -r photos="$1"; shift
local photo
local -i index=0
@@ -1020,7 +1029,7 @@ _stats_build_filter_thumbs() {
while IFS= read -r photo; do
if [ -n "$photo" ]; then
(( ++index ))
- _stats_filter_thumbnail "$backhref_html" "$pagebase" "$index" "$photo"
+ _stats_filter_thumbnail "$backhref_html" "$index" "$photo"
fi
done <<< "$photos"
}
@@ -1028,36 +1037,35 @@ _stats_build_filter_thumbs() {
# Render a filter gallery page (<pagebase>.html): header + camera.tmpl (heading +
# pre-built thumbnail grid) + footer. camera.tmpl is reused for every filter; the
# heading is the bucket's title (camera name, "ISO 400", "Year 2023", ...).
+# Each filter mini-album lives in its own directory stats/<pagebase>/, so the
+# gallery is index.html and the view pages are <index>.html. backhref is fixed
+# (../.. back to the album root). camera.tmpl/cameraview.tmpl are reused.
_stats_render_filter_gallery() {
- local -r html_dir="$1"; shift
- local -r backhref="$1"; shift
- local -r backhref_html="$1"; shift
local -r pagebase="$1"; shift
local thumbs
local background_image
- local -r page="$pagebase.html"
+ local -r html_dir="$STATS_DIR/$pagebase"
+ local -r backhref="$STATS_FILTER_BACKHREF"
+ local -r backhref_html="$STATS_FILTER_BACKHREF"
local -r title="${STATS_FILTER_TITLE[$pagebase]:-}"
thumbs=$(_stats_build_filter_thumbs \
- "$backhref_html" "$pagebase" "${STATS_FILTER_PHOTOS[$pagebase]}")
- background_image=$(_stats_random_background "$page")
- template header "$page" \
+ "$backhref_html" "${STATS_FILTER_PHOTOS[$pagebase]}")
+ background_image=$(_stats_random_background "$pagebase/index")
+ template header index.html \
html_dir "$html_dir" backhref "$backhref" \
blurs_dir "$STATS_BLURS_DIR" background_image "$background_image" \
show_header_bar 'yes'
- template camera "$page" \
+ template camera index.html \
html_dir "$html_dir" backhref "$backhref" \
camera_name "$title" camera_thumbs "$thumbs"
- template footer "$page" \
+ template footer index.html \
html_dir "$html_dir" backhref "$backhref" tarball_name ''
}
# Render one filter view page (<pagebase>--<index>.html): header + cameraview
# body + footer, with prev/next cycling within the filter.
_stats_render_filter_view_page() {
- local -r html_dir="$1"; shift
- local -r backhref="$1"; shift
- local -r backhref_html="$1"; shift
local -r pagebase="$1"; shift
local -r photo="$1"; shift
local -ri index="$1"; shift
@@ -1065,11 +1073,13 @@ _stats_render_filter_view_page() {
local -ri next="$1"; shift
local body
local background_image
- local -r page="$pagebase--$index.html"
+ local -r html_dir="$STATS_DIR/$pagebase"
+ local -r backhref="$STATS_FILTER_BACKHREF"
+ local -r page="$index.html"
body=$(_stats_build_filterview_body \
- "$backhref_html" "$pagebase" "$photo" "$prev" "$next")
- background_image=$(_stats_random_background "$page")
+ "$STATS_FILTER_BACKHREF" "$photo" "$prev" "$next")
+ background_image=$(_stats_random_background "$pagebase/$index")
template header "$page" \
html_dir "$html_dir" backhref "$backhref" \
blurs_dir "$STATS_BLURS_DIR" background_image "$background_image" \
@@ -1086,7 +1096,6 @@ _stats_render_filter_view_page() {
# image link.
_stats_build_filterview_body() {
local -r backhref_html="$1"; shift
- local -r pagebase="$1"; shift
local -r photo="$1"; shift
local -ri prev="$1"; shift
local -ri next="$1"; shift
@@ -1108,15 +1117,17 @@ _stats_build_filterview_body() {
details_link=$(printf ' <a href="%s/%s-details.html">Details</a> |' \
"$backhref_html" "$view_page")
fi
- _stats_print_filterview_body "$backhref_html" "$pagebase" "$photo_html" \
+ _stats_print_filterview_body "$backhref_html" "$photo_html" \
"$animation_class" "$tooltip_attr" "$details_link" "$prev" "$next"
}
# Emit the filter view page markup. Split out so _stats_build_filterview_body
# stays focused on assembling the pieces.
+# The prev/next/gallery links are same-directory (this view page lives in the
+# filter's own stats/<pagebase>/ dir); the image, details, and direct links go
+# back to the album root via backhref.
_stats_print_filterview_body() {
local -r backhref_html="$1"; shift
- local -r pagebase="$1"; shift
local -r photo_html="$1"; shift
local -r animation_class="$1"; shift
local -r tooltip_attr="$1"; shift
@@ -1126,14 +1137,14 @@ _stats_print_filterview_body() {
cat <<END
<div class='view'>
- <a href="$backhref_html/$pagebase--$next.html">
+ <a href="$next.html">
<img class='view $animation_class' border='0' src='$backhref_html/$STATS_PHOTOS_DIR/$photo_html'$tooltip_attr />
</a>
<div class="navigator">
- <a href="$backhref_html/$pagebase--$prev.html" class="arrow">&lArr;</a>
- <a href="$backhref_html/$pagebase.html">Gallery</a> |$details_link
+ <a href="$prev.html" class="arrow">&lArr;</a>
+ <a href="index.html">Gallery</a> |$details_link
<a href="$backhref_html/$STATS_PHOTOS_DIR/$photo_html">Direct link</a>
- <a href="$backhref_html/$pagebase--$next.html" class="arrow">&rArr;</a>
+ <a href="$next.html" class="arrow">&rArr;</a>
</div>
</div>
END
@@ -1143,9 +1154,6 @@ END
# shared render job pool, waiting for a free slot (<= IMAGE_JOBS) before each
# background render so parallelism follows the configured IMAGE_JOBS.
_stats_enqueue_filter_album() {
- local -r html_dir="$1"; shift
- local -r backhref="$1"; shift
- local -r backhref_html="$1"; shift
local -r pagebase="$1"; shift
local -r pids_name="$1"; shift
local -r statuses_name="$1"; shift
@@ -1166,8 +1174,7 @@ _stats_enqueue_filter_album() {
wait_for_template_render_job_slot \
"$pids_name" "$statuses_name" "$labels_name" "$failed_name"
- _stats_render_filter_gallery \
- "$html_dir" "$backhref" "$backhref_html" "$pagebase" &
+ _stats_render_filter_gallery "$pagebase" &
pids_ref+=("$!")
labels_ref["$!"]="filter gallery $pagebase"
@@ -1175,11 +1182,10 @@ _stats_enqueue_filter_album() {
wait_for_template_render_job_slot \
"$pids_name" "$statuses_name" "$labels_name" "$failed_name"
_stats_render_filter_view_page \
- "$html_dir" "$backhref" "$backhref_html" "$pagebase" \
- "${photo_list[i - 1]}" "$i" \
+ "$pagebase" "${photo_list[i - 1]}" "$i" \
"$(( i == 1 ? n : i - 1 ))" "$(( i == n ? 1 : i + 1 ))" &
pids_ref+=("$!")
- labels_ref["$!"]="filter view $pagebase--$i"
+ labels_ref["$!"]="filter view $pagebase/$i"
done
}
@@ -1188,10 +1194,11 @@ _stats_enqueue_filter_album() {
# dist-relative output dir ("." for a top-level album) and backhref the relative
# path back to the album root ("."), the same values render_stats_page uses.
# Pagebases are walked in LC_ALL=C order for reproducible enqueue order.
+# Render every filter mini-album under dist/stats/<pagebase>/ in parallel. Each
+# mini-album's location and backhref are fixed by the layout (see STATS_DIR /
+# STATS_FILTER_BACKHREF), so no path arguments are needed. Call
+# collect_photo_exif_stats first to fill STATS_FILTER_PHOTOS.
render_filter_pages() {
- local -r html_dir="$1"; shift
- local -r backhref="$1"; shift
- local backhref_html
local pagebase
# Render job pool, throttled to IMAGE_JOBS by the job-pool helpers.
local -a render_job_pids=()
@@ -1204,13 +1211,11 @@ render_filter_pages() {
if (( ${#STATS_FILTER_PHOTOS[@]} == 0 )); then
return
fi
- backhref_html=$(_html_escape "$backhref")
# Load the background photo list once now so every forked render job inherits
# the cached array rather than rescanning the photos directory per page.
_stats_load_background_photos
while IFS= read -r pagebase; do
- _stats_enqueue_filter_album \
- "$html_dir" "$backhref" "$backhref_html" "$pagebase" \
+ _stats_enqueue_filter_album "$pagebase" \
render_job_pids render_job_statuses render_job_labels render_failed
done < <(printf '%s\n' "${!STATS_FILTER_PHOTOS[@]}" | LC_ALL=C sort)
wait_for_template_render_jobs \
diff --git a/tests/cli.sh b/tests/cli.sh
index d2140e3..a435e58 100755
--- a/tests/cli.sh
+++ b/tests/cli.sh
@@ -1865,8 +1865,9 @@ test_dry_run_reports_cli_overrides_without_writes() {
test::assert_contains \
" $dist_dir/[redirect].html (14 navigation redirects)" \
"$output"
- test::assert_contains " $dist_dir/stats.html (EXIF stats page)" "$output"
- test::assert_contains " $dist_dir/camera-*.html (per-camera pages)" \
+ test::assert_contains " $dist_dir/stats/index.html (EXIF stats page)" \
+ "$output"
+ test::assert_contains " $dist_dir/stats/*/ (filter mini-albums)" \
"$output"
test::assert_not_contains "$dist_dir/html" "$output"
test::assert_contains " $dist_dir/incoming-<timestamp>.tar" "$output"
@@ -1902,8 +1903,8 @@ test_dry_run_no_stats_omits_stats_plan() {
)
test::assert_contains 'Stats page: no' "$output"
- test::assert_not_contains 'stats.html (EXIF stats page)' "$output"
- test::assert_not_contains 'camera-*.html (per-camera pages)' "$output"
+ test::assert_not_contains 'stats/index.html (EXIF stats page)' "$output"
+ test::assert_not_contains 'stats/*/ (filter mini-albums)' "$output"
test::assert_path_absent "$dist_dir"
test::teardown
}
@@ -3084,44 +3085,47 @@ test_generate_stats_pages_created_and_nav_linked() {
"$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 stats page gets a random blurred background like the album pages.
- test::assert_contains 'background-image: url("./blurs/' \
- "$(<"$TEST_TMPDIR/dist/stats.html")"
-
- # The camera gallery is a mini album: thumbnails link to per-camera view
- # pages (camera-<slug>--<n>.html), not the album view pages or raw images.
- camera_html=$(<"$TEST_TMPDIR/dist/camera-canon-eos-r5.html")
- test::assert_contains 'src="./thumbs/' "$camera_html"
- test::assert_contains 'href="./camera-canon-eos-r5--1.html"' "$camera_html"
- test::assert_not_contains 'href="./photos/' "$camera_html"
+ # Only the main album is in the dist root; all stats content is under stats/.
+ # The overview is stats/index.html and each mini-album is stats/<pagebase>/.
+ test::assert_path_absent "$TEST_TMPDIR/dist/stats.html"
+ test::assert_file_exists "$TEST_TMPDIR/dist/stats/index.html"
+ test::assert_file_exists "$TEST_TMPDIR/dist/stats/camera-canon-eos-r5/index.html"
+ test::assert_contains 'Canon EOS R5' \
+ "$(<"$TEST_TMPDIR/dist/stats/index.html")"
+ test::assert_not_contains '<script' \
+ "$(<"$TEST_TMPDIR/dist/stats/index.html")"
+
+ # The stats overview gets a random blurred background (one level deep -> ..).
+ test::assert_contains 'background-image: url("../blurs/' \
+ "$(<"$TEST_TMPDIR/dist/stats/index.html")"
+
+ # The camera gallery is a mini album: thumbnails link to view pages in the
+ # same dir (<index>.html), and the thumb image points at the album root.
+ camera_html=$(<"$TEST_TMPDIR/dist/stats/camera-canon-eos-r5/index.html")
+ test::assert_contains 'src="../../thumbs/' "$camera_html"
+ test::assert_contains 'href="1.html"' "$camera_html"
# Non-camera stats are clickable mini-albums too: the ISO row links to a
- # filter page that exists and is itself a gallery of matching photos.
- test::assert_contains 'href="iso-400.html"' \
- "$(<"$TEST_TMPDIR/dist/stats.html")"
- test::assert_file_exists "$TEST_TMPDIR/dist/iso-400.html"
- test::assert_file_exists "$TEST_TMPDIR/dist/iso-400--1.html"
- test::assert_contains 'href="./iso-400--1.html"' \
- "$(<"$TEST_TMPDIR/dist/iso-400.html")"
-
- # A per-camera view page exists and its navigation stays within the camera:
- # prev/next point at this camera's view pages, plus links back to the gallery
- # and to the album details page for the photo.
- test::assert_file_exists "$TEST_TMPDIR/dist/camera-canon-eos-r5--1.html"
- camera_view_html=$(<"$TEST_TMPDIR/dist/camera-canon-eos-r5--1.html")
- test::assert_contains 'href="./camera-canon-eos-r5--2.html"' "$camera_view_html"
- test::assert_contains 'href="./camera-canon-eos-r5.html">Gallery</a>' \
- "$camera_view_html"
+ # filter mini-album that exists and is itself a gallery of matching photos.
+ test::assert_contains 'href="iso-400/index.html"' \
+ "$(<"$TEST_TMPDIR/dist/stats/index.html")"
+ test::assert_file_exists "$TEST_TMPDIR/dist/stats/iso-400/index.html"
+ test::assert_file_exists "$TEST_TMPDIR/dist/stats/iso-400/1.html"
+ test::assert_contains 'href="1.html"' \
+ "$(<"$TEST_TMPDIR/dist/stats/iso-400/index.html")"
+
+ # A per-camera view page exists and its navigation stays within the filter:
+ # prev/next point at sibling view pages, plus a Gallery link and a Details
+ # link back to the album details page for the photo.
+ test::assert_file_exists "$TEST_TMPDIR/dist/stats/camera-canon-eos-r5/1.html"
+ camera_view_html=$(<"$TEST_TMPDIR/dist/stats/camera-canon-eos-r5/1.html")
+ test::assert_contains 'href="2.html"' "$camera_view_html"
+ test::assert_contains 'href="index.html">Gallery</a>' "$camera_view_html"
test::assert_contains '-details.html">Details</a>' "$camera_view_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)
+ # The header bar links to the stats overview on at least one generated page.
+ nav_links=$(grep -lF 'stats/index.html">Stats' "$TEST_TMPDIR"/dist/*.html \
+ | wc -l)
test "$nav_links" -gt 0
python3 - "$TEST_TMPDIR/dist/shuriken.json" <<'PY'
@@ -3158,14 +3162,10 @@ test_generate_no_stats_suppresses_pages_and_nav() {
"$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 directory at all (no overview, no mini-albums).
+ test::assert_path_absent "$TEST_TMPDIR/dist/stats"
# No stats nav link anywhere.
- if grep -RF 'stats.html">Stats' "$TEST_TMPDIR"/dist/*.html; then
+ if grep -RF 'stats/index.html">Stats' "$TEST_TMPDIR"/dist/*.html; then
printf 'FAIL: --no-stats still rendered the Stats nav link\n' >&2
exit 1
fi
@@ -3981,9 +3981,10 @@ BASH
html=$(cat "$output_file")
- # Leaderboard entry links to camera-<slug>.html with the right count/percent.
+ # Leaderboard entry links to the camera's mini-album with the right
+ # count/percent (relative to the stats overview: <pagebase>/index.html).
test::assert_contains \
- '<a href="camera-canon-eos-5d.html">Canon EOS 5D</a>' "$html"
+ '<a href="camera-canon-eos-5d/index.html">Canon EOS 5D</a>' "$html"
test::assert_contains '2 (67%)' "$html"
# A histogram section is present.
test::assert_contains '<h2>ISO</h2>' "$html"
@@ -4078,54 +4079,61 @@ EXIF
EXIF
}
-mkdir -p "$dist_dir/run1" "$dist_dir/run2"
+# render_filter_pages writes under $DIST_DIR/stats/, so point DIST_DIR at a
+# fresh per-run dir to compare the two runs for determinism.
feed
-render_filter_pages run1 ..
+DIST_DIR="$dist_dir/run1"
+mkdir -p "$DIST_DIR"
+render_filter_pages
feed
-render_filter_pages run2 ..
+DIST_DIR="$dist_dir/run2"
+mkdir -p "$DIST_DIR"
+render_filter_pages
BASH
- # Camera mini-albums: one gallery per camera, collision-resolved names.
- test::assert_file_exists "$dist_dir/run1/camera-canon-eos-5d.html"
- test::assert_file_exists "$dist_dir/run1/camera-canon-eos-5d-2.html"
- test::assert_file_exists "$dist_dir/run1/camera-nikon-co-z6.html"
+ # Each mini-album is its own stats/<pagebase>/ directory (gallery index.html +
+ # view pages <index>.html), keeping the album root uncluttered. Camera names
+ # are collision-resolved (canon-eos-5d / canon-eos-5d-2).
+ local s="$dist_dir/run1/stats"
+ test::assert_file_exists "$s/camera-canon-eos-5d/index.html"
+ test::assert_file_exists "$s/camera-canon-eos-5d-2/index.html"
+ test::assert_file_exists "$s/camera-nikon-co-z6/index.html"
# Non-camera stats are mini-albums too: orientation comes from Geometry, so
# the three landscape photos get their own filter mini-album.
- test::assert_file_exists "$dist_dir/run1/orientation-landscape.html"
- test::assert_file_exists "$dist_dir/run1/orientation-landscape--1.html"
-
- # The Canon EOS 5D gallery lists its two photos as thumbnails linking to thi