MusicIndexer/Gui.py
Шурупов Илья Викторович 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

90 lines
3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()