From 21159ad2c50e0b76ee26d021bff56eb8534f2353 Mon Sep 17 00:00:00 2001 From: Honey Date: Wed, 27 May 2026 18:01:09 +0200 Subject: [PATCH] fix: iTunes route order (lookup before proxy); stable cover art URLs for browser caching --- backend/novasonic-server.js | 46 +++++++++++++++++++------------------ src/api.js | 19 ++++++++++++++- src/ui.js | 4 +++- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 11c28dc..abffff0 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -160,8 +160,32 @@ app.post('/api/spotify/track-artwork', async (req, res) => { res.json({ results }); }); +// ─── ITUNES METADATA LOOKUP (must be before the proxy route) ───────────────── +app.get('/api/itunes/lookup', async (req, res) => { + const { artist, title } = req.query; + if (!artist || !title) return res.status(400).json({ error: 'artist and title required' }); + try { + const meta = await lookupItunesTrackMeta(artist, title); + if (!meta.album && !meta.genre && !meta.year) return res.json({ found: false }); + res.json({ + found: true, + album: meta.album || undefined, + year: meta.year || undefined, + trackNum: meta.trackNum || undefined, + totalTracks: meta.totalTracks || undefined, + genre: meta.genre || undefined, + coverUrl: meta.artworkUrl || undefined, + artist: meta.itunesArtistName || undefined, + }); + } catch (e) { + console.error('[itunes/lookup]', e.message); + res.status(500).json({ error: e.message }); + } +}); + // ─── ITUNES PROXY ───────────────────────────────────────────────────────────── // Browsers can't call itunes.apple.com directly (no CORS headers). Proxy it here. +// Note: /api/itunes/lookup is handled above — proxy only covers /search and id-based /lookup. app.get('/api/itunes/:endpoint', (req, res) => { const allowed = new Set(['search', 'lookup']); if (!allowed.has(req.params.endpoint)) return res.status(404).json({ error: 'not found' }); @@ -570,28 +594,6 @@ app.post('/api/soundcloud/resolve', async (req, res) => { } }); -// ─── ITUNES LOOKUP ─────────────────────────────────────────────────────────── -app.get('/api/itunes/lookup', async (req, res) => { - const { artist, title } = req.query; - if (!artist || !title) return res.status(400).json({ error: 'artist and title required' }); - try { - const meta = await lookupItunesTrackMeta(artist, title); - if (!meta.album && !meta.genre && !meta.year) return res.json({ found: false }); - res.json({ - found: true, - album: meta.album || undefined, - year: meta.year || undefined, - trackNum: meta.trackNum || undefined, - totalTracks: meta.totalTracks || undefined, - genre: meta.genre || undefined, - coverUrl: meta.artworkUrl || undefined, - artist: meta.itunesArtistName || undefined, - }); - } catch (e) { - console.error('[itunes/lookup]', e.message); - res.status(500).json({ error: e.message }); - } -}); // ─── MUSICBRAINZ LOOKUP ────────────────────────────────────────────────────── app.get('/api/musicbrainz/lookup', async (req, res) => { diff --git a/src/api.js b/src/api.js index b6adc86..c055a84 100644 --- a/src/api.js +++ b/src/api.js @@ -28,12 +28,29 @@ export async function getLyricsBySongId(songId) { } catch { return []; } } export function downloadUrl(id) { return apiUrl('download', `&id=${id}`); } +// Stable salt derived from credentials so the browser HTTP cache survives page reloads. +// Random salt (apiUrl default) changes every session → cache miss every time → art re-fetches. +let _coverSalt = null, _coverToken = null; +function _coverAuth() { + if (!_coverSalt && S.cfg.user && S.cfg.pass) { + _coverSalt = md5(S.cfg.user + S.cfg.pass).slice(0, 8); + _coverToken = md5(S.cfg.pass + _coverSalt); + } + return _coverSalt ? `u=${encodeURIComponent(S.cfg.user)}&t=${_coverToken}&s=${_coverSalt}` : null; +} export function coverUrl(id, size=300) { if (!id) return null; const key = `${id}:${size}`; - if (!_coverUrlCache.has(key)) _coverUrlCache.set(key, apiUrl('getCoverArt', `&id=${id}&size=${size}`)); + if (!_coverUrlCache.has(key)) { + const auth = _coverAuth(); + const url = auth + ? `${S.cfg.url}/rest/getCoverArt?${auth}&v=1.16.1&c=NovaSonic&f=json&id=${id}&size=${size}` + : apiUrl('getCoverArt', `&id=${id}&size=${size}`); + _coverUrlCache.set(key, url); + } return _coverUrlCache.get(key); } +export function invalidateCoverCache() { _coverUrlCache.clear(); _coverSalt = null; _coverToken = null; } // ─── NAVIDROME REST AUTH ─── export async function ndAuth() { diff --git a/src/ui.js b/src/ui.js index 8da373b..417ef24 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,6 +1,6 @@ import { S } from './state.js'; import { esc, formatDur, toast } from './utils.js'; -import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex } from './api.js'; +import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache } from './api.js'; import { THEMES, applyTheme, getCurrentThemeId } from './themes.js'; // Circular imports — safe (only used inside function bodies) import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon, nativeInsertAt, refreshDisplayMode } from './player.js'; @@ -78,7 +78,9 @@ export function saveSettings() { const pass = document.getElementById('s-pass').value; S.cfg.url = document.getElementById('s-url').value.trim().replace(/\/$/,''); S.cfg.user = document.getElementById('s-user').value.trim(); + const credChanged = S.cfg.url !== document.getElementById('s-url').value.trim().replace(/\/$/,'') || !!pass; if(pass) S.cfg.pass = pass; + if(credChanged) invalidateCoverCache(); S.cfg.lastfmKey = document.getElementById('s-lastfm').value.trim(); S.cfg.backendUrl = document.getElementById('s-backend').value.trim().replace(/\/$/,''); S.cfg.showPlayCount = document.getElementById('s-show-playcount').classList.contains('on');