diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-15 18:48:49 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-15 23:50:41 +0300 |
| commit | e7aeb1f998aef664d3fc6cc21dc8c3b6b7579d01 (patch) | |
| tree | 08ba293284c3ad5293fb2252407f83b5ddfebeb0 | |
| parent | 66a39be5271393b95cddae1bd61206eea8427934 (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.md | 18 | ||||
| -rwxr-xr-x | bin/shuriken | 104 | ||||
| -rw-r--r-- | share/templates/default/camera.tmpl | 2 | ||||
| -rw-r--r-- | share/templates/default/header.tmpl | 2 | ||||
| -rw-r--r-- | src/lib/album.source.sh | 15 | ||||
| -rw-r--r-- | src/lib/stats.source.sh | 89 | ||||
| -rwxr-xr-x | tests/cli.sh | 166 |
7 files changed, 215 insertions, 181 deletions
@@ -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">⇐</a> - <a href="$backhref_html/$pagebase.html">Gallery</a> |$details_link + <a href="$prev.html" class="arrow">⇐</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">⇒</a> + <a href="$next.html" class="arrow">⇒</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} — ${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">⇐</a> - <a href="$backhref_html/$pagebase.html">Gallery</a> |$details_link + <a href="$prev.html" class="arrow">⇐</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">⇒</a> + <a href="$next.html" class="arrow">⇒</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" |
