From 4f83eebe7948bf75ce861b4b8a1d8143188f2f21 Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 23 May 2026 12:02:25 +0200 Subject: [PATCH] feat: sort genre pills by total play count via backend Page loads instantly with song-count order, then silently re-sorts once the backend aggregates play counts across all songs. Result cached 20 min server-side to avoid repeated full-library fetches. Co-Authored-By: Claude Sonnet 4.6 --- backend/lib/romaji.js | 2 +- backend/novasonic-server.js | 28 +++++++++++++++++++++++++++- src/views.js | 11 +++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/backend/lib/romaji.js b/backend/lib/romaji.js index 02f04ee..dff9dbc 100644 --- a/backend/lib/romaji.js +++ b/backend/lib/romaji.js @@ -123,4 +123,4 @@ function clearRomajiCache() { romajiCache.clear(); } function isTokenizerReady() { return !!_tokenizer; } -module.exports = { buildRomajiIndex, clearRomajiCache, isTokenizerReady }; +module.exports = { buildRomajiIndex, clearRomajiCache, isTokenizerReady, fetchNdSongs: _fetchNdSongs }; diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 615b304..f2d90c9 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -18,7 +18,7 @@ const CONFIG = require('./lib/config'); const { jobs, updateJob, log } = require('./lib/jobs'); const { lookupItunesTrackMeta, fetchItunesTracks, searchItunesAlbumTracks } = require('./lib/metadata'); const { downloadTrack, downloadAlbumTracks } = require('./lib/downloader'); -const { buildRomajiIndex, clearRomajiCache, isTokenizerReady } = require('./lib/romaji'); +const { buildRomajiIndex, clearRomajiCache, isTokenizerReady, fetchNdSongs } = require('./lib/romaji'); // ─── INIT ───────────────────────────────────────────────────────────────────── const app = express(); @@ -92,6 +92,32 @@ app.delete('/api/romaji-index', (req, res) => { res.json({ ok: true }); }); +// GET /api/genre-playcounts?ndUrl=...&user=...&pass=... +// Returns { [genre]: totalPlayCount } sorted by plays desc, cached for 20 min. +const _genrePlayCache = new Map(); +const GENRE_PLAY_TTL = 20 * 60 * 1000; +app.get('/api/genre-playcounts', async (req, res) => { + const { ndUrl, user, pass } = req.query; + if (!ndUrl || !user || !pass) return res.status(400).json({ error: 'missing params' }); + const key = `${ndUrl}|${user}`; + const cached = _genrePlayCache.get(key); + if (cached && Date.now() - cached.ts < GENRE_PLAY_TTL) return res.json(cached.data); + try { + const songs = await fetchNdSongs(ndUrl, user, pass); + const totals = {}; + for (const s of songs) { + const g = s.genre; + if (!g || !(s.playCount > 0)) continue; + totals[g] = (totals[g] || 0) + s.playCount; + } + _genrePlayCache.set(key, { ts: Date.now(), data: totals }); + res.json(totals); + } catch (e) { + console.error('genre-playcounts error:', e); + res.status(500).json({ error: e.message }); + } +}); + app.post('/api/spotify/track-artwork', async (req, res) => { const tracks = req.body.tracks || []; if (!tracks.length) return res.json({ results: [] }); diff --git a/src/views.js b/src/views.js index 1ec79a8..042403b 100644 --- a/src/views.js +++ b/src/views.js @@ -182,6 +182,17 @@ async function loadGenreSection() { _renderGenres(); if (_allGenres.length > GENRE_LIMIT() && more) more.style.display = ''; } catch { el.style.display = 'none'; } + // Re-sort by total play count once backend data arrives (non-blocking) + if (S.cfg.backendUrl && _allGenres.length) { + fetch(`${S.cfg.backendUrl}/api/genre-playcounts?ndUrl=${encodeURIComponent(S.cfg.url)}&user=${encodeURIComponent(S.cfg.user)}&pass=${encodeURIComponent(S.cfg.pass)}`) + .then(r => r.ok ? r.json() : null) + .then(plays => { + if (!plays || !_allGenres.length) return; + _allGenres.sort((a, b) => (plays[b.value] || 0) - (plays[a.value] || 0)); + _renderGenres(); + }) + .catch(() => {}); + } } function _renderGenres() {