A local web GUI for beets — runs as a standalone macOS app via Safari Web App.
No Electron. No Docker. Just a small Flask server and a single HTML file.
- Inbox — scan a folder for music not yet in your library (matches on artist/title, not path, so it's correct however previous imports were copied or moved), then import with a keyboard-driven decision queue for matches and duplicates
- Library — search and browse your collection, manage duplicates, cover art and metadata, convert WAV/AIFF/FLAC to ALAC, remove tracks
- Export — playlists and tracklists for Lexicon/Traktor, USB mirror
- Preferences (⚙ in the header, or ⌘,) — library/import/plugin config with a live
config.yamlpreview, and Discogs/MusicBrainz/Beatport4 credentials - Dark + light mode (follows macOS system preference)
- macOS (Ventura or later recommended for Safari Web App)
- beets —
pipx install beets - Flask — already included in beets' pipx environment:
pipx inject beets flask
- ffmpeg (optional, for lossless → ALAC conversion):
brew install ffmpeg
- fd (required for Inbox's Utilities section and Library's Formats/WAV-AIFF finder — the unimported-music scan itself doesn't need it):
brew install fd
git clone https://github.com/YOUR_USERNAME/beetsgui.git
cd beetsgui# If Flask is in beets' pipx environment:
~/.local/pipx/venvs/beets/bin/python server.py
# Or if Flask is in your system Python:
python3 server.pyOpen http://localhost:1312 in Safari.
With http://localhost:1312 open in Safari: File → Add to Dock → name it "beetsGUI"
The server will now open the standalone app automatically on next launch.
Open Automator → New → Application → Run Shell Script:
/path/to/python server.py
# Example with pipx beets:
# ~/.local/pipx/venvs/beets/bin/python /path/to/beetsgui/server.pySave as beetsGUI Launcher.app, drag to Dock. One click starts everything.
The importer runs inside the server through the beets Python API — no
beet import subprocess, so match decisions are made in the app instead of
in Terminal. The Inbox tab is a decision queue: candidate cards for
album/track matches, a side-by-side compare for duplicates, and full keyboard
control (1-9 picks a candidate, Enter applies, S skips, A keeps
tags as-is; duplicates use K/S/M/R for keep/skip/merge/replace). One
import runs at a time; closing the tab mid-import is safe — reopening the app
rejoins whatever is still running.
Duplicates are ranked by quality before you're ever asked: a lossy copy of an
album already in the library is skipped automatically, a lossless copy
recommends replacing the existing one, and equal-quality duplicates still ask
with no recommendation either way. See quality_rank() in importsession.py.
The underlying endpoints also work with curl alone, for scripting or
debugging:
# Start: mode is interactive | fast | quiet | timid
# handling is copy | move | keep; incremental and singleton are booleans
curl -s -X POST localhost:1312/import/start -H 'Content-Type: application/json' \
-d '{"path":"~/Downloads/some album","mode":"interactive","handling":"copy"}'
# Or a curated set of folders (e.g. found while scanning in Inbox, or via an fd search) —
# beets groups them itself, same as `beet import path1 path2`
curl -s -X POST localhost:1312/import/start -H 'Content-Type: application/json' \
-d '{"paths":["~/Downloads/album1","~/Downloads/album2"],"mode":"interactive"}'# Watch: status lines, decisions and a final done event, as SSE
curl -sN localhost:1312/import/<id>/events# Answer a decision. choice is apply|skip|asis|tracks|albums for a match,
# skip|keep|remove|merge for a duplicate, or resume|restart to resume.
curl -s -X POST localhost:1312/import/<id>/decide -H 'Content-Type: application/json' \
-d '{"decision_id":"<from the event>","choice":"apply","candidate":0}'POST /import/<id>/abort cancels cleanly, and GET /import/current returns the
running import plus its waiting decision, so a reloaded page can rejoin.
An unanswered decision times out after 15 minutes (BEETSGUI_DECISION_TIMEOUT)
and aborts the import rather than blocking the server forever.
Test it end to end (needs ffmpeg; runs against a throwaway library):
~/.local/pipx/venvs/beets/bin/python test_importsession.py- Lossless files (WAV, AIFF, FLAC) convert to ALAC 24-bit on import — 32-bit float is handled automatically
- MP3 and AAC are never re-encoded
- Designed for Traktor / Lexicon / Rekordbox workflows
Metadata sources: musicbrainz chroma beatport4 discogs deezer spotify tidal
Enrichment: fetchart embedart lastgenre fromfilename bpsync autobpm keyfinder replaygain lyrics
Maintenance: duplicates missing mbsync importfeeds dirfields scrub smartplaylist unimported
Pull requests welcome. This started as a personal tool for a DJ/electronic music collection — if you have a different workflow, open an issue.
Built by DR. WARTEMAL — if you'd like to hear what this tool is for, find my music at soundcloud.com/drwartemal.
AGPLv3 — see LICENSE.
