aboutsummaryrefslogtreecommitdiffstats

MedMan — Self-Hosted Media Manager with Tagging & squashr Integration

MedMan is a web-based media manager designed to work alongside squashr (SquashFS/OverlayFS/LUKS backup tool).

Overview

1. Drop files into source directories (or upload via web UI)
       ↓
2. MedMan scans/ingests → files are distributed into data directories
   using a full-hash directory hierarchy (every 2 hex chars = one level)
       ↓
3. Browse & tag files through the web interface
       ↓
4. Export tags → archival.py packs them into SquashFS containers
       ↓
5. Burn containers to CD/DVD/Blu-Ray or archive offline
       ↓
6. Offline use: mount container, start MedMan, import index.json →
   browse & search media with full tag metadata

Configuration

MedMan is configured primarily through Docker volume mounts. Environment variables are minimal:

Variable Description Default
MEDMAN_PORT HTTP port 8080
MEDMAN_HOST Bind address 0.0.0.0
MEDMAN_W Comma-separated weights for data-dir distribution equal weight

Directory Structure

All directories are inside /app in the container and wired via docker-compose.yml mounts:

/app/
├── srcs/           ← source directories (immediate subdirs)
│   ├── src0/       ← mount: ./src0:/app/srcs/src0
│   └── src1/       ← mount: ./src1:/app/srcs/src1
├── dats/           ← data directories (immediate subdirs)
│   ├── data0/      ← mount: ./data0:/app/dats/data0
│   └── data1/      ← mount: ./data1:/app/dats/data1
├── imports/        ← auto-import directories (immediate subdirs, read-only)
│   └── my-archive/ ← mount: /mnt/archive:/app/imports/my-archive:ro
└── db/             ← SQLite database + thumbnails (auto-created)

Key principle: Every immediate subdirectory of srcs/, dats/, and imports/ is automatically discovered. No comma-separated path lists in environment variables.

Data Distribution Weights

MEDMAN_W controls how new files are distributed across data directories:

# 3 data dirs — dir 0 gets 50%, dirs 1 and 2 get 25% each
MEDMAN_W=2,1,1

If not set, distribution is equal across all data directories.

Hash-Based Storage

Every file is SHA-256 hashed. All 32 byte-pairs of the 64-character hex hash become directory levels:

Hash:  a1b2c3d4e5f6...a1b2c3d4e5f6 (64 chars)
Path:  data0/a1/b2/c3/d4/e5/f6/.../a1/b2/c3/d4/e5/f6
  • 32 directory levels + filename (last hex pair)
  • Automatic deduplication via content hashing
  • Distribution across data directories according to MEDMAN_W weights
  • No rel column stored in the database — the path is always deterministically computed from the hash

Quickstart (Docker)

cd medman
docker compose up -d

MedMan is available at http://localhost:8080.

Drop files into src0/ or src1/ — they are automatically scanned and distributed into the data directories on startup. Upload is also supported via the web UI.

Auto-Import on Startup

Directories mounted under /app/imports/ are scanned on startup (and on each manual scan). Each subdirectory may contain:

  • index.json — a MedMan export file with tags and file references
  • Files in the canonical hash-prefix hierarchy (same layout as data dirs)

All tags and file registrations from index.json are imported automatically if the referenced files exist on disk. Import directories receive zero distribution weight (new files are never placed there).

Example: Mount a read-only SquashFS container containing an index.json and canonical file paths:

# docker-compose.yml
volumes:
  - /mnt/my-archive:/app/imports/my-archive:ro

Features

  • Source directories: files auto-ingested on startup, upload via UI
  • Manual scan button: re-scan source and import directories anytime
  • Hash-based storage: content-addressable, no duplicates
  • Multiple data directories: weighted random distribution
  • Move: relocate individual files or selections between data dirs
  • Tag system: plain labels, no color coding
  • Tag search: combined AND search across multiple tags
  • Bulk operations: select-all, batch tag/untag/move/delete
  • Previous/Next navigation: arrow keys or buttons in detail view
  • Checkbox selection: click the checkbox on any thumbnail to toggle
  • Export/Import: tags as JSON (export/import)
  • archival.py: create SquashFS + LUKS containers directly from MedMan
  • Container mount API: mount SquashFS containers as additional data dirs

Database Schema

tags(id, name, ts)
files(hash PK, didx, size, mt, w, h, dur, ts)
ftags(id, hash FK→files, tid FK→tags, UNIQUE(hash, tid))

didx is the index into the dats/ subdirectory list. The on-disk path is always dats/<name>/<h2rel(hash)>.

API

Endpoint Method Description
/api/files GET All files (?tag= filter)
/api/files/<hash> DELETE Delete file
/api/files/<hash>/move POST {"didx":N} move to data dir
/api/files/<hash>/tags GET/POST Get or add tags
/api/files/<hash>/tags/<id> DELETE Remove tag from file
/api/tags GET/POST List or create tags
/api/tags/<id> DELETE Delete tag
/api/tags/export GET Full tag export (?tags=a,b AND filter)
/api/tags/import POST Import tag JSON
/api/search?tags=a,b GET AND tag search
/api/upload POST Upload file(s)
/api/scan POST Scan sources + imports
/api/dat GET Data directory stats
/api/containers/mount POST Mount container dir as data dir
/api/containers/mount DELETE Unmount container dir
/thumb/<hash> GET JPEG thumbnail
/raw/<hash> GET Original file

Export Format (index.json)

{
  "ts": "2025-01-15T12:00:00+00:00",
  "tags": [{"id": 1, "name": "example"}],
  "files": {
    "a1b2c3d4...": {"didx": 0, "tags": ["example"]}
  }
}

The on-disk path for each file is deterministic: h2rel(hash) → 32 hex-pair directory levels. No rel field is needed.

archival.py — Creating SquashFS Containers

# Build containers via MedMan API
python3 archival.py --medman-url http://localhost:8080 \
    --dat /app/data0 /app/data1 \
    --tags vacation 2024 \
    --container-size 4700372992 \
    --output-prefix ./archive

# Build with LUKS encryption
python3 archival.py --db /app/db/medman.db \
    --dat /app/data0 /app/data1 \
    --tags photos \
    --container-size 25000000000 \
    --output-prefix ./photos \
    --cryptsetup --key-file /path/to/keyfile

# Direct DB access (no running MedMan needed)
python3 archival.py --db /app/db/medman.db \
    --dat /app/data0 /app/data1 \
    --container-size 10000000000 \
    --output-prefix ./offline

The script uses tar → sqfstar piping (no staging directories, no temp files beyond a single index.json) — exactly like squashr's build_squash_image_tar_sqfstar.

Container Size Margin

A margin of 1 MiB is reserved per container for the index.json and filesystem metadata overhead. Adjust with --margin if needed.

Filter Options

Option Description
--tags a b Only files with ALL given tags (AND)
--hashes a1b2... Specific hashes
--hashes-from-file Read hashes from file
--from-date 2024-01-01 Modified on or after
--to-date 2024-12-31 Modified on or before
--mime-type video Filter by media type

squashr Integration

MedMan containers produced by archival.py are standard SquashFS images with an index.json at the root plus the canonical hash hierarchy. They can be mounted and imported into any MedMan instance via the auto-import feature or the container mount API.

License

GNU AGPLv3