# Albumen — Photo Album Server Self-hosted photo/video album. Directory hierarchy = album hierarchy. Ruby/Sinatra back end, plain HTML/CSS/JS front end. Live at **https://albumen.jots.org**. --- ## Features ### Browsing - Nested album hierarchy — every directory is an album, unlimited nesting - Grid view with auto-generated 300×300 thumbnails (images, video frames, audio placeholder) - Chronological photo sorting when EXIF dates are present, filename order otherwise - Live search/filter box to narrow albums by name - Breadcrumb navigation - Lightbox with keyboard (← →, Esc) and touch-swipe navigation - Info panel showing filename, EXIF date, and pixel dimensions - Shareable per-photo URLs (`?photo=filename`) that open the lightbox directly - Social media link previews via Open Graph meta tags (album cover or specific photo) ### Slideshow - Per-album or root-level (all photos across every album) - Shuffle and Full screen options selectable before launch - Respects the current album filter — filtered view launches a filtered slideshow - Cross-fade transitions with configurable interval (1–60 s) - Keyboard (← →, Space, F), touch swipe, and on-screen controls - Click/tap a photo during slideshow to jump to its album lightbox ### Admin - Single admin account; password stored as a PBKDF2-SHA256 hash (no native gem needed) - Logging in from an album page redirects back to that album's edit view - Per-album: title, description, cover image (specific file or random), sub-album order, visibility - Per-file: caption, visibility - Save button at top and bottom of the edit form - **Run Update** button scans for new/removed files and generates missing thumbnails; **Force rescan all** checkbox bypasses the sentinel and rescans every directory ### Media support | Category | Extensions | |---|---| | Images | jpg jpeg png gif webp heic heif tiff bmp | | Videos | mp4 mov avi mkv webm m4v ogv | | Audio | mp3 flac ogg wav m4a aac | --- ## Directory layout | Path | Purpose | |---|---| | `/opt/albumen/` | Application root | | `/opt/albumen/app.rb` | Sinatra application | | `/opt/albumen/views/` | ERB templates | | `/opt/albumen/public/` | CSS, JS, static assets | | `/opt/albumen/scripts/` | CLI utilities | | `/opt/albumen/cache/thumbs/` | Auto-generated thumbnails (safe to delete/regenerate) | | `/opt/albumen/config.yml` | Admin password hash + session secret (mode 600) | | `/opt/albumen/log/` | Puma stdout/stderr logs | | `/var/albumen/` | **Your photos live here** | Each subdirectory under `/var/albumen/` is an album. Nesting is unlimited. The directory name is the album name; the admin UI lets you assign a prettier title. --- ## Uploading photos Drop files into `/var/albumen/` (or any subdirectory) via `rsync` or `scp` from your local machine: ```bash rsync -av ~/Pictures/2024-Italy/ root@albumen.jots.org:/var/albumen/2024-Italy/ # or a single file scp photo.jpg root@albumen.jots.org:/var/albumen/2024-Italy/ ``` After any upload **run the update script** (see below). --- ## Running the update script The update script walks the media tree, creates/updates `album.json` files with EXIF dates and image dimensions, and pre-generates thumbnails. ```bash # On the server — process the entire tree (skips unchanged directories) ruby /opt/albumen/scripts/update.rb # Process only one album (and its sub-albums) — always runs regardless of mtime ruby /opt/albumen/scripts/update.rb 2024-Italy # With an absolute path ruby /opt/albumen/scripts/update.rb /var/albumen/2024-Italy # Force a full rescan of everything, ignoring all change detection ruby /opt/albumen/scripts/update.rb --force ``` **Resilience guarantees — safe to interrupt and re-run at any point:** - `album.json` is written atomically (temp file + rename); no partial writes. - Unchanged directories are skipped via a `.albumen_scanned` sentinel file — a global run with nothing new typically completes in under a second. - Providing an explicit directory bypasses the sentinel for that subtree, so `update.rb some-album` always rescans that album even if nothing appears changed. - Thumbnails that already exist are skipped entirely. - EXIF metadata already recorded is not re-extracted. - Images with no EXIF data are marked `exif_absent` after the first attempt so exiftool is not re-invoked on them in subsequent rescans. - Deleted files are pruned from `album.json` automatically. Typical workflow: ```bash rsync -av ~/Pictures/trip/ root@albumen.jots.org:/var/albumen/trip/ ssh root@albumen.jots.org 'ruby /opt/albumen/scripts/update.rb trip' ``` --- ## album.json reference Each directory gets an `album.json` created by the update script. You can edit these by hand or through the admin UI at `/admin`. ```jsonc { "title": "Italy 2024", // overrides the directory name in the UI "description": "Two weeks in Rome and Florence", "cover": "DSC_0042.jpg", // specific file, or "__random__" for a random pick "visible": true, // false = hidden from non-admin users "files": { "DSC_0042.jpg": { "title": "Colosseum", // shown in lightbox (defaults to filename) "caption": "Just after sunrise, no crowds yet.", "visible": true, "taken_at": "2024-06-03T06:14:00", // from EXIF; set by update script "width": 6000, "height": 4000 } } } ``` Fields set by the update script (`taken_at`, `width`, `height`) are not overwritten if already present — safe to correct by hand. --- ## Admin interface Go to `https://albumen.jots.org/admin` and log in. From there: - Edit album title, description, cover image, visibility - Edit per-file title, caption, visibility - Navigate into sub-albums ### Changing the admin password ```bash ssh root@albumen.jots.org ruby /opt/albumen/scripts/set_password.rb ``` The PBKDF2-SHA256 hash is stored in `/opt/albumen/config.yml` (readable only by the `albumen` service user). --- ## Service management ```bash systemctl status albumen # is it running? systemctl restart albumen # restart (e.g. after editing app.rb) journalctl -u albumen -f # live service logs tail -f /opt/albumen/log/puma.stdout.log # Puma access log tail -f /opt/albumen/log/puma.stderr.log # Puma error log ``` The service runs as the `albumen` user. App code lives in `/opt/albumen/`. --- ## Thumbnail cache Thumbnails are stored in `/opt/albumen/cache/thumbs/`, mirroring the media tree. The cache is fully regenerable — delete any or all of it and the app regenerates on demand (or run the update script to pre-generate). ```bash # Regenerate all thumbnails for one album rm -rf /opt/albumen/cache/thumbs/2024-Italy ruby /opt/albumen/scripts/update.rb 2024-Italy ``` --- ## Infrastructure | Component | Location | Notes | |---|---|---| | App server | `192.168.10.245` | Puma on port 4567, nginx on 80 | | Reverse proxy | `192.168.10.1` (prouter) | Apache, handles TLS termination | | DNS | `mirkwood.jots.org` (209.141.48.158) | BIND 9, zone `/etc/bind/zones/jots.org.zone` | | TLS cert | `/etc/letsencrypt/live/albumen.jots.org/` on prouter | Expires 2026-08-07; auto-renewed by certbot | | Public URL | `https://albumen.jots.org` | → prouter → 192.168.10.245:80 | ### Sinatra settings required for the proxy setup Two settings in `app.rb` are necessary when running behind an HTTPS reverse proxy: - `set :absolute_redirects, false` — Sinatra redirects use relative paths (`/foo`) so the browser stays on HTTPS rather than following an `http://` Location header. - `set :protection, except: :http_origin` — prevents Rack::Protection from dropping the admin session when the `Origin` header's scheme (`https://`) doesn't match the backend connection scheme (`http://`). ### Cert renewal Certbot auto-renewal is managed by the system cron on prouter. To test: ```bash ssh root@192.168.10.1 'certbot renew --dry-run' ``` --- ## Re-deploying after code changes ```bash # From your workstation scp -r /home/ken/albumen/. root@albumen.jots.org:/opt/albumen/ ssh root@albumen.jots.org 'chown -R albumen:albumen /opt/albumen && systemctl restart albumen' ```