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 @@
+
+