"""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 (ID)", 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}%") if source == "spotify": st.caption( f"Origins — liked: {s.get('liked', 0)} · " f"in a playlist: {s.get('from_playlist', 0)} · " f"top tracks: {s.get('from_top', 0)}" ) 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 (ID)’ = certain ISRC/MBID match; ‘Have (text)’ = normalized " "artist/title match (run `fetch`/`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", "origin", "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()