From eba20b39fb27ff6c32659f6d66c00231cd5cb600 Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Wed, 1 Jul 2026 05:49:19 -0500 Subject: [PATCH] feat(v3): sort the library by mastery (needs-practice / most-mastered) Adds two sort options to the Songs library: "Needs practice first" (weakest measured accuracy first) and "Most mastered first". Mastery = MAX(best_accuracy) across a song's arrangements, from song_stats; because that's a separate table it's a correlated subquery in the ORDER BY, so these sorts use OFFSET paging like tuning/year. Unscored ("not started") songs always sort to the bottom in both directions, so a large unpracticed library doesn't bury the songs you're actually working on. Never the default. Verified against a running server: scored songs at 0.90 / 0.30 plus unscored -> ascending orders 0.30, 0.90, unscored; descending 0.90, 0.30, unscored. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF --- server.py | 14 ++++++++++++++ static/v3/songs.js | 3 +++ 2 files changed, 17 insertions(+) diff --git a/server.py b/server.py index cbb9f25..347d9e4 100644 --- a/server.py +++ b/server.py @@ -2153,6 +2153,20 @@ class MetadataDB: # '2005' rather than alphabetic. "year": "(year = '') ASC, CAST(year AS INTEGER) ASC", "year-desc": "(year = '') ASC, CAST(year AS INTEGER) DESC", + # Mastery = best accuracy across a song's arrangements, from the + # separate song_stats table (so via a correlated subquery — this sort + # drops to OFFSET paging, like tuning/year). Unscored ("not started") + # songs push to the BOTTOM in both directions (the IS NULL term); + # ascending is "needs practice first" (weakest measured first), + # descending is "most mastered first". + "mastery": ( + "((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) ASC" + ), + "mastery-desc": ( + "((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" + ), } order = sort_map.get(sort, "artist COLLATE NOCASE") # Legacy `dir=desc` toggle: only safe to append on simple sort diff --git a/static/v3/songs.js b/static/v3/songs.js index 5b64135..8e7b182 100644 --- a/static/v3/songs.js +++ b/static/v3/songs.js @@ -35,6 +35,9 @@ ['title', 'Title A–Z'], ['title-desc', 'Title Z–A'], ['recent', 'Recently Added'], ['year-desc', 'Year (newest)'], ['year', 'Year (oldest)'], ['tuning', 'Tuning'], + // Mastery = best accuracy across arrangements (song_stats); unscored songs + // sort last either way. Ascending surfaces what needs work; never default. + ['mastery', 'Needs practice first'], ['mastery-desc', 'Most mastered first'], ]; const FORMATS = [['', 'All formats'], ['sloppak', 'Feedpak'], ['loose', 'Folder']]; const ARRANGEMENTS = ['Lead', 'Rhythm', 'Bass', 'Combo', 'Vocals'];