No description
  • Rust 39.8%
  • TypeScript 35.1%
  • Svelte 17.9%
  • CSS 4.4%
  • Shell 2.3%
  • Other 0.5%
Find a file
2026-06-10 18:16:32 -07:00
backend working checkpoint, but animations lost 2026-06-09 14:52:52 -07:00
docs working checkpoint, but animations lost 2026-06-09 14:52:52 -07:00
frontend slight gallery load improvement 2026-06-10 18:16:32 -07:00
infra initial todo 2026-04-12 01:08:00 -07:00
mk refactor for size 2026-04-29 11:06:21 -07:00
.browserstack.yml initial todo 2026-04-12 01:08:00 -07:00
.dockerignore initial todo 2026-04-12 01:08:00 -07:00
.flake8 initial todo 2026-04-12 01:08:00 -07:00
.gitignore initial todo 2026-04-12 01:08:00 -07:00
albumz.example.toml working implementation 2026-04-16 13:37:27 -07:00
backlog-methodology.md streaming performance 2026-04-18 04:08:46 -07:00
CONVENTIONS.md initial todo 2026-04-12 01:08:00 -07:00
date_taken initial implementation plan 2026-04-12 18:55:08 -07:00
Dockerfile working implementation 2026-04-16 13:37:27 -07:00
implementation-questions.md initial implementation plan 2026-04-12 18:55:08 -07:00
README.md performance of gallery 2026-04-23 12:57:15 -07:00
TODO.md initial todo 2026-04-12 01:08:00 -07:00
todo.md ui checkpoint 2026-05-05 10:36:23 -07:00

Albumz

Albumz is a single-user photo library app built for a small VPS.

  • frontend/ is a Svelte app that builds to static files.
  • backend/ is an Axum server that serves the frontend, scans the library, and exposes the admin API.
  • the production shape is one Rust process behind nginx on one public domain.

The current MVP supports owner login, library scanning, filesystem-first album browsing, generated image/HEIC previews, video posters and lightweight playback transcodes, direct file downloads with HTTP range support, explicit image/video download quality presets, album/share download manifests, cached ZIP archive downloads for album/share scopes, extracted media metadata with location fields, first map surfaces for selected media and current album subtrees, scan issue visibility, safe deletion of exact symlink entries, symlink-only subset directory creation, API key management, a local server-side admin CLI, a separate remote client CLI over API keys, a separate interactive remote TUI, mpv-oriented viewer handoff, and public share links for albums or individual media.

Current status

  • map hotness, time filters, and radius-gallery heuristics are not implemented yet

The on-server admin CLI now exists as albumz-admin. The scriptable remote client exists as albumz-client for API-key-authenticated browsing, share management, and mpv-oriented viewer handoff. The separate interactive remote browser exists as albumz-tui for list-first filesystem traversal.

Project layout

frontend/             Svelte app
backend/              Axum app and scanner
docs/                 implementation notes and supporting docs
mk/                   build, test, and deploy helpers
Dockerfile            production image build
albumz.example.toml   example runtime config

Local development

Frontend

cd frontend
npm install
npm run dev

Backend

cd backend
APP_NAME="Albumz" \
APP_DOMAIN="http://localhost:5173" \
APP_API="http://localhost:8000" \
cargo run

Open http://localhost:5173.

Production-like local run

cd frontend
npm run build

cd ../backend
APP_NAME="Albumz" \
APP_DOMAIN="http://localhost:8000" \
APP_API="http://localhost:8000" \
cargo run

Open http://localhost:8000.

The backend writes /static/config.json at runtime and serves frontend/build/ unless APP_PUBLIC_DIR is overridden.

Docker-based validation

Run the end-to-end docker wrapper:

./mk/test

That wrapper builds the frontend, builds the albumz image, starts the container, runs backend tests, and then runs frontend checks plus Playwright.

Single-VPS deploy flow

The deploy helper for the current single-host shape is:

./mk/deploy-single-vps <target-host> <port> <force> <clean> <state-dir> <library-root> <domain> [display-name]

Example shape:

./mk/deploy-single-vps my-vps 8013 0 0 /root/.local/share/albumz /srv/photos photos.example.net

Argument meaning:

  1. <target-host>: ssh/comms host alias for the VPS
  2. <port>: backend listen port behind nginx on the VPS
  3. <force>: currently parsed but not used by the wrapper
  4. <clean>: 1 runs ./mk/clean on the VPS first, 0 leaves existing state alone
  5. <state-dir>: persistent host directory mounted at /app/state; this is where the SQLite database and sidecars live
  6. <library-root>: persistent host photo root mounted read-only and passed into the app as APP_LIBRARY_ROOT
  7. <domain>: public nginx/https domain
  8. [display-name]: optional app display name; defaults to Albumz

Important deployment notes:

  • the wrapper expects an existing LetsEncrypt certificate for the target domain
  • the current runtime expects exactly one library root in this deploy path
  • the Docker image and container name now follow the Rust package name: albumz

Initial login

Default admin password:

changeme

Override it with either:

  • APP_ADMIN_PASSWORD
  • [auth].admin_password in a config file

Runtime config

The app reads runtime settings from environment variables and an optional config file.

Config file search order:

  1. APP_CONFIG
  2. albumz.toml
  3. config/albumz.toml
  4. XDG config path for Albumz

Useful environment variables:

  • APP_NAME
  • APP_DOMAIN
  • APP_API
  • APP_HOST_IP
  • APP_PORT or PORT
  • APP_STATIC_DIR
  • APP_PUBLIC_DIR
  • APP_LIBRARY_ROOT
  • APP_DATA_DIR
  • APP_DATABASE_PATH
  • APP_ADMIN_PASSWORD
  • APP_MAGICK_BIN
  • APP_FFMPEG_BIN
  • APP_FFPROBE_BIN

See albumz.example.toml for the fuller shape.

The default Docker runtime now includes ffmpeg and ImageMagick. Outside Docker, make sure the configured tool paths resolve correctly on the server host.

Resetting stored photo dates

Albumz does not use a migration system, so there are no schema migrations to run or delete.

If you previously scanned with older capture-time logic and your photo order still looks wrong, you usually do not need to delete the SQLite database first.

Recommended recovery path:

  1. Stop the server.
  2. Remove the metadata sidecar cache.
  3. Start the server and run a fresh scan.

By default, that means removing:

  • database: <APP_DATA_DIR>/albumz.sqlite3
  • sidecars: <APP_DATA_DIR>/sidecars/metadata/

The metadata cache is stored as a sharded directory tree under sidecars/metadata/ rather than one flat directory, so deleting that top-level metadata/ directory clears the full cache.

More exactly:

  • database path: APP_DATABASE_PATH or [storage].database_path
  • sidecar path: [storage].sidecar_dir or the default APP_DATA_DIR/sidecars

If you only want Albumz to recompute extracted capture times, delete the sidecar metadata cache and rescan. That is the preferred fix.

If you want a full clean rebuild of local state for a personal/no-history setup, you can delete both the SQLite database and the metadata sidecar cache, then start the app and scan again.

Example reset flow:

rm -rf /path/to/state/sidecars/metadata
# optional full reset:
rm -f /path/to/state/albumz.sqlite3

After either reset path, run a fresh scan so Albumz repopulates ordering and metadata from the current library.

Local admin CLI

albumz-admin is the local VPS-side admin tool. It reads the same config and environment variables as the server process and operates directly on the configured library root and SQLite database.

Examples:

cd backend

cargo run --bin albumz-admin -- status
cargo run --bin albumz-admin -- scan
cargo run --bin albumz-admin -- albums tree
cargo run --bin albumz-admin -- albums browse --path subsets/spring
cargo run --bin albumz-admin -- shares list
cargo run --bin albumz-admin -- shares create subsets/spring
cargo run --bin albumz-admin -- symlinks delete subsets/spring/phone.jpg
cargo run --bin albumz-admin -- subsets create --parent-path subsets --name picked-phone --target-path 2024/04/20240401120000-phone.jpg
cargo run --bin albumz-admin -- doctor --limit 20

albums browse shows direct directory contents by default. Add --recursive when you explicitly want the current subtree flattened.

JSON output is available on every command via --json:

cargo run --bin albumz-admin -- --json status

Remote client CLI

albumz-client is the separate remote-friendly CLI that talks to the deployed API over bearer API keys instead of requiring shell access to the VPS.

The client can read configuration from:

  • --config <path>
  • APP_CLIENT_CONFIG
  • albumz-client.toml
  • config/albumz-client.toml
  • XDG config path albumz/client.toml

Minimal config file shape:

server = "https://albums.example.net"
api_key = "alb_..."
player = "mpv"

You can also pass the values directly via --server, --api-key, --player, APP_CLIENT_URL, APP_CLIENT_API_KEY, and APP_CLIENT_PLAYER.

Examples:

cd backend

cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' status
cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' albums tree
cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' albums browse --path subsets/spring
cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' media show --path subsets/spring/phone.jpg
cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' shares list
cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' shares create subsets/spring
cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' shares revoke 12
cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' view media --path subsets/spring/phone.jpg
cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' view album --path subsets/spring

albums browse is also direct-by-default in the remote client. Add --recursive when you want recursive media under the current path.

JSON output is available on every command via --json.

If you want to inspect the exact mpv invocation without launching it, use --print-command:

cargo run --bin albumz-client -- --server https://albums.example.net --api-key 'alb_...' view album --path subsets/spring --print-command

Remote TUI

albumz-tui is the separate interactive remote browser. It keeps the traversal model list-first and filesystem-like instead of overloading albumz-client.

It reuses the same remote connection fields as albumz-client:

  • --server, --api-key, --player
  • APP_CLIENT_URL, APP_CLIENT_API_KEY, APP_CLIENT_PLAYER
  • --config <path>

Config file search order is TUI-aware first, then falls back to the remote client config shape:

  • --config <path>
  • APP_TUI_CONFIG
  • APP_CLIENT_CONFIG
  • albumz-tui.toml
  • albumz-client.toml
  • config/albumz-tui.toml
  • config/albumz-client.toml
  • XDG albumz/tui.toml
  • XDG albumz/client.toml

Example:

cd backend

cargo run --bin albumz-tui -- --server https://albums.example.net --api-key 'alb_...'

Key bindings in the first TUI slice:

  • j / k: move selection
  • h: go to parent directory
  • l or Enter: enter selected album or open selected media in mpv
  • p: play the current album recursively in mpv
  • r: refresh current directory
  • q: quit

Checks

Frontend:

cd frontend
npm run check
npm run test:unit -- --run
npm run build

Backend:

cd backend
cargo test

More docs

  • docs/api.md
  • docs/TESTING.md
  • docs/service-worker.md
  • docs/types.md