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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-23 12:02:25 +02:00
1 parent 790f02e5b4
commit 4f83eebe79
3 files changed
+39 -2

No files matched your search

+1 -1
View File
@@ -123,4 +123,4 @@ function clearRomajiCache() { romajiCache.clear(); }
function isTokenizerReady() { return !!_tokenizer; } function isTokenizerReady() { return !!_tokenizer; }
module.exports = { buildRomajiIndex, clearRomajiCache, isTokenizerReady }; module.exports = { buildRomajiIndex, clearRomajiCache, isTokenizerReady, fetchNdSongs: _fetchNdSongs };
+27 -1
View File
@@ -18,7 +18,7 @@ const CONFIG = require('./lib/config');
const { jobs, updateJob, log } = require('./lib/jobs'); const { jobs, updateJob, log } = require('./lib/jobs');
const { lookupItunesTrackMeta, fetchItunesTracks, searchItunesAlbumTracks } = require('./lib/metadata'); const { lookupItunesTrackMeta, fetchItunesTracks, searchItunesAlbumTracks } = require('./lib/metadata');
const { downloadTrack, downloadAlbumTracks } = require('./lib/downloader'); const { downloadTrack, downloadAlbumTracks } = require('./lib/downloader');
const { buildRomajiIndex, clearRomajiCache, isTokenizerReady } = require('./lib/romaji'); const { buildRomajiIndex, clearRomajiCache, isTokenizerReady, fetchNdSongs } = require('./lib/romaji');
// ─── INIT ───────────────────────────────────────────────────────────────────── // ─── INIT ─────────────────────────────────────────────────────────────────────
const app = express(); const app = express();
@@ -92,6 +92,32 @@ app.delete('/api/romaji-index', (req, res) => {
res.json({ ok: true }); 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) => { app.post('/api/spotify/track-artwork', async (req, res) => {
const tracks = req.body.tracks || []; const tracks = req.body.tracks || [];
if (!tracks.length) return res.json({ results: [] }); if (!tracks.length) return res.json({ results: [] });
+11
View File
@@ -182,6 +182,17 @@ async function loadGenreSection() {
_renderGenres(); _renderGenres();
if (_allGenres.length > GENRE_LIMIT() && more) more.style.display = ''; if (_allGenres.length > GENRE_LIMIT() && more) more.style.display = '';
} catch { el.style.display = 'none'; } } 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() { function _renderGenres() {