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>
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 (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}%")
|
||
|
||
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", "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()
|