MusicIndexer/Gui.py
Шурупов Илья Викторович 03b98d9a1d Real backends + fetch/display workflows; config paths
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>
2026-06-15 01:46:56 +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 (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()