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:
1 parent
790f02e5b4
commit
4f83eebe79
3 files changed
+39
-2
No files matched your search
@@ -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 };
|
||||||
@@ -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: [] });
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in new issue
Block a user