feat(spotify): lazy-load per-track iTunes artwork for playlists/albums

Adds POST /api/spotify/track-artwork endpoint that takes an array of
{idx, trackName, artistName} and returns iTunes artwork URLs in batches
of 8 concurrent requests.

Frontend calls this after rendering the track list, then patches each
.gtrack-art element with the correct album cover as results arrive.
Also updates _spotifyResolved.tracks so downloads use the right artwork.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-04-27 18:19:08 +02:00
1 parent a305ec98e2
commit 5446d83ab3
2 files changed
+49

No files matched your search

+16
View File
@@ -484,6 +484,22 @@ function _spTrackItem(t, defaultAlbum, defaultArt, defaultAlbumArtist) {
// ─── ROUTES ────────────────────────────────────────────────────────────────── // ─── ROUTES ──────────────────────────────────────────────────────────────────
app.post('/api/spotify/track-artwork', async (req, res) => {
const tracks = req.body.tracks || [];
if (!tracks.length) return res.json({ results: [] });
const CONC = 8;
const results = [];
for (let i = 0; i < tracks.length; i += CONC) {
const batch = tracks.slice(i, i + CONC);
const batchRes = await Promise.all(batch.map(async t => {
const meta = await lookupItunesTrackMeta(t.artistName || '', t.trackName || '');
return { idx: t.idx, artworkUrl: meta.artworkUrl || '' };
}));
results.push(...batchRes);
}
res.json({ results });
});
app.get('/api/health', (req, res) => { app.get('/api/health', (req, res) => {
res.json({ status: 'ok', ytdlpVersion, musicPath: CONFIG.musicPath, audioFormat: CONFIG.audioFmt, activeJobs: activeDownloads, queueLength: downloadQueue.length }); res.json({ status: 'ok', ytdlpVersion, musicPath: CONFIG.musicPath, audioFormat: CONFIG.audioFmt, activeJobs: activeDownloads, queueLength: downloadQueue.length });
}); });
+33
View File
@@ -1043,6 +1043,39 @@ function _renderSpotifyPreview(data) {
list.innerHTML = ''; list.innerHTML = '';
(data.tracks || []).forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, data.tracks))); (data.tracks || []).forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, data.tracks)));
section.style.display = ''; section.style.display = '';
// Lazy-load correct per-track artwork via iTunes in the background
if ((data.type === 'playlist' || data.type === 'album') && count > 0 && S.cfg.backendUrl) {
_loadSpotifyTrackArtwork(data.tracks, list);
}
}
async function _loadSpotifyTrackArtwork(tracks, listEl) {
try {
const payload = tracks.map((t, i) => ({ idx: i, trackName: t.trackName, artistName: t.artistName }));
const res = await fetch(`${S.cfg.backendUrl}/api/spotify/track-artwork`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tracks: payload }),
});
if (!res.ok) return;
const { results } = await res.json();
const artEls = listEl.querySelectorAll('.gtrack-art');
for (const r of results) {
if (!r.artworkUrl || r.idx == null) continue;
const el = artEls[r.idx];
if (!el) continue;
const img = document.createElement('img');
img.src = r.artworkUrl;
img.loading = 'lazy';
el.innerHTML = '';
el.appendChild(img);
// Update resolved data so downloads use the correct artwork
if (window._spotifyResolved?.tracks?.[r.idx]) {
window._spotifyResolved.tracks[r.idx].artworkUrl100 = r.artworkUrl;
}
}
} catch { /* artwork loading is non-critical */ }
} }
// ─── HELPERS ─── // ─── HELPERS ───