mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
v3 library: first-hour polish — zero-states, match progress, provenance, alias search (#730)
* v3 library: first-hour polish — zero-states, match progress, provenance, alias search
Six launch-eve fixes for a brand-new user's first hour with a fresh,
being-matched library. Each is small and reuses shipped idioms.
- Invitational repertoire meter: with no practice data yet, the home meter
no longer reads "0 of N mastered" (debt framing) — it shows an empty bar
with "grows as you master songs". A count of 0 read as failure on day one.
- "Start here" starter shelf: growth_edge_suggestions() distinguishes two
empties — attempts exist but all mastered (honest empty shelf) vs nothing
attempted yet (day one) → new starter_suggestions() returns up to 8
approachable songs (90–480s, shortest first) flagged starter:true, and the
client renders a "Start here" shelf instead of a blank home.
- Library-visible match progress: while the background pass runs, a quiet
"Matching your library — X of Y" line sits by the review chip (5s poll,
single guarded interval, cleared the moment the pass stops — no leak,
no toast, silent completion).
- One-time transparency toast: the first time an install is seen matching a
real library, one fbNotify names what's contacted (MusicBrainz / Cover Art
Archive), that results are stored locally, that files aren't changed
without you, and where the switch is. localStorage-gated, wrapped so a
blocked notifier can't break the chip.
- Empty-library dead-end card: a genuinely empty local library (no songs, no
query/filter) shows "Your library is empty" + drop-files hint + Open
Settings, instead of a bare grid under dead dropdowns.
- Alias-aware search: searching a canonical name ("AC/DC") now also finds
songs whose raw tag is a merged variant ("ACDC"), via the artist_alias
table. Probe-guarded so a no-aliases library keeps the exact original
3-term query; pure predicate, keyset-safe.
- Details-drawer provenance line: matched/manual rows show "Matched:
<artist — title> (source) · Fix match" under the Identity fields — the
wrong-match escape hatch at the point of the data, wired to the same
fix-match flow the card menu uses. New read-only GET
/api/enrichment/song/{filename} backs it.
Tests: tests/test_starter_suggestions.py (starter vs normal-shelf behaviour,
length window, attempts-exist path unchanged) + alias-search cases added to
tests/test_artist_alias.py. 34 targeted pass; node --check clean; no new
Tailwind classes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nm7tHs1Yvjjtnnu4nzJgdN
* fix(v3 library): stop match-progress poll when leaving the library screen
The 5s enrichment poll (_pollTimer) was cleared on pass completion and on
fetch error, but not when the user navigated away from the library. Leaving
v3-songs mid-pass left the interval pinging /api/enrichment/status in the
background until the pass ended. Subscribe to the existing feedBack
'screen:changed' event: clear the poll when any non-v3-songs screen shows,
and refresh (re-arming if a pass is still running) on returning to v3-songs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
df2d660d1e
commit
8c7cde5d5c
@@ -2079,7 +2079,13 @@ class MetadataDB:
|
||||
cands = [(fn, a) for fn, a in agg.items()
|
||||
if a["plays"] > 0 and a["acc"] is not None and a["acc"] < MASTERY_ACCURACY]
|
||||
if not cands:
|
||||
return []
|
||||
# Two different empties (launch polish): attempts exist but
|
||||
# everything attempted is mastered → an empty shelf is honest;
|
||||
# NOTHING attempted yet (day one) → "starter" picks instead, so
|
||||
# the library home invites a first play rather than dead-ending.
|
||||
if any(a["plays"] > 0 and a["acc"] is not None for a in agg.values()):
|
||||
return []
|
||||
return self.starter_suggestions(limit)
|
||||
diffs = self.user_meta_map([fn for fn, _ in cands]) # {filename: 1..5}
|
||||
out = []
|
||||
for fn, a in cands:
|
||||
@@ -2095,6 +2101,28 @@ class MetadataDB:
|
||||
out.sort(key=lambda r: (r["growth_score"], r["last_played_at"] or "", r["filename"]), reverse=True)
|
||||
return out[:limit]
|
||||
|
||||
def starter_suggestions(self, limit: int = 8) -> list[dict]:
|
||||
"""Day-one 'Start here' picks for a library with no practice attempts
|
||||
yet: up to 8 approachable songs — sensible length (90s–480s, so intros/
|
||||
jingles and 10-minute epics don't lead), shortest first, filename as a
|
||||
stable tiebreak. Same row shape as the growth-edge rows plus a
|
||||
`starter: true` marker so the client renders the invitational 'Start
|
||||
here' shelf instead of 'Keep practicing'. Read-only."""
|
||||
limit = max(1, min(8, int(limit)))
|
||||
rows = self.conn.execute(
|
||||
"SELECT filename FROM songs WHERE title != '' "
|
||||
"AND duration >= 90 AND duration <= 480 "
|
||||
"ORDER BY duration ASC, filename ASC LIMIT ?", (limit,)).fetchall()
|
||||
return [{
|
||||
"filename": r[0],
|
||||
"best_accuracy": None,
|
||||
"arrangement": None,
|
||||
"last_played_at": None,
|
||||
"user_difficulty": None,
|
||||
"growth_score": 0.0,
|
||||
"starter": True,
|
||||
} for r in rows]
|
||||
|
||||
# ── Playlists ─────────────────────────────────────────────────────────--
|
||||
SAVED_KEY = "saved_for_later"
|
||||
|
||||
@@ -3129,8 +3157,21 @@ class MetadataDB:
|
||||
if _msel:
|
||||
where += " AND (" + " OR ".join(_msel) + ")"
|
||||
if q:
|
||||
where += " AND (title LIKE ? COLLATE NOCASE OR artist LIKE ? COLLATE NOCASE OR album LIKE ? COLLATE NOCASE)"
|
||||
params += [f"%{q}%"] * 3
|
||||
_qlike = f"%{q}%"
|
||||
_qterms = ("title LIKE ? COLLATE NOCASE OR artist LIKE ? COLLATE NOCASE "
|
||||
"OR album LIKE ? COLLATE NOCASE")
|
||||
_qparams = [_qlike] * 3
|
||||
# Alias-aware artist term (launch polish): searching the CANONICAL
|
||||
# name ("AC/DC") must also find songs whose raw tag is a merged
|
||||
# variant ("ACDC") — expand via the artist_alias table. Pure
|
||||
# predicate (keyset-safe); probe-guarded so the common no-aliases
|
||||
# library keeps the exact original 3-term query.
|
||||
if self.conn.execute("SELECT 1 FROM artist_alias LIMIT 1").fetchone() is not None:
|
||||
_qterms += (" OR artist COLLATE NOCASE IN (SELECT raw_name FROM artist_alias "
|
||||
"WHERE canonical_name LIKE ? COLLATE NOCASE)")
|
||||
_qparams.append(_qlike)
|
||||
where += f" AND ({_qterms})"
|
||||
params += _qparams
|
||||
if include_intrinsic:
|
||||
ifrag, iparams = self._build_intrinsic_where(
|
||||
"songs", format_filter=format_filter,
|
||||
@@ -6723,6 +6764,19 @@ def enrichment_status():
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/enrichment/song/{filename:path}")
|
||||
def api_enrichment_song(filename: str):
|
||||
"""Read-only per-song match provenance for the Details drawer (launch
|
||||
polish): which canonical identity this chart matched and how. A tiny
|
||||
projection of the cache row — no candidates, no cache paths."""
|
||||
row = meta_db.get_enrichment(filename)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="no enrichment row")
|
||||
return {k: row.get(k) for k in
|
||||
("match_state", "canon_artist", "canon_title",
|
||||
"match_source", "match_score")}
|
||||
|
||||
|
||||
@app.post("/api/enrichment/kick")
|
||||
def api_enrichment_kick():
|
||||
"""The Settings "Match now" button: request an enrichment pass without
|
||||
|
||||
Reference in New Issue
Block a user