No description
Find a file
Шурупов Илья Викторович 03b98d9a1d Real backends + fetch/display workflows; config paths
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>
2026-06-15 01:46:56 +03:00
old fixes 2026-01-05 01:19:00 +03:00
src Real backends + fetch/display workflows; config paths 2026-06-15 01:46:56 +03:00
.env.example Real backends + fetch/display workflows; config paths 2026-06-15 01:46:56 +03:00
.gitignore Real backends + fetch/display workflows; config paths 2026-06-15 01:46:56 +03:00
Env.py Real backends + fetch/display workflows; config paths 2026-06-15 01:46:56 +03:00
error.log nice 2026-02-07 00:48:46 +03:00
Flow.py spotify: resilient fetching, progress bars, venv launcher 2026-06-15 00:39:59 +03:00
Gui.py Real backends + fetch/display workflows; config paths 2026-06-15 01:46:56 +03:00
gui.sh spotify: resilient fetching, progress bars, venv launcher 2026-06-15 00:39:59 +03:00
main.py Real backends + fetch/display workflows; config paths 2026-06-15 01:46:56 +03:00
README.md Real backends + fetch/display workflows; config paths 2026-06-15 01:46:56 +03:00
requirements.txt DB-backed incremental redesign: find tracks missing locally 2026-06-15 01:25:21 +03:00
TODO stable - generate local playlists & add default options 2026-01-04 01:37:47 +03:00

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 enrich runs, matching is normalized-text only (still useful). enrich upgrades 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 fetch runs (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.).