From 75673c3b47f4a55ab05d40131e6d60380d96f842 Mon Sep 17 00:00:00 2001 From: Matthew Harris Glover Date: Tue, 7 Jul 2026 15:56:11 -0400 Subject: [PATCH] feat(library): sort and badge by personal difficulty rating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds sort=difficulty/difficulty-desc to the library API (correlated subquery over song_user_meta.user_difficulty, unrated songs pushed to the bottom either direction, same pattern as the existing mastery sort) and surfaces the rating as a badge on library cards in both the v2 grid/tree views and the v3 grid. The rating itself already existed (song_user_meta) — this just makes it sortable and visible, so it's no longer only readable in the per-song edit drawer. Co-Authored-By: Claude Sonnet 5 --- server.py | 12 ++++++++++++ static/app.js | 4 ++++ static/index.html | 2 ++ static/v3/songs.js | 3 +++ 4 files changed, 21 insertions(+) diff --git a/server.py b/server.py index e183f08..c63064d 100644 --- a/server.py +++ b/server.py @@ -4107,6 +4107,18 @@ class MetadataDB: "((SELECT MAX(best_accuracy) FROM song_stats s WHERE s.filename = songs.filename) IS NULL) ASC, " "(SELECT MAX(best_accuracy) FROM song_stats s WHERE s.filename = songs.filename) DESC" ), + # Personal difficulty rating (song_user_meta.user_difficulty, 1..5 — + # manually set or seeded by the difficulty_tagger plugin), via a + # correlated subquery like mastery above (drops to OFFSET paging). + # Unrated songs push to the bottom in both directions. + "difficulty": ( + "((SELECT user_difficulty FROM song_user_meta u WHERE u.filename = songs.filename) IS NULL) ASC, " + "(SELECT user_difficulty FROM song_user_meta u WHERE u.filename = songs.filename) ASC" + ), + "difficulty-desc": ( + "((SELECT user_difficulty FROM song_user_meta u WHERE u.filename = songs.filename) IS NULL) ASC, " + "(SELECT user_difficulty FROM song_user_meta u WHERE u.filename = songs.filename) DESC" + ), } if group and sort in ("mastery", "mastery-desc"): # Sort law (§7.1): mastery aggregates MAX across the WHOLE group — diff --git a/static/app.js b/static/app.js index 673167f..bc1e681 100644 --- a/static/app.js +++ b/static/app.js @@ -1110,6 +1110,7 @@ const _LIB_VIEW_VALUES = new Set(['grid', 'tree', 'folder']); const _LIB_SORT_VALUES = new Set([ 'artist', 'artist-desc', 'title', 'title-desc', 'recent', 'year-desc', 'year', 'tuning', + 'difficulty', 'difficulty-desc', ]); const _LIB_FORMAT_VALUES = new Set(['', 'sloppak', 'loose']); // Tree-view expand/collapse persistence. Three states per tree: @@ -2078,6 +2079,7 @@ function renderGridCards(songs, containerId = 'lib-grid', mode = 'replace') { ${(() => { const _nm = _getArrangementNamingMode(); return (song.arrangements || []).map(a => _arrangementBadgeHtml(a, _nm)).join(''); })()} ${tuning ? `${esc(tuning)}` : ''} ${song.has_lyrics ? `Lyrics` : ''} + ${song.user_difficulty != null ? `◆${song.user_difficulty}` : ''} ${duration ? `${duration}` : ''} ${retuneBtn} @@ -2277,6 +2279,8 @@ async function renderTreeInto(containerId, countId, stats, letter, q, favoritesO html += `${esc(tuning)}`; if (song.has_lyrics) html += `Lyrics`; + if (song.user_difficulty != null) + html += `◆${song.user_difficulty}`; if (duration) html += `${duration}`; if (stdRetune) diff --git a/static/index.html b/static/index.html index 906a166..4fe85b5 100644 --- a/static/index.html +++ b/static/index.html @@ -119,6 +119,8 @@ + +