libvips · ExifTool · --dry-run everywhere · crash-safe undo
mediakit
Bulk media chores. Previewable, atomic, reversible.
One single-binary CLI to inspect, rename, clean, convert, resize, and dedup images in bulk — wrapping libvips and ExifTool behind one safe, scriptable interface.
Bulk chores, without the footguns
Five core principles, and the two safety mechanisms that make them true.
Wraps engines, never reimplements them
Pixels go to libvips, metadata to ExifTool — the fidelity oracle for MakerNotes, XMP, and ICC. mediakit is the safe orchestration layer, not a new codec.
Preview before you commit
--dry-run is global: see every resolved source→target mapping before a byte moves. It is a lower bound on failure, never a promise of success.
Crash-safe undo journal
An append-only, fsync'd JSONL journal is written before every mutation, so mediakit undo --last rolls a batch back — even after a crash mid-run.
Atomic, non-clobbering writes
Temp file on the same filesystem → fsync → rename. No-clobber by default, and an in-place edit preserves the file's exact pre-existing mode.
Signals, not verdicts
Perceptual hash, blur, colorfulness, entropy, clipping — raw scalars with documented, corpus-relative thresholds. calibrate tunes them against your own corpus.
Scriptable by design
Every read path emits -o table|json|ndjson. Compose verbs into a versioned run recipe, or let the watch daemon apply it to a directory's future arrivals.
Every destructive path runs the same spine
One planner, one journal, one undo — no verb writes files on its own.
discovery
Magic-byte kind detection; the reserved .mediakit-* temp namespace is never a source
planner
Resolves every source→target up front, so --dry-run is exact and collisions are global
executor
A bounded errgroup worker pool over the planned set
safety + journal
Temp on the same filesystem → fsync → rename, no-clobber by default, journalled before the write
engines
libvips for pixels · ExifTool for metadata — wrapped as L4 ports, never reimplemented
L1 CLI (cobra) → L2 handlers → L3 core → L4 engine ports → L5 engines. Core never imports the CLI layer; new verbs and heuristics register through the operation registry, never by editing the executor.
Twelve verbs, one safety model
Every read path emits a table and JSON. Every destructive path is previewable, atomic, and reversible.
Dimensions, format, content SHA-256, every ExifTool metadata group — plus opt-in heuristics and AI-provenance evidence.
Token-template bulk rename — {date}, {counter}, {camera}, {w}/{h}, {hash} — one global counter, collision auto-suffixed.
Strip metadata: everything, selectively by tag or group, or just GPS. ExifTool-backed and orientation-safe.
Reformat between JPEG/PNG/WebP/AVIF/HEIC with quality targets and explicit --icc assign|convert|strip.
--resize, --fit, --crop, --rotate, --flip via libvips shrink-on-load. --fit is downscale-only.
Cluster near-duplicates by perceptual-hash Hamming distance. Strictly read-only — it reports, it never deletes.
set, remove, copy (ExifTool's native -tagsFromFile), and apply from a YAML manifest.
Survey heuristic distributions across a corpus and report percentiles — corpus-relative tuning, never a verdict.
Compose verbs into a versioned YAML recipe: one preview, one confirmation, one journal batch, one undo.
An fsnotify ingest daemon that applies a recipe to a directory's future arrivals, settling each file first.
Roll back journalled batches — --last, --batch <id>, --list, --gc.
Engine detection, versions, and the real format-support matrix with per-OS install hints. Exits 3 when your install is incomplete.
config and completion round out the surface — run mediakit --help for the whole tree. Video (video probe / transcode / frames / clean) is Phase 2, parked: additive on the same spine, deliberately not queued.
Quick Start
Runtime engines aren't vendored — mediakit shells out to them, anddoctoris the single source of truth for "is my install complete".
# Debian / Ubuntu
sudo apt install libimage-exiftool-perl libvips-tools
# macOS
brew install exiftool vips# Build the static binary into bin/ (pure Go, CGO disabled)
make build
# Verify the runtime engines are present (libvips + ExifTool for v1)
./bin/mediakit doctor
# Inspect an image, add low-level heuristics, emit JSON
./bin/mediakit inspect photo.jpg --heuristics -o json
# Preview a bulk rename before touching anything (dry-run is global)
./bin/mediakit rename ./shoot -r --template '{date:2006-01-02}_{counter:04}' --dry-run
# Run the tests (unit + golden + testscript e2e; engine tests self-skip if absent)
make testv0.1.0 is tagged and released, but the source repository is still private — the release tarballs, the .deb/.rpm packages, and the GHCR image all sit behind that same access control, andgo install github.com/ArmandoHerra/mediakit/cmd/mediakit@latestwill not resolve without it. Building from source is the canonical install path today. Full detail in theREADME's Installation section.
A stable exit-code contract
The same contract mediakit --help prints — a host with the binary and no network can discover it from the tool.
0
success
1
partial failure (some bulk items failed)
2
usage error
3
required engine missing or too old
4
no files matched
5
aborted (declined confirmation / --fail-fast)
Technology Stack
Every version below, straight fromthe README's Tech Stack table.
| Technology | Version | Purpose |
|---|---|---|
| Go | 1.25+ (toolchain 1.26) | language/runtime |
| spf13/cobra | 1.10 | CLI command tree |
| spf13/viper | 1.21 | config file + MEDIAKIT_* env + profiles |
| golang.org/x/sync | 0.22 | bounded worker pool (errgroup.SetLimit) |
| golang.org/x/image | 0.45 | WebP/TIFF decoders for the engine-free dimension probe and dedup's full decode |
| corona10/goimagehash | 1.1 | perceptual hashing (aHash/dHash/pHash) |
| davidbyttow/govips/v2 | 2.18 | optional in-process libvips (-tags govips) |
| rogpeppe/go-internal | 1.14 | testscript (txtar) CLI e2e tests |
| libvips (runtime) | ≥ 8.14 | pixel engine (default: vips CLI subprocess) |
| ExifTool (runtime) | ≥ 12.60 | metadata engine |