The challenge
Movie collectors keep films and series spread across several external hard drives, so the collection can only be browsed with every drive plugged in, and messy release filenames defeat most media-server matching.
The approach
Built a local-first Tauri 2 (Rust) desktop app with a React/TypeScript UI over a Rust workspace of focused crates.
What shipped- A release-name parser
- Stable drive identity with plug/unplug detection
- An incremental SQLite catalog scanner
- Ffprobe media info with ffmpeg frame thumbnails
- An mpv player embedded in the app window over JSON IPC
A Next.js and MongoDB API on Vercel adds Google sign-in with rotating refresh tokens, a cached TMDB metadata proxy and push/pull sync of watch history.
The outcome
One Netflix-style library across every drive that stays browsable while the drives are unplugged and plays 4K Dolby Vision and TrueHD at full quality. The filename parser reads 98.5% of 459 real files correctly and 97.2% of titles match TMDB automatically, with a marketing and download site alongside.
How Shutter is put together
A local-first Tauri 2 app built as a Rust workspace of focused crates. The catalog is SQLite on the user’s machine and keeps working with every drive unplugged; a small Next.js API on Vercel adds Google sign-in, a cached TMDB proxy and cross-device sync.
- filenames parsed correctly (459 real files)
- 98.5%
- titles matched to TMDB automatically
- 97.2%
- Rust crates in the workspace
- 5
- confidence needed to auto-match
- ≥ 0.85
01 · Desktop UI
client
Library UI
React · TypeScript · Vite
02 · Rust core
gateway
Tauri core
commands, artwork:// protocol
service
Player
mpv · JSON IPC
embedded via --wid
03 · Engine crates
worker
Drive watcher
stable identity, USB + SMB
service
Scanner & matcher
incremental, one transaction
service
Release-name parser
worker
Media probe
ffprobe · ffmpeg
HDR, audio, thumbnails
04 · Local
database
SQLite catalog
rusqlite
object store
Artwork cache
app data, offline
third-party
Drives & shares
read-only while scanning
05 · Cloud
service
Shutter API
Next.js · Vercel
auth, meta proxy, sync
database
MongoDB
accounts, sync, TTL meta cache
third-party
TMDB
third-party
Google OAuth
PKCE, loopback
14 connections
- Library UI→Tauri coreinvoke
- Tauri core→Scanner & matcher
- Tauri core→Playerplay file
- Drive watcher→Scanner & matcherplug events · async
- Scanner & matcher→Release-name parserfilenames
- Scanner & matcher→Media probemedia info · async
- Scanner & matcher→SQLite catalogtransaction
- Scanner & matcher→Drives & shareswalk
- Player→Drives & sharesstream
- Tauri core→Artwork cache
- Tauri core→Shutter APImeta · sync · auth
- Tauri core→Google OAuthPKCE sign-in
- Shutter API→MongoDB
- Shutter API→TMDBcached proxy
Request flow · Plugging in a drive
- 1The drive watcher recognises the disk by a stable identity rather than its letter or mount point and raises a plug event.
- 2The scanner walks the drive; files with an unchanged path, size and mtime are only marked seen. New or changed files are quick-hashed and parsed.
- 3A vanished file and a new one with the same quick hash are a move: the row keeps its id and its watch history. The whole scan is one transaction, so pulling the drive mid-scan leaves the catalog untouched.
- 4ffprobe adds resolution, HDR and audio tracks; titles are matched through the API’s cached TMDB proxy (auto at ≥ 0.85 confidence, review from 0.60) and posters land in the local artwork cache.
Design decisions
- Scanning never writes to drives
- Catalog, artwork and thumbnails live in app data, so drives work read-only and duplicate copies share one image. The optional organizer renames files only after a dry-run plan, with an operation log and one-click undo.
- History follows the title, not the file
- Progress and watch counts attach to the movie or episode, so a copy on another drive shares the same history.
- A name alone is never proof
- Matching scores TMDB candidates against parsed names and probed media details, with thresholds for auto-match, review and unmatched.
- Conflict rules per field
- Sync uses last-write-wins for playback position, max for last-watched and append-only for watch events.
- Secrets stay server-side
- The TMDB key never ships in the app; the API proxies and caches responses in MongoDB with a TTL. Sign-in uses PKCE, so an intercepted loopback code is useless alone.
Trade-offs & known limits
- macOS code paths for drive detection and SMB are written but not yet run on a Mac.
- mpv runs as a child process: robust playback for every format, at the cost of bundling the binary.
Have something similar in mind?
I'm open for freelance and contract work — let's talk about what you're building.