- Rust 39.8%
- TypeScript 35.1%
- Svelte 17.9%
- CSS 4.4%
- Shell 2.3%
- Other 0.5%
| backend | ||
| docs | ||
| frontend | ||
| infra | ||
| mk | ||
| .browserstack.yml | ||
| .dockerignore | ||
| .flake8 | ||
| .gitignore | ||
| albumz.example.toml | ||
| backlog-methodology.md | ||
| CONVENTIONS.md | ||
| date_taken | ||
| Dockerfile | ||
| implementation-questions.md | ||
| README.md | ||
| TODO.md | ||
| todo.md | ||
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:
<target-host>: ssh/comms host alias for the VPS<port>: backend listen port behind nginx on the VPS<force>: currently parsed but not used by the wrapper<clean>:1runs./mk/cleanon the VPS first,0leaves existing state alone<state-dir>: persistent host directory mounted at/app/state; this is where the SQLite database and sidecars live<library-root>: persistent host photo root mounted read-only and passed into the app asAPP_LIBRARY_ROOT<domain>: public nginx/https domain[display-name]: optional app display name; defaults toAlbumz
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_passwordin a config file
Runtime config
The app reads runtime settings from environment variables and an optional config file.
Config file search order:
APP_CONFIGalbumz.tomlconfig/albumz.toml- XDG config path for Albumz
Useful environment variables:
APP_NAMEAPP_DOMAINAPP_APIAPP_HOST_IPAPP_PORTorPORTAPP_STATIC_DIRAPP_PUBLIC_DIRAPP_LIBRARY_ROOTAPP_DATA_DIRAPP_DATABASE_PATHAPP_ADMIN_PASSWORDAPP_MAGICK_BINAPP_FFMPEG_BINAPP_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:
- Stop the server.
- Remove the metadata sidecar cache.
- 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_PATHor[storage].database_path - sidecar path:
[storage].sidecar_diror the defaultAPP_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_CONFIGalbumz-client.tomlconfig/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,--playerAPP_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_CONFIGAPP_CLIENT_CONFIGalbumz-tui.tomlalbumz-client.tomlconfig/albumz-tui.tomlconfig/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 selectionh: go to parent directorylorEnter: enter selected album or open selected media inmpvp: play the current album recursively inmpvr: refresh current directoryq: 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.mddocs/TESTING.mddocs/service-worker.mddocs/types.md