Wire the incremental pipeline to real sources and expose two workflows. Config (Env.py): three explicit paths — LOCAL_LIBRARY_PATH (scan source), ITUNES_XML_PATH (manual export), DATA_DIR (storage root: DB + Spotify cache). Real, incremental ingesters: - local_scan: walks the library, skips files whose mtime/size are unchanged, prunes deleted ones, and reads embedded ISRC + recording MBID from tags (tagged files get real IDs with no network), via tags.py. - itunes_xml: parse the iTunes Library.xml as a plist; skip if mtime unchanged. - spotify_raw: ingest raw Spotify JSON (ISRC from external_ids); live fetch wrapper for when credentials are set. Workflows (src/workflows.py + main.py): - `fetch` — pull + scan + parse + enrich + match, with readable step logs. First run is slow (full scan + MB lookups); later runs only touch changes (verified: ~71s first, ~0.6s second). - `display` — launch the GUI. Also: enrich uses ISRC-first again (local MBIDs now come from tags), fix --enrich-limit 0, quiet musicbrainzngs XML warnings, ignore /data/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| old | ||
| src | ||
| .env.example | ||
| .gitignore | ||
| Env.py | ||
| error.log | ||
| Flow.py | ||
| Gui.py | ||
| gui.sh | ||
| main.py | ||
| README.md | ||
| requirements.txt | ||
| TODO | ||
MusicIndexer
Pull your Spotify library and listening stats via the Spotify Web API, index a local music library (audio tags + iTunes XML), and match the two so you can see which tracks you own, what's missing, and your top artists/tracks/playlists — in a Streamlit GUI.
It is database-backed and incremental: data lives in a DB (SQLite by default, Postgres optional) and every step UPSERTs, so re-running only updates what changed and nothing already fetched is lost. The headline use case is seeing which Spotify/iTunes tracks you don't have locally, decided by real recording identifiers (ISRC / MusicBrainz MBID) with a normalized-text fallback.
Pipeline
ingest -> enrich (MBIDs) -> match -> GUI
| Step | Command | Incremental rule |
|---|---|---|
| ingest | python main.py ingest |
upsert tracks by (source, source_id); never drops enrichment |
| enrich | python main.py enrich |
query MusicBrainz only for tracks with no MBID yet; every lookup (incl. "not found") cached; rate-limited & resumable |
| match | python main.py match |
classify each Spotify/iTunes track: present locally (ISRC > MBID > text) or missing |
| GUI | ./gui.sh |
browse missing/present with the match method shown |
How matching works
Each track is resolved to a MusicBrainz recording MBID by searching with the
normalized artist+title, so the same song from different sources lands on the
same MBID (and shares one cached lookup). A Spotify/iTunes track counts as
"have" if its ISRC, its MBID, or its normalized artist|title matches a local
track — otherwise it's missing. The GUI labels each match isrc/mbid
(ID-certain) or text (fuzzy).
Note: the local library here has no ISRC/MBID tags, so before
enrichruns, matching is normalized-text only (still useful).enrichupgrades matches to ID-certain. Tagging local files with Picard/AcoustID (ISRC) would make the ID tiers exact end-to-end.
Setup
Requires Python 3.13+.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Configure (.env)
cp .env.example .env # then edit and `source .env`
Three things you point at your own data:
| Variable | What |
|---|---|
LOCAL_LIBRARY_PATH |
folder of audio files to scan (ISRC/MusicBrainz tags are read) |
ITUNES_XML_PATH |
the Library.xml you export from Music (File → Library → Export Library…) |
DATA_DIR |
storage root — the database and cached Spotify responses live here |
SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET are only needed for live Spotify
fetching (register http://127.0.0.1:8888 as a Redirect URI in your
Spotify app). Set
DATABASE_URL=postgresql+psycopg://… to use Postgres instead of SQLite — no
code change.
Two workflows
python main.py fetch # do all the work, incrementally, and report
python main.py display # open the GUI to see what you're missing
fetch pulls Spotify, scans the local library, parses the iTunes export,
resolves MBIDs, and matches — logging progress at each step. The first run is
slow (full scan + MusicBrainz lookups at ~1 req/s); later runs are quick
because only changed files, the iTunes export if it changed, and still-unresolved
MBIDs are touched. Useful flags/helpers:
python main.py fetch --enrich-limit 500 # cap MBID lookups this run
python main.py enrich # resume MBID resolution only
python main.py match # print the present/missing summary
ISRC/MBID matching works as soon as
fetchruns (local tags + Spotify ISRC). MusicBrainz enrichment fills in the rest (iTunes, untagged local files); it's cached and resumable, so you can stop/continue any time.
Layout
main.py CLI: fetch / display / enrich / match
Gui.py / gui.sh Streamlit GUI (missing-tracks viewer)
Env.py config: LOCAL_LIBRARY_PATH / ITUNES_XML_PATH / DATA_DIR
src/
workflows.py run_fetch (the whole pipeline) + run_display
db/ SQLAlchemy models + engine/session
ingest/
spotify_raw.py live fetch + ingest raw Spotify JSON (ISRC)
local_scan.py incremental file scan (reads ISRC/MBID tags)
itunes_xml.py parse the iTunes Library.xml (plist)
tags.py read ISRC / recording MBID / duration from a file
enrich/ MusicBrainz MBID resolver (cached, resumable)
match/ normalization + cross-source matcher
backends/ lower-level Spotify Web API client / OAuth
$DATA_DIR/ database + cached Spotify responses (gitignored)
Notes
- The DB is the source of truth; everything is incremental and re-runnable.
- Local matching is real-ID where files are tagged (ISRC/MusicBrainz, e.g. via Picard); untagged files fall back to MusicBrainz text lookup, then text.
- Only currently-supported Spotify endpoints are used (no deprecated audio-features/recommendations/etc.).