summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorKen D'Ambrosio <ken@jots.org>2026-05-22 22:50:35 +0000
committerKen D'Ambrosio <ken@jots.org>2026-05-22 22:50:35 +0000
commitd32b5e99afc6f0cffefa594510cda0e4f414db75 (patch)
treeb4c24a1a7264bcbde72c0fff906e7bf380c18a02 /scripts
parentde80b9871ebe1497c672f3c7c7bb5467dabcb83a (diff)
Speed up update.rb and fix UI always forcing full rescan
- update.rb: skip exiftool on images marked exif_absent (set after first failed attempt); prevents repeated slow scans of old photos with no EXIF - update.rb: explicit directory argument now implies force — passing a path always rescans that subtree regardless of sentinel mtime - app.rb: /admin/update no longer hardcodes --force; sentinel-based skipping is used by default, making UI updates finish in seconds instead of minutes - admin/album.erb: add "Force rescan all" checkbox to Run Update button; checked state passes force=1 to the server and restores --force behavior - README.md, DESIGN.md: document sentinel skipping, exif_absent flag, and explicit-directory force behavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'scripts')
-rw-r--r--scripts/update.rb36
1 files changed, 31 insertions, 5 deletions
diff --git a/scripts/update.rb b/scripts/update.rb
index 9953505..822405f 100644
--- a/scripts/update.rb
+++ b/scripts/update.rb
@@ -1,9 +1,12 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
-# Usage: ruby update.rb [relative/path]
-# Without argument: process entire MEDIA_ROOT tree.
-# With argument: process only that subdirectory (and its children).
+# Usage: ruby update.rb [--force] [relative/path]
+# Without argument: process entire MEDIA_ROOT tree, skipping directories
+# whose mtime hasn't changed since the last scan.
+# With argument: process only that subdirectory (and its children),
+# always scanning regardless of mtime (explicit request).
+# --force: scan entire tree ignoring all mtime sentinels.
#
# Resilience guarantees:
# - album.json is written atomically (temp-file + rename), so a crash
@@ -11,6 +14,8 @@
# - Thumbnails are checked before generation; already-done work is skipped.
# - EXIF and dimension extraction are skipped if already recorded.
# - Safe to re-run at any time; all operations are idempotent.
+# - Unchanged directories are skipped via a .albumen_scanned sentinel file;
+# pass --force to bypass.
require 'json'
require 'yaml'
@@ -27,12 +32,25 @@ VIDEO_EXTS = %w[mp4 mov avi mkv webm m4v ogv].freeze
AUDIO_EXTS = %w[mp3 flac ogg wav m4a aac].freeze
MEDIA_EXTS = (IMAGE_EXTS + VIDEO_EXTS + AUDIO_EXTS).freeze
TRANSCODE_EXTS = %w[avi mkv mov].freeze # not universally browser-playable; convert to MP4
+SENTINEL_FILE = '.albumen_scanned'.freeze
+
+# Explicit directory argument implies force — you asked for it, it should run.
+FORCE_UPDATE = !!(ARGV.delete('--force') || ARGV[0])
# ── Directory processing ───────────────────────────────────────────────────────
def process_dir(dir)
rel = dir.delete_prefix(MEDIA_ROOT).delete_prefix('/')
label = rel.empty? ? '(root)' : rel
+
+ unless FORCE_UPDATE
+ sentinel = File.join(dir, SENTINEL_FILE)
+ if File.exist?(sentinel) && File.mtime(sentinel) >= File.mtime(dir)
+ puts "Skipping #{label} (unchanged)"
+ return
+ end
+ end
+
puts "Scanning #{label}"
json_path = File.join(dir, 'album.json')
@@ -102,13 +120,15 @@ def process_dir(dir)
end
atomic_write_json(json_path, data)
+ FileUtils.touch(File.join(dir, SENTINEL_FILE))
end
# ── Metadata enrichment ────────────────────────────────────────────────────────
def enrich_image(full, name, meta)
- needs_exif = meta['taken_at'].nil? || meta['camera'].nil? ||
- meta['aperture'].nil? || meta['shutter'].nil? || meta['iso'].nil?
+ needs_exif = !meta['exif_absent'] &&
+ (meta['taken_at'].nil? || meta['camera'].nil? ||
+ meta['aperture'].nil? || meta['shutter'].nil? || meta['iso'].nil?)
if needs_exif
begin
exif = MiniExiftool.new(full, numerical: false)
@@ -133,6 +153,12 @@ def enrich_image(full, name, meta)
rescue StandardError => e
warn " #{name}: EXIF error — #{e.message}"
end
+
+ # If exiftool found nothing at all, record that so we don't retry on every re-scan.
+ if meta['taken_at'].nil? && meta['camera'].nil? &&
+ meta['aperture'].nil? && meta['shutter'].nil? && meta['iso'].nil?
+ meta['exif_absent'] = true
+ end
end
# Dimensions (skip if already recorded)