███████╗██████╗ ██████╗ ███████╗████████╗███████╗ ██████╗██████╗ ██╗██████╗ ███████╗
██╔════╝██╔══██╗██╔═══██╗██╔════╝╚══██╔══╝██╔════╝██╔════╝██╔══██╗██║██╔══██╗██╔════╝
█████╗ ██████╔╝██║ ██║███████╗ ██║ ███████╗██║ ██████╔╝██║██████╔╝█████╗
██╔══╝ ██╔══██╗██║ ██║╚════██║ ██║ ╚════██║██║ ██╔══██╗██║██╔══██╗██╔══╝
██║ ██║ ██║╚██████╔╝███████║ ██║ ███████║╚██████╗██║ ██║██║██████╔╝███████╗
╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═════╝ ╚══════╝
❄ disc to library · natively ❄
A native macOS tool for ripping and preserving physical disc media to a local Jellyfin, Plex, or Kodi library.
Frostscribe wraps makemkvcon and HandBrakeCLI into a polished interactive CLI and menu bar app. Insert a disc, confirm the title, and walk away. The background worker handles encoding while you use your Mac normally.
| Feature | Status |
|---|---|
| Movie ripping — Vigil Mode | ✅ Stable |
| Movie encoding (H.265, hardware) | ✅ Stable |
| Background worker (rip + encode) | ✅ Stable |
| Menu bar app + GUI rip flow | ✅ Stable |
| CLI rip flow | ✅ Stable |
| Event hooks (Home Assistant, etc.) | ✅ Stable |
| TV show ripping | ✅ Stable |
| AutoScribe (auto-rip without prompting) |
brew install handbrake- VLC — optional, for in-app disc preview. To play encrypted Blu-rays in VLC, also install
libaacsand place aKEYDB.cfgkeys database at~/.config/aacs/KEYDB.cfg:
brew install libaacs
mkdir -p ~/.config/aacs
# Download KEYDB.cfg from vlc-bluray.whoknowsmy.name and place it at ~/.config/aacs/KEYDB.cfgHomebrew (recommended):
brew tap trevholliday/frostscribe
brew install frostscribeThis installs the frostscribe CLI, frostscribe-worker background daemon, and registers the LaunchAgent so the worker starts automatically on login.
Menu bar app:
Download FrostscribeUI.app from the latest release and move it to /Applications.
Build from source:
git clone https://github.com/trevholliday/frostscribe
cd frostscribe
swift build -c release
sudo cp .build/release/frostscribe /usr/local/bin/
sudo cp .build/release/frostscribe-worker /usr/local/bin/Run the setup wizard on first use:
frostscribe initThis creates ~/Library/Application Support/Frostscribe/config.json with your output directories, media server selection, and optional API keys.
Then install and start the background worker:
cp com.frostscribe.worker.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.frostscribe.worker.plistWhy the worker runs as a launchd agent
Encoding is slow — a Blu-ray can take 30–90 minutes. The worker runs as a persistent background service managed by launchd so it survives terminal sessions, restarts after crashes, and starts automatically on login. It polls the rip and encode queues and processes jobs one at a time using VideoToolbox hardware encoding.
Logs: ~/Library/Logs/Frostscribe/worker.log
Open FrostscribeUI from /Applications. The snowflake icon lives in your menu bar and shows rip/encode status at a glance.
Click Rip Disc to open the guided rip flow:
- Disc scans automatically
- Search TMDB to identify the title (or enter manually)
- Select the disc title and audio tracks
- Confirm output path
- The rip runs in the background worker — closing the app will not interrupt it
- Encoding begins automatically when the rip finishes
- Push notification fires on completion (configure via
event_hook)
TV show discs: After identifying the show, Frostscribe presents all episode-length titles with checkboxes. Set the season and starting episode number, select the episodes you want, and click Queue Episodes. Each episode is queued as a separate rip job; the disc is not ejected until all jobs complete.
Vigil Mode (default) means ripping is guided and interactive. Disable it in Settings to enable AutoScribe, which rips any inserted disc automatically without prompting.
frostscribe rip # Guided interactive rip session
frostscribe status # Current rip and encode status
frostscribe queue # Encode queue with per-job progresslaunchctl load ~/Library/LaunchAgents/com.frostscribe.worker.plist # Start
launchctl unload ~/Library/LaunchAgents/com.frostscribe.worker.plist # Stop
launchctl kickstart -k gui/$(id -u)/com.frostscribe.worker # Restart
tail -f ~/Library/Logs/Frostscribe/worker.log # Follow logsOutput paths are automatically formatted for your configured media server.
Jellyfin / Emby
Movies/The Dark Knight (2008)/The Dark Knight (2008).mkv
TV Shows/Breaking Bad (2008)/Season 01/Breaking Bad (2008) - S01E01.mkv
Plex
Movies/The Dark Knight (2008)/The Dark Knight (2008).mkv
TV Shows/Breaking Bad/Season 01/S01E01.mkv
Kodi
Movies/The Dark Knight (2008)/The Dark Knight (2008).mkv
TV Shows/Breaking Bad/Season01/Breaking Bad S01E01.mkv
~/Library/Application Support/Frostscribe/config.json
| Field | Required | Description |
|---|---|---|
media_server |
Yes | jellyfin, plex, or kodi |
movies_dir |
Yes | Root directory for movie output |
tv_dir |
Yes | Root directory for TV show output |
temp_dir |
Yes | Staging area for raw ripped MKV files |
tmdb_api_key |
No | TMDB API v3 key for automatic title lookup |
makemkv_key |
No | MakeMKV registration key (trial mode without it) |
makemkv_bin |
No | Full path to makemkvcon (searched in $PATH if empty) |
handbrake_bin |
No | Full path to HandBrakeCLI (searched in $PATH if empty) |
event_hook |
No | Shell command executed on lifecycle events. Receives FROSTSCRIBE_EVENT (rip_complete, encode_complete, encode_failed), FROSTSCRIBE_TITLE, and FROSTSCRIBE_BODY as environment variables. |
vigil_mode |
No | true = Vigil Mode (interactive, default). false = AutoScribe (auto-rips without prompting) |
select_audio_tracks |
No | Prompt to choose audio tracks before ripping (default: false) |
quality_dvd |
No | HandBrake RF quality for DVD (default: 80) |
quality_bluray |
No | HandBrake RF quality for Blu-ray (default: 70) |
quality_uhd |
No | HandBrake RF quality for UHD Blu-ray (default: 70) |
python3 -c "
import urllib.request, json, os
urllib.request.urlopen(
urllib.request.Request(
'http://localhost:8123/api/services/notify/mobile_app_YOUR_DEVICE',
json.dumps({'title': os.environ.get('FROSTSCRIBE_TITLE',''),
'message': os.environ.get('FROSTSCRIBE_BODY','')}).encode(),
{'Authorization': 'Bearer YOUR_HA_TOKEN', 'Content-Type': 'application/json'}
), timeout=5
)
"See ARCHITECTURE.md for a full breakdown of the package structure, data flow, and design decisions.
MIT © 2026 trevholliday