Skip to content

farmhutsoftwareteam/savemymusic

Cratify

Cratify

Drop a Spotify playlist → get Serato-ready 320kbps MP3s, tagged and crate-organised.

Latest release CI License: MIT macOS 12+ Python 3.9+ PRs welcome

A Mac app for home DJs who want a clean, batched alternative to dragging YouTube links one at a time. Strict matching by default — Cratify refuses remixes, edits, covers, or sped-up versions unless the source track explicitly contains those keywords. When auto-match isn't confident, you pick the right candidate from the top 8 YouTube results yourself.


Quick start

Download the latest DMG, drag Cratify.app to Applications, right-click → Open the first time.

That's it — no Homebrew, no Terminal. ffmpeg and yt-dlp are bundled inside the app, and yt-dlp auto-updates in the background once a day so YouTube can't break it for long.

Drop a Spotify-exported CSV onto the window, or click Import from Spotify… to pull one straight from your account.

Hacking on it instead? git clone https://github.com/farmhutsoftwareteam/savemymusic && cd savemymusic && ./savemymusic.sh ui — the wrapper creates a .venv and uses the binaries from build/. If you don't have build/ffmpeg yet, run build/build_ffmpeg.sh (one-time, ~5min) and build/fetch_ytdlp.sh.


Install

The fast path — grab the latest release:

  1. Download Cratify-x.y.z.dmg
  2. Open it → drag Cratify.app to Applications
  3. Double-click to launch. (Cratify v0.4.1+ is signed with a Developer ID and notarised by Apple — no right-click dance.)

That's it. Both ffmpeg (LGPL build, ~20MB) and yt-dlp (~35MB) live inside the .app. On first launch they're copied to ~/Library/Application Support/Cratify/bin/ (user-writable) so the auto-updater can keep yt-dlp current.


How it works

Spotify playlist
      │
      ▼
Exportify CSV  ◀── pulled inside Cratify via the
      │           "Import from Spotify" button,
      │           or dropped onto the window
      ▼
Strict YouTube matcher
      │
      ▼ (or: manual pick from top 8 candidates)
      │
yt-dlp → ffmpeg → 320kbps MP3 + ID3 tags + album art
      │
      ▼
~/Music/SaveMyMusic/<crate>/   ◀── ready to drag into Serato

The matcher rejects modified versions by default (remix, edit, bootleg, live, instrumental, karaoke, sped-up, slowed, lyric video, mashup, etc.) — unless the source track title itself contains those keywords (so "CamelPhat – Home (Samm & Ajna Remix)" still matches correctly because "Remix" is in the source).

It also enforces a duration tolerance (within ±12s of the Spotify duration by default) and a composite score gate. Tracks that don't pass land in the Unmatched panel with the reason and the closest rejected title — you can retry with looser strictness, edit the search query, or open the candidate panel and pick manually.


Features

  • Drag-drop multiple CSVs at once. Each becomes a section with its own table, tabs (All / Matched / Unmatched / Done / Skipped), and download queue.
  • Live progress via SSE — rows update one by one as they resolve and download. Auto-reconnects after transient network drops via Last-Event-ID.
  • Strict / Normal / Loose presets per CSV. Loose mode allows the disqualifying-keyword bypass for festival-edit playlists; Strict tightens duration tolerance + raises the score gate.
  • Manual candidate picker on every row — shows the top 8 YouTube results with thumbnail, channel, duration, score, and reject reason. Pick one, paste a URL, or leave auto-match alone.
  • Cancel mid-flight — abort resolve or download cleanly; partial files cleaned up.
  • Automatic retry on transient yt-dlp failures (one retry after 5s before marking a row failed).
  • Subprocess timeouts — idle 90s, total 10 min — so a stalled download can't hang the queue.
  • Tags — title, artist (with featured), album, genre(s), year, embedded album art. (BPM + key are left blank for Serato/Rekordbox to auto-detect on first import — they're better at audio analysis than we are.)
  • Reveal in Finder per row or per crate. Preview audio on completed rows (only one preview plays at a time).
  • Logs at ~/Library/Logs/Cratify/app.log (5MB × 3 rotation).

Import from Spotify (one-button)

The Import from Spotify… button opens Exportify in a second window inside the app. Log in to Spotify, pick a playlist, click Export — Cratify intercepts the CSV in-page (no Downloads detour) and ships it straight into your session. Four capture strategies catch it whether Exportify uses data-URLs, blob URLs, FileSaver, or programmatic anchor clicks.


Output

Files land in ~/Music/SaveMyMusic/<crate-name>/ as:

Artist - Track Title.mp3

with full ID3v2 tags. Drag the folder into Serato and they'll appear with BPM + key already populated.

Why is the folder called SaveMyMusic and not Cratify? Cratify started as "SaveMyMusic". The output folder kept the old name so existing crates and any DJ-software references to that path keep working. Move freely if you want — set a custom output via the --out flag in the CLI.


CLI mode

Cratify is also a command-line tool — handy for automating batch jobs or running headless:

git clone https://github.com/farmhutsoftwareteam/savemymusic
cd savemymusic
./savemymusic.sh path/to/playlist.csv [path/to/another.csv ...] [--limit N] [--out DIR]

The wrapper script creates a .venv, installs Python deps, and runs the same pipeline as the desktop app. Use --limit 3 for a quick smoke test.

To launch the desktop UI from the same checkout: ./savemymusic.sh ui.


Bundled binaries

The .app ships everything it needs. No Homebrew, no Terminal, no install prerequisites.

ffmpeg

We build a minimal LGPL ffmpeg from source (build/build_ffmpeg.sh) — --disable-gpl --disable-nonfree, only libmp3lame as an external dep (statically linked + bundled as a dylib next to the binary via install_name_tool so it's fully relocatable). The result is a ~20MB binary that depends only on Apple system frameworks and travels inside the .app.

This means Cratify can stay MIT-licensed — we never touch the GPL ffmpeg builds Homebrew ships by default (x264, x265, libfdk-aac).

yt-dlp

The bundle includes the latest yt-dlp_macos universal binary at build time. On every launch (throttled to once per 24h), updater.py hits GitHub's API and replaces it if a newer release exists. This solves yt-dlp's biggest weakness: YouTube changes its player code constantly, so a frozen version goes stale within weeks. The user-writable copy at ~/Library/Application Support/Cratify/bin/yt-dlp is what actually gets executed; the bundled one is just the seed.

Lookup order at runtime

Both binaries follow the same resolution chain in savemymusic/bundled_bins.py:

  1. ~/Library/Application Support/Cratify/bin/<name> (user-writable, kept fresh by the updater)
  2. Cratify.app/Contents/Resources/bin/<name> (frozen at build time, seeded into the user dir on first launch)
  3. /opt/homebrew/bin/<name> / /usr/local/bin/<name> (Homebrew, if you happen to have it)
  4. shutil.which(<name>) from PATH (last resort)

So if you've got newer-than-bundled yt-dlp from Homebrew and want Cratify to use that instead, just delete ~/Library/Application Support/Cratify/bin/yt-dlp — the resolver falls through to your Homebrew copy.


Troubleshooting

The Import from Spotify window opens but nothing happens when I export. Check ~/Library/Logs/Cratify/app.log — the JS hook logs [cratify] Exportify hook installed when it injects. If you don't see that, the page was navigated before the hook installed; close the window and try again.

Downloads keep failing with HTTP 403. The auto-updater normally keeps yt-dlp fresh, but if it hasn't run yet (or GitHub was unreachable), force-update by deleting the user copy: rm ~/Library/Application\ Support/Cratify/bin/yt-dlp and restart. The next launch will reseed from the bundled binary AND ping GitHub for the latest. If you have a newer Homebrew yt-dlp you'd rather use, that works too — see the lookup chain above.

The .app won't open — "Cratify is damaged and can't be opened". Should only happen on builds before v0.4.1 (ad-hoc signed). From v0.4.1 onwards the app is notarised; double-click should just work. If you do hit it on an old version: xattr -dr com.apple.quarantine /Applications/Cratify.app.

"FFmpeg not found" — but the bundle includes it. Should never happen with the v0.2.0 .app since ffmpeg is shipped inside. If it does: delete ~/Library/Application Support/Cratify/bin/ffmpeg and libmp3lame.0.dylib, restart — the bundled copies are re-seeded on launch. If you're running from source, run build/build_ffmpeg.sh to produce them.


Development

Requirements: macOS 12+, Python 3.9+, Xcode Command Line Tools.

git clone https://github.com/farmhutsoftwareteam/savemymusic
cd savemymusic
build/fetch_ytdlp.sh      # downloads latest yt-dlp into build/
build/build_ffmpeg.sh     # one-time, ~5min: builds LGPL ffmpeg into build/
./savemymusic.sh ui       # creates .venv on first run, then launches

You only need Homebrew for the lame library (brew install lame) — that's the one upstream LGPL audio dep ffmpeg links against.

Project layout:

savemymusic/          ← Python CLI core
├── parser.py          – Exportify CSV → Track dataclass
├── matcher.py         – yt-dlp search + strict scoring (find_best, find_candidates)
├── downloader.py      – yt-dlp subprocess wrapper with progress + timeouts
├── tagger.py          – mutagen ID3v2 writer
├── keys.py            – Spotify key+mode → Camelot
└── __main__.py        – CLI entry, also delegates to UI when first arg is "ui"

savemymusic_ui/       ← FastAPI + PyWebView desktop layer
├── launcher.py        – PyWebView window + uvicorn glue
├── app.py             – FastAPI app: routes for CSV / resolve / download / picker
├── workers.py         – Sync background workers (resolve_worker, download_worker)
├── tasks.py           – SSE event bus with Last-Event-ID replay
├── session.py         – In-memory state
├── exportify_bridge.py – PyWebView js_api + Exportify popup window
├── logging_setup.py   – Rotating file handler at ~/Library/Logs/Cratify/
└── static/            – Vanilla JS + HTML + CSS frontend

build/make_icon.py    ← Pillow icon renderer (regenerates .icns)
setup.py              ← py2app build config → dist/Cratify.app

Build the .app yourself:

build/fetch_ytdlp.sh                         # ensure binaries are present
build/build_ffmpeg.sh                        # (skip if build/ffmpeg exists)
.venv/bin/python build/make_icon.py          # regen icon (only if you changed it)
iconutil -c icns build/Cratify.iconset
.venv/bin/python setup.py py2app --bdist-base build/cratify-build --dist-dir dist
codesign --force --deep --sign - dist/Cratify.app

Contributing

PRs welcome — especially small ones that fix a real annoyance you hit. See CONTRIBUTING.md for the dev loop, project layout, and what makes a good PR. Bugs go through the issue template; security issues follow SECURITY.md.

Areas where help would move the needle most:

  • More DJ software backends — Rekordbox / Traktor metadata mapping
  • CI-built LGPL ffmpeg — the build script works locally, but baking it into GitHub Actions so the release artifacts are reproducible would be nice
  • A real matcher evaluation suite — calibrated on known-good track↔YouTube-URL pairs
  • Linux + Windows builds (PyInstaller / Briefcase; PyWebView already supports both OSes)
  • Apple Developer ID notarisation so the DMG opens without right-click → Open

License

MIT — fork it, ship it, embed it. Just keep the copyright + permission notice.

The runtime bundle is fully MIT-compatible: every Python dependency is MIT/BSD/Apache, and the only LGPL components (ffmpeg, libmp3lame) are correctly attributed and replaceable per LGPL terms. See THIRDPARTY-LICENSES.md for the full inventory.


Legal note

Cratify is a tool. It can be pointed at YouTube content that's properly licensed for you to download (your own uploads, Creative Commons material, public-domain music, content where you hold the rights) or content that isn't. The tool doesn't enforce licensing — that's your responsibility.

Working DJs typically use licensed DJ-pool services (Beatport DJ, Beatsource, DJcity, BPM Supreme) for source files — those handle the licensing for DJ use. Use Cratify the way that fits your jurisdiction and your conscience.

About

macOS desktop app: drop a Spotify playlist CSV, get Serato-ready 320kbps MP3s with full ID3 tags, BPM, and Camelot keys. Strict YouTube matching, manual override per track.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors