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>
90 lines
3 KiB
Python
90 lines
3 KiB
Python
"""Streamlit GUI: what's in your Spotify / iTunes libraries that you do NOT
|
||
have locally, decided by real ID matching (recording MBID / ISRC) with a
|
||
normalized-text fallback.
|
||
|
||
Reads only the database (run `python main.py ingest` and `enrich` first).
|
||
"""
|
||
import pandas as pd
|
||
import streamlit as st
|
||
from st_aggrid import AgGrid, GridOptionsBuilder
|
||
|
||
from src.db.database import init_db
|
||
from src.match.matcher import classify_all, summary
|
||
|
||
st.set_page_config(layout="wide", page_title="MusicIndexer — Missing tracks")
|
||
|
||
|
||
@st.cache_data(ttl=30)
|
||
def load_data():
|
||
init_db()
|
||
missing, present = classify_all()
|
||
return missing, present, summary()
|
||
|
||
|
||
def grid(df, key):
|
||
gb = GridOptionsBuilder.from_dataframe(df)
|
||
gb.configure_default_column(filter=True, sortable=True, resizable=True)
|
||
gb.configure_pagination(paginationAutoPageSize=False, paginationPageSize=50)
|
||
AgGrid(df, gridOptions=gb.build(), height=620, fit_columns_on_grid_load=True, key=key)
|
||
|
||
|
||
def main():
|
||
st.title("🎧 Tracks you're missing locally")
|
||
|
||
if st.sidebar.button("🔄 Refresh from DB"):
|
||
load_data.clear()
|
||
|
||
missing, present, summ = load_data()
|
||
|
||
# --- summary metrics ---
|
||
for source in ("spotify", "itunes"):
|
||
s = summ.get(source, {})
|
||
if not s:
|
||
continue
|
||
st.subheader(source.capitalize())
|
||
c1, c2, c3, c4, c5 = st.columns(5)
|
||
c1.metric("Total", s["total"])
|
||
c2.metric("Missing locally", s["missing"])
|
||
c3.metric("Have (MBID)", s["present_mbid"])
|
||
c4.metric("Have (text)", s["present_text"])
|
||
coverage = (s["has_mbid"] / s["total"] * 100) if s["total"] else 0
|
||
c5.metric("MBID coverage", f"{coverage:.0f}%")
|
||
|
||
local = summ.get("local", {})
|
||
st.caption(
|
||
f"Local library: {local.get('total', 0)} tracks, "
|
||
f"{local.get('has_mbid', 0)} with a resolved MBID. "
|
||
"‘Have (MBID)’ = certain ID match; ‘Have (text)’ = normalized "
|
||
"artist/title match (run `enrich` to upgrade text matches to ID)."
|
||
)
|
||
|
||
st.divider()
|
||
|
||
# --- controls ---
|
||
view = st.sidebar.radio("View", ["Missing", "Present", "All"])
|
||
sources = st.sidebar.multiselect("Sources", ["spotify", "itunes"],
|
||
default=["spotify", "itunes"])
|
||
|
||
if view == "Missing":
|
||
rows = [dict(r, status="missing") for r in missing]
|
||
elif view == "Present":
|
||
rows = [dict(r, status="present") for r in present]
|
||
else:
|
||
rows = ([dict(r, status="missing") for r in missing]
|
||
+ [dict(r, status="present") for r in present])
|
||
|
||
rows = [r for r in rows if r["source"] in sources]
|
||
|
||
if not rows:
|
||
st.info("Nothing to show — run `python main.py ingest` (and `enrich`).")
|
||
return
|
||
|
||
df = pd.DataFrame(rows)[
|
||
["status", "source", "artist", "title", "album", "matched_by",
|
||
"isrc", "mbid", "play_count", "spotify_id"]
|
||
]
|
||
st.write(f"**{len(df)}** tracks")
|
||
grid(df, key=f"{view}-{'-'.join(sorted(sources))}")
|
||
|
||
|
||
main()
|