summaryrefslogtreecommitdiffstats
path: root/scripts/update.rb
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/update.rb')
-rw-r--r--scripts/update.rb103
1 files changed, 29 insertions, 74 deletions
diff --git a/scripts/update.rb b/scripts/update.rb
index 5671330..f909510 100644
--- a/scripts/update.rb
+++ b/scripts/update.rb
@@ -16,6 +16,9 @@
# - 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.
+#
+# Face detection is NOT handled here. Run face_daemon.rb (or let the systemd
+# service manage it) to detect faces and write per-directory faces.json files.
require 'json'
require 'yaml'
@@ -23,27 +26,30 @@ require 'fileutils'
require 'mini_magick'
require 'mini_exiftool'
-MEDIA_ROOT = (ENV['MEDIA_ROOT'] || '/var/albumen').freeze
-CACHE_ROOT = (ENV['CACHE_ROOT'] || '/opt/albumen/cache/thumbs').freeze
-CONFIG_PATH = (ENV['CONFIG_PATH'] || '/opt/albumen/config.yml').freeze
-THUMB_SIZE = 300
+MEDIA_ROOT = (ENV['MEDIA_ROOT'] || '/var/albumen').freeze
+CACHE_ROOT = (ENV['CACHE_ROOT'] || '/opt/albumen/cache/thumbs').freeze
+LOG_PATH = (ENV['LOG_PATH'] || '/opt/albumen/log/albumen.log').freeze
+THUMB_SIZE = 300
IMAGE_EXTS = %w[jpg jpeg png gif webp heic heif tiff bmp].freeze
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
+TRANSCODE_EXTS = %w[avi mkv mov].freeze
SENTINEL_FILE = '.albumen_scanned'.freeze
-_cfg = File.exist?(CONFIG_PATH) ? YAML.load_file(CONFIG_PATH, symbolize_names: true) : {}
-FACES_ENABLED = (_cfg.dig(:faces, :enabled) == true).freeze
-FACES_WORKERS = (_cfg.dig(:faces, :workers) || 4).freeze
-VENV_PYTHON = File.expand_path('../venv/bin/python3', __dir__).freeze
-FACES_SCRIPT = File.expand_path('faces.py', __dir__).freeze
-
# Explicit directory argument implies force — you asked for it, it should run.
FORCE_UPDATE = !!(ARGV.delete('--force') || ARGV[0])
+def log(msg)
+ $stdout.puts msg
+ $stdout.flush
+ ts = Time.now.strftime('%Y-%m-%d %H:%M:%S')
+ File.open(LOG_PATH, 'a') { |f| f.puts "[#{ts}] [update] #{msg}" }
+rescue StandardError
+ # never crash on log failure
+end
+
# ── Directory processing ───────────────────────────────────────────────────────
def process_dir(dir, idx, total)
@@ -51,20 +57,15 @@ def process_dir(dir, idx, total)
label = rel.empty? ? '(root)' : rel
prefix = "[#{idx}/#{total}]"
- pending_faces = false
unless FORCE_UPDATE
sentinel = File.join(dir, SENTINEL_FILE)
if File.exist?(sentinel) && File.mtime(sentinel) >= File.mtime(dir)
- if faces_pending?(dir)
- pending_faces = true # fall through, but only to run face detection
- else
- puts "#{prefix} Skipping #{label} (unchanged)"
- return
- end
+ log "#{prefix} Skipping #{label} (unchanged)"
+ return
end
end
- puts "#{prefix} Scanning #{label}#{' (face detection pending)' if pending_faces}"
+ log "#{prefix} Scanning #{label}"
json_path = File.join(dir, 'album.json')
data = load_json(json_path)
@@ -84,9 +85,9 @@ def process_dir(dir, idx, total)
thumb = File.join(CACHE_ROOT, rel.empty? ? "#{n}.th.jpg" : "#{rel}/#{n}.th.jpg")
if File.exist?(thumb)
File.unlink(thumb)
- puts " Removed: #{n} (+ thumb)"
+ log " Removed: #{n} (+ thumb)"
else
- puts " Removed: #{n}"
+ log " Removed: #{n}"
end
end
@@ -96,20 +97,19 @@ def process_dir(dir, idx, total)
base = File.basename(name, '.*')
target = "#{base}.mp4"
if current.include?(target)
- # MP4 already exists — just ensure the marker is recorded
data['files'][name] ||= {}
data['files'][name]['transcoded_to'] = target
next
end
full = File.join(dir, name)
dest = File.join(dir, target)
- puts " Transcoding: #{name} → #{target}"
+ log " Transcoding: #{name} → #{target}"
transcode_to_mp4(full, dest)
if File.exist?(dest)
data['files'][name] ||= {}
data['files'][name]['transcoded_to'] = target
current << target
- puts " → done"
+ log " → done"
else
warn " Transcode failed: #{name}"
end
@@ -132,8 +132,6 @@ def process_dir(dir, idx, total)
generate_thumb_if_needed(full, rel, name, ext)
end
- batch_detect_faces(dir, current, data) if FACES_ENABLED
-
atomic_write_json(json_path, data)
FileUtils.touch(File.join(dir, SENTINEL_FILE))
end
@@ -152,7 +150,7 @@ def enrich_image(full, name, meta)
raw = exif.date_time_original || exif.create_date || exif.date_time
if raw
meta['taken_at'] = raw.respond_to?(:strftime) ? raw.strftime('%Y-%m-%dT%H:%M:%S') : raw.to_s
- puts " #{name}: taken_at = #{meta['taken_at']}"
+ log " #{name}: taken_at = #{meta['taken_at']}"
end
end
@@ -169,14 +167,12 @@ def enrich_image(full, name, meta)
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)
if meta['width'].nil?
begin
img = MiniMagick::Image.open(full)
@@ -186,37 +182,6 @@ def enrich_image(full, name, meta)
warn " #{name}: dimension error — #{e.message}"
end
end
-
-end
-
-def batch_detect_faces(dir, names, data)
- return unless File.exist?(VENV_PYTHON) && File.exist?(FACES_SCRIPT)
-
- unprocessed = names.select do |name|
- IMAGE_EXTS.include?(File.extname(name).downcase.delete_prefix('.')) &&
- (data['files'][name] || {})['faces'].nil?
- end
- return if unprocessed.empty?
-
- puts " Detecting faces in #{unprocessed.length} image(s) (#{FACES_WORKERS} workers)…"
- paths = unprocessed.map { |n| File.join(dir, n) }
- cmd = [VENV_PYTHON, FACES_SCRIPT, '--workers', FACES_WORKERS.to_s] + paths
-
- begin
- out = IO.popen(cmd, err: '/dev/null', &:read).strip
- results = JSON.parse(out.empty? ? '{}' : out)
- raise 'expected Hash' unless results.is_a?(Hash)
-
- results.each do |path, faces|
- name = File.basename(path)
- next unless data['files'].key?(name)
- next if faces.nil? # error on this file — leave faces: null to retry
- data['files'][name]['faces'] = faces
- puts " #{name}: #{faces.length} face(s)" unless faces.empty?
- end
- rescue StandardError => e
- warn " Face detection batch error — #{e.message}"
- end
end
def enrich_video(full, name, meta)
@@ -232,12 +197,12 @@ end
# ── Thumbnail generation ───────────────────────────────────────────────────────
def generate_thumb_if_needed(full, rel, name, ext)
- return if AUDIO_EXTS.include?(ext) # audio uses a static icon
+ return if AUDIO_EXTS.include?(ext)
cache = File.join(CACHE_ROOT, rel.empty? ? "#{name}.th.jpg" : "#{rel}/#{name}.th.jpg")
return if File.exist?(cache)
- puts " Generating thumb: #{name}"
+ log " Generating thumb: #{name}"
FileUtils.mkdir_p(File.dirname(cache))
if VIDEO_EXTS.include?(ext)
@@ -295,16 +260,6 @@ rescue JSON::ParserError => e
{}
end
-def faces_pending?(dir)
- return false unless FACES_ENABLED
- json_path = File.join(dir, 'album.json')
- return false unless File.exist?(json_path)
- (load_json(json_path)['files'] || {}).any? do |name, meta|
- IMAGE_EXTS.include?(File.extname(name).downcase.delete_prefix('.')) &&
- meta['faces'].nil?
- end
-end
-
# Fields the admin controls — never overwrite with stale values from our earlier read.
ADMIN_ALBUM_KEYS = %w[title description cover cover_dynamic sort_reverse visible].freeze
ADMIN_FILE_KEYS = %w[title caption visible].freeze
@@ -348,7 +303,7 @@ if Process.uid == 0
begin
require 'etc'
pw = Etc.getpwnam(service_user)
- puts "Fixing ownership of #{start} → #{service_user}"
+ log "Fixing ownership of #{start} → #{service_user}"
FileUtils.chown_R(pw.uid, pw.gid, start)
rescue ArgumentError
warn "Warning: user '#{service_user}' not found; skipping chown"
@@ -361,4 +316,4 @@ dirs = dirs.uniq
total = dirs.size
dirs.each_with_index { |d, i| process_dir(d, i + 1, total) }
-puts 'Done.'
+log 'Done.'