Files
mule-image/README.md
dtoro 204d2bf2a8 feat: simplify folder setup — single mount, auto bootstrap, browser dialog
Cleans up the maze of overlapping ways folders entered the app, plus
removes the dead trash plumbing left over from the soft-discard
refactor.

Setup model (now)
- ONE env var: PHOTO_DIRS in .env, set to the host path of your
  library. Compose mounts that at /photos. That's the entire setup.
- On first boot, the backend auto-creates a SourceRoot row named
  "Library" pointing at /photos so the user sees their photos
  immediately without configuring anything.
- Source roots and discard live in the database; mulita.yml only
  carries operational settings (thumbnails, scanner, performance).
- The "Add Source Folder" dialog is now a directory browser
  restricted server-side to /photos and any existing source root —
  the user clicks through actual mounted directories instead of
  typing container paths they can't possibly know.

Backend
- New services/scanner.bootstrap_default_source_root(): if no
  SourceRoot rows exist and /photos is mounted, create one. Wired
  into the lifespan handler before cleanup + initial scan.
- New GET /library/browse?path= returning the immediate child
  directories of `path`, validated to live under one of the allowed
  roots (default mount + every active SourceRoot). Hidden entries
  are filtered. Children are tagged with is_existing_root so the UI
  can show an "Added" badge. Returns parent path for up-nav, or
  null when at the top of the allowed scope.
- scan_all_source_roots now reads from the DB instead of the YAML
  config so DB-managed source roots are honoured by initial scan.
- Dropped the placeholder source_roots block from mulita.yml — the
  paths /photos/main and /photos/iphone never existed and just
  produced startup warnings.
- Dropped TrashSettings, settings.trash, settings.source_roots,
  and the SourceRoot pydantic model from config.py. Soft discard
  has owned this for a while; it was dead code.

Compose
- Single ${PHOTO_DIRS:-./photos}:/photos:rw mount in both backend
  and worker.
- Removed the hardcoded ~/Pictures:/host/Pictures:rw mount — the
  PHOTO_DIRS variable is the single source of truth now.
- Removed the trash_data named volume + mounts (no consumers).
- backend/Dockerfile no longer creates /data/trash; it now creates
  /data/proxies (which the proxy endpoint actually uses).

Frontend
- AddSourceFolderDialog rewritten as a directory tree picker:
  loads /library/browse on open, lets the user navigate up via a
  ChevronUp button or down by clicking subfolders, shows the
  current path inline, and adds whatever directory is currently
  shown. Existing source roots are tagged "Added" so the user
  knows what's already registered. Errors from the backend (e.g.
  trying to navigate outside the allowed scope) surface inline.
- New library.browse() helper + BrowseChild / BrowseResponse types
  in services/api.ts.

Docs
- README Quick Start rewritten around the single PHOTO_DIRS env
  var, with macOS/Linux/Windows examples.
- New "How mounted folders and source folders relate" section that
  spells out the two-layer model (mount = visibility, source root
  = scanning) so the most common confusion is addressed up front.
- Added a "Read-only libraries" subsection that lists exactly which
  endpoints fail under :ro.
- "Configuration" section reframed: source roots are managed by the
  UI/API now, mulita.yml is operational settings only.
- .env file now has examples for the common host paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:27:21 +02:00

5.8 KiB

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:
git clone <repository-url>
cd muleimage
  1. Set one environment variable in .env — the host directory that contains your photo library. Whatever you point at will become your library inside Mulita.

    # macOS / Linux
    PHOTO_DIRS=/Users/you/Pictures
    
    # or any folder
    PHOTO_DIRS=/mnt/nas/photos
    
    # Windows (WSL)
    PHOTO_DIRS=/mnt/c/Users/you/Pictures
    
  2. Start the stack:

docker compose up -d
  1. 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.:

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

cd backend
pip install -r requirements.txt
uvicorn app.main:app --reload

Frontend Development

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