fix: spotify importer uses same iTunes lookup as global search for artwork + metadata

- _loadSpotifyTrackArtwork: rewritten to call itunesSearch() directly from browser (same as global search) instead of backend endpoint — same quality, works for all regions
- enriches track objects with collectionName, releaseDate, trackNumber, trackExplicitness so downloads get full metadata too
- 8-concurrent batch to avoid hammering iTunes
- adds onerror fallback to makeGlobalTrackItem img so broken URLs show 🎵 placeholder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-04-28 00:49:55 +02:00
1 parent a31953d17e
commit 90c76fe9a7
2 files changed
+34 -29

No files matched your search

+1 -1
View File
@@ -71,7 +71,7 @@ export function makeGlobalTrackItem(t, idx, list) {
el.className = 'gtrack-item'; el.className = 'gtrack-item';
const art = t.artworkUrl60 || t.artworkUrl100 || ''; const art = t.artworkUrl60 || t.artworkUrl100 || '';
el.innerHTML = ` el.innerHTML = `
<div class="gtrack-art">${art ? `<img src="${art}" loading="lazy">` : '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:22px">🎵</div>'}</div> <div class="gtrack-art">${art ? `<img src="${art}" loading="lazy" onerror="this.parentNode.innerHTML='<div style=\\'width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:22px\\'>🎵</div>'">` : '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:22px">🎵</div>'}</div>
<div class="gtrack-info"> <div class="gtrack-info">
<div class="gtrack-title">${esc(t.trackName||t.collectionName||'—')}${t.trackExplicitness==='explicit'?'<span class="explicit-badge">E</span>':''}</div> <div class="gtrack-title">${esc(t.trackName||t.collectionName||'—')}${t.trackExplicitness==='explicit'?'<span class="explicit-badge">E</span>':''}</div>
<div class="gtrack-sub">${esc(t.artistName||'—')} · ${esc(t.collectionName||'')} ${t.releaseDate?'· '+new Date(t.releaseDate).getFullYear():''}</div> <div class="gtrack-sub">${esc(t.artistName||'—')} · ${esc(t.collectionName||'')} ${t.releaseDate?'· '+new Date(t.releaseDate).getFullYear():''}</div>
+33 -28
View File
@@ -1,5 +1,6 @@
import { S } from './state.js'; import { S } from './state.js';
import { esc, toast } from './utils.js'; import { esc, toast } from './utils.js';
import { itunesSearch } from './api.js';
// Circular imports — safe (only used inside function bodies) // Circular imports — safe (only used inside function bodies)
import { makeGlobalTrackItem } from './components.js'; import { makeGlobalTrackItem } from './components.js';
import { openDlModal } from './ui.js'; import { openDlModal } from './ui.js';
@@ -159,43 +160,47 @@ function _renderSpotifyPreview(data) {
} }
async function _loadSpotifyTrackArtwork(tracks, listEl) { async function _loadSpotifyTrackArtwork(tracks, listEl) {
try {
const artEls = listEl.querySelectorAll('.gtrack-art'); const artEls = listEl.querySelectorAll('.gtrack-art');
// Render any artwork already present in the Spotify data immediately // Show any Spotify artwork that's already present immediately
tracks.forEach((t, i) => { tracks.forEach((t, i) => {
const url = t.artworkUrl100 || t.artworkUrl60 || ''; const url = t.artworkUrl100 || t.artworkUrl60 || '';
if (!url) return; if (!url) return;
const el = artEls[i]; const el = artEls[i];
if (!el) return; if (!el) return;
const img = document.createElement('img'); _setArt(el, url);
img.src = url; img.loading = 'lazy';
el.innerHTML = ''; el.appendChild(img);
}); });
const payload = tracks.map((t, i) => ({ // Enrich each track with iTunes data — same call as global search
idx: i, const CONC = 8;
trackName: t.trackName, for (let i = 0; i < tracks.length; i += CONC) {
artistName: t.artistName, const batch = tracks.slice(i, i + CONC);
existingArt: t.artworkUrl100 || t.artworkUrl60 || '', await Promise.all(batch.map(async (t, bi) => {
const idx = i + bi;
try {
const firstArtist = (t.artistName || '').split(/\s*[,/•]\s*/)[0].trim();
const results = await itunesSearch(`${t.trackName} ${firstArtist}`, 'musicTrack', 1);
const match = results[0];
if (!match) return;
// Enrich track object so downloads get correct metadata
if (match.artworkUrl100) t.artworkUrl100 = match.artworkUrl100;
if (match.collectionName) t.collectionName = match.collectionName;
if (match.releaseDate) t.releaseDate = match.releaseDate;
if (match.trackNumber) t.trackNumber = match.trackNumber;
if (match.trackExplicitness) t.trackExplicitness = match.trackExplicitness;
if (window._spotifyResolved?.tracks?.[idx]) Object.assign(window._spotifyResolved.tracks[idx], t);
const url = match.artworkUrl100 || match.artworkUrl60 || '';
if (url) _setArt(artEls[idx], url);
} catch { /* non-critical */ }
})); }));
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();
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);
if (window._spotifyResolved?.tracks?.[r.idx]) {
window._spotifyResolved.tracks[r.idx].artworkUrl100 = r.artworkUrl;
} }
} }
} catch { /* artwork loading is non-critical */ }
function _setArt(el, url) {
if (!el) return;
const img = document.createElement('img');
img.loading = 'lazy';
img.onerror = () => { el.innerHTML = '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:22px">🎵</div>'; };
img.src = url;
el.innerHTML = ''; el.appendChild(img);
} }