No description
Find a file
Шурупов Илья Викторович 0e64d439c4 DB-backed incremental redesign: find tracks missing locally
Replace the JSON-file pipeline with a SQLite/SQLAlchemy store (Postgres via
DATABASE_URL) and an incremental, ID-based matcher whose goal is "which
Spotify/iTunes tracks am I missing locally".

- src/db: models (Track/MbCache/FileState/IngestState) + engine/session.
- src/ingest: upsert prefetched spotify/itunes/local (work_dir/latest-1) by
  (source, source_id); pulls ISRC from raw Spotify; preserves enrichment.
- src/enrich: MusicBrainz recording-MBID resolver. Resolves via NORMALIZED
  text search (so the same song across sources lands on one MBID + one cached
  lookup), ISRC as exact fallback. Only touches unresolved tracks, caches
  positive AND negative results, rate-limited and resumable.
- src/match: normalization + cross-source matcher with tiers ISRC > MBID >
  normalized text; reports present/missing and the match method.
- main.py: CLI (ingest / enrich / match). Gui.py: missing-tracks viewer.
- README + requirements (SQLAlchemy, musicbrainzngs); ignore *.db.

Incremental everywhere: re-running updates only what changed; enrich continues
where it stopped. Live Spotify/local backends are left intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 01:25:21 +03:00
old fixes 2026-01-05 01:19:00 +03:00
src DB-backed incremental redesign: find tracks missing locally 2026-06-15 01:25:21 +03:00
.env.example Remove hardcoded credentials; read them from the environment 2026-06-15 00:39:59 +03:00
.gitignore DB-backed incremental redesign: find tracks missing locally 2026-06-15 01:25:21 +03:00
Env.py DB-backed incremental redesign: find tracks missing locally 2026-06-15 01:25:21 +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 DB-backed incremental redesign: find tracks missing locally 2026-06-15 01:25:21 +03:00
gui.sh spotify: resilient fetching, progress bars, venv launcher 2026-06-15 00:39:59 +03:00
main.py DB-backed incremental redesign: find tracks missing locally 2026-06-15 01:25:21 +03:00
README.md DB-backed incremental redesign: find tracks missing locally 2026-06-15 01:25:21 +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

Credentials

Create an app at https://developer.spotify.com/dashboard and add http://127.0.0.1:8888 as a Redirect URI. Credentials are read from the environment — never commit them.

cp .env.example .env
# edit .env with your client id/secret
source .env

Env.py reads SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET (only needed for live fetching), plus optional DATABASE_URL, PREFETCHED_DIR and MB_CONTACT.

Database

Defaults to a local SQLite file (musicindexer.db). To use Postgres instead, set one env var — no code changes:

export DATABASE_URL=postgresql+psycopg://user:pass@localhost:5432/musicindexer

Usage

python main.py ingest        # load prefetched data (work_dir/latest-1) into the DB
python main.py enrich        # resolve MusicBrainz MBIDs (resumable; ~1 req/s)
python main.py match         # print present/missing summary
./gui.sh                     # browse what you're missing

ingest and the GUI work immediately (text-based matching). enrich queries MusicBrainz only for tracks that don't have an MBID yet, caches every result, and is resumable — if it's interrupted, just run it again to continue. Run python main.py enrich --limit 300 to do a quick partial pass, or --source local to restrict to one source.

Live fetching (SpotifyWebAPI, the local indexer) is unchanged and still available; this redesign reads prefetched JSON so the pipeline can be built and demoed without network access.

Layout

main.py                 CLI: ingest / enrich / match
Gui.py / gui.sh         Streamlit GUI (missing-tracks viewer)
Env.py                  config (DB URL, paths, credentials from env)
src/
  db/                   SQLAlchemy models + engine/session
  ingest/               load prefetched JSON into the DB (upsert)
  enrich/               MusicBrainz MBID resolver (cached, resumable)
  match/                normalization + cross-source matcher
  backends/             live fetchers/parsers (spotify, local, itunes, ...)
  Library.py            in-memory data model used by the backends
work_dir/latest-1/      prefetched JSON used by `ingest` (gitignored)
musicindexer.db         the database (gitignored)

Notes

  • The DB is the source of truth; work_dir/ JSON is only an ingest source.
  • Only currently-supported Spotify endpoints are used (no deprecated audio-features/recommendations/etc.).