# Mulita - Self-Hosted Photo Management Application A self-hosted, Docker-deployed photo management application inspired by Lightroom's workflow. Mulita provides a fast, keyboard-driven interface to browse, organize, tag, and manage your photo library. ## Features - **Photo Organization**: Browse photos in a timeline view with virtual scrolling for performance - **Thumbnail Generation**: Automatic thumbnail generation for all photo formats including RAW - **Metadata Extraction**: Full EXIF/XMP metadata extraction and search - **Keyboard Shortcuts**: Lightroom-style keyboard navigation and actions - **File Support**: JPEG, PNG, RAW formats (CR2, CR3, NEF, ARW, etc.), HEIC/HEIF, and videos - **Heaps**: Temporary collections for organizing photos - **Tags & Ratings**: Organize with tags, star ratings, and color labels - **Dark Mode**: Photography-optimized dark interface ## Tech Stack ### Backend - Python 3.12 with FastAPI - SQLite with SQLAlchemy (async) - Celery + Redis for background tasks - pyvips for fast thumbnail generation - ExifTool for metadata extraction ### Frontend - React 18 with TypeScript - Vite for fast development - TanStack Query for data fetching - TanStack Virtual for virtualized scrolling - Tailwind CSS for styling - Zustand for state management ## Quick Start ### Prerequisites - Docker and Docker Compose ### Setup (one variable) 1. Clone the repo: ```bash git clone cd muleimage ``` 2. Set **one** environment variable in `.env` — the **host** directory that contains your photo library. Whatever you point at will become your library inside Mulita. ```bash # macOS / Linux PHOTO_DIRS=/Users/you/Pictures # or any folder PHOTO_DIRS=/mnt/nas/photos # Windows (WSL) PHOTO_DIRS=/mnt/c/Users/you/Pictures ``` 3. Start the stack: ```bash docker compose up -d ``` 4. Open `http://localhost:3000`. On first boot Mulita will: - Mount your `PHOTO_DIRS` at `/photos` inside the container - Auto-create a source root called **Library** pointing at `/photos` - Queue an initial scan, generate thumbnails, and start serving them You don't need to touch `mulita.yml` or the API to get started. ### How "mounted folders" and "source folders" relate There are two layers, and confusing them is the most common source of "why doesn't this work" questions: | Layer | Lives in | What it controls | |---|---|---| | **Mount** | `docker-compose.yml` (`${PHOTO_DIRS}:/photos:rw`) | What's *visible* inside the container | | **Source root** | Database (managed by the UI) | What the scanner *walks and indexes* | Both are required. A folder that isn't mounted is invisible to the container regardless of what the database says, and a folder that's mounted but not registered as a source root won't be scanned. In practice, the default flow handles this for you: you mount one host directory via `PHOTO_DIRS`, and the bootstrap automatically registers it as a source root. If you want to add a *subfolder* of your library as a separate source root (so it shows up as its own item in the sidebar), use the **Add Source Folder** button — the dialog is a directory browser restricted to what's mounted, so you can only add things the container can actually see. ### Adding more libraries Today the compose file mounts a single host directory as `/photos`. If you want multiple libraries from different host paths, edit `docker-compose.yml` and add additional mount lines, e.g.: ```yaml volumes: - ${PHOTO_DIRS}:/photos:rw - /Volumes/Archive:/archive:rw # additional library ``` Then in the UI, click **Add Source Folder**, browse to `/archive`, and add it. (Multi-mount support via env vars is on the roadmap.) ### Read-only libraries The default mount is `:rw` because file operations (rename, move, empty discard pile) need to mutate the filesystem. If you want a strict read-only library — pointing at a network share, an authoritative archive, etc. — flip `:rw` to `:ro` in `docker-compose.yml`. Mulita will keep working for browsing, rating, color labels, picks, heaps, and the (soft) discard flag, but the following will return an OS error: - `PATCH /photos/{id}` with a new `filename` (rename) - `POST /photos/move` (bulk move) - `DELETE /discard/empty` (file unlinks) **Heads up**: with `:rw`, Mulita has full write access to whatever host directory you mount. Treat the same way you would Lightroom's catalog folder. ## Architecture The application consists of 5 Docker services: - **frontend**: React SPA served by Nginx - **backend**: FastAPI REST API - **worker**: Celery workers for background tasks - **redis**: Message broker for Celery - **db**: SQLite database (file-based) ## Keyboard Shortcuts | Key | Action | |-----|--------| | `←` `→` `↑` `↓` | Navigate photos | | `Space` | Quick preview | | `Enter` | Open loupe view | | `P` | Pick photo | | `X` | Reject photo | | `1-5` | Set star rating | | `Tab` | Toggle left sidebar | | `I` | Toggle metadata panel | | `G` | Grid view | | `E` | Loupe view | | `Delete` | Move to trash | ## Development ### Backend Development ```bash cd backend pip install -r requirements.txt uvicorn app.main:app --reload ``` ### Frontend Development ```bash cd frontend npm install npm run dev ``` ## Configuration Source roots are managed by the UI / API (the database owns them). Edit `mulita.yml` to configure operational settings only: - Thumbnail sizes, quality, and format - Scanner behaviour (watch, batch size, initial scan) - Performance tuning (concurrency, cache TTLs, DB pool) ## Performance - Handles 100,000+ photos efficiently - Virtual scrolling for smooth timeline navigation - Thumbnail generation at 10+ photos/second - SQLite FTS5 for fast full-text search ## Future Features (Phase 2) - AI-powered scene classification - Face detection and clustering - Smart albums - Duplicate detection - Export presets - Multi-user support ## License MIT