Add <img onerror> fallbacks for search result images
Self-audit catch: my earlier cover-art commit claimed 'the frontend's <img onerror> fallback handles 404s' — that was wrong. The enhanced search result images in shared-helpers.js renderCompactSection and all five gsearch-item/track templates in downloads.js render bare `<img src="...">` with no fallback. With the MusicBrainz adapter now emitting Cover Art Archive URLs deterministically (no HEAD probe), albums that don't have cover art would show the browser's broken-image icon instead of the emoji placeholder. Two fallback shapes: - shared-helpers.js renderCompactSection: the `<img>` sits inside a card with a sibling placeholder pattern. On error, replace the img's outerHTML with the placeholder div, matching the shape used when config.image is missing entirely. - downloads.js gsearch items: the `<img>` sits inside a `.gsearch-item-art` div whose default text content is the emoji fallback (🎤 / 💿 / 🎶 / 🎵). On error, set parentElement.textContent to the emoji, which wipes the img and shows the glyph. Same shape as the "no image_url" branch. Applies to every card type that renders a user-provided image URL so the fix covers all sources that might return 404s — MB is the most common offender but iTunes/Deezer/Discogs can all miss too. Tested against the live MB API: Metallica albums without CAA cover art now show the 💿 emoji instead of a broken-image icon.
This commit is contained in:
parent
2b7d6c8c7c
commit
a6359a2690
2 changed files with 13 additions and 7 deletions
|
|
@ -5333,13 +5333,13 @@ function _gsRenderFromState(state) {
|
|||
|
||||
if (dbArtists.length) {
|
||||
h += '<div class="gsearch-section-header">📚 In Your Library</div><div class="gsearch-grid">';
|
||||
h += dbArtists.map(a => `<div class="gsearch-item" onclick="_gsClickArtist('${a.id}', '${_escAttr(a.name)}', true)"><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy">` : '🎤'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div><div class="gsearch-item-sub">Library</div></div></div>`).join('');
|
||||
h += dbArtists.map(a => `<div class="gsearch-item" onclick="_gsClickArtist('${a.id}', '${_escAttr(a.name)}', true)"><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy" onerror="this.parentElement.textContent='🎤'">` : '🎤'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div><div class="gsearch-item-sub">Library</div></div></div>`).join('');
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
if (artists.length) {
|
||||
h += `<div class="gsearch-section-header">🎤 Artists <span class="gsearch-source-badge">${srcLabel}</span></div><div class="gsearch-grid" id="gsearch-artists-grid">`;
|
||||
h += artists.map(a => `<div class="gsearch-item" onclick="_gsClickArtist('${a.id}', '${_escAttr(a.name)}', false)" ${!a.image_url ? `data-artist-id="${a.id}" data-needs-image="true"` : ''}><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy">` : '🎤'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div></div></div>`).join('');
|
||||
h += artists.map(a => `<div class="gsearch-item" onclick="_gsClickArtist('${a.id}', '${_escAttr(a.name)}', false)" ${!a.image_url ? `data-artist-id="${a.id}" data-needs-image="true"` : ''}><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy" onerror="this.parentElement.textContent='🎤'">` : '🎤'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div></div></div>`).join('');
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
|
|
@ -5349,7 +5349,7 @@ function _gsRenderFromState(state) {
|
|||
const ar = a.artist || (a.artists ? a.artists.join(', ') : '');
|
||||
const yr = a.release_date ? a.release_date.substring(0, 4) : '';
|
||||
const img = (a.image_url || '').replace(/'/g, "\\'");
|
||||
return `<div class="gsearch-item" onclick="_gsClickAlbum('${a.id}', '${_escAttr(a.name)}', '${_escAttr(ar)}', '${img}', '${activeSrc}')"><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy">` : '💿'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div><div class="gsearch-item-sub">${_escToast(ar)}${yr ? ` · ${yr}` : ''}</div></div></div>`;
|
||||
return `<div class="gsearch-item" onclick="_gsClickAlbum('${a.id}', '${_escAttr(a.name)}', '${_escAttr(ar)}', '${img}', '${activeSrc}')"><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy" onerror="this.parentElement.textContent='💿'">` : '💿'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div><div class="gsearch-item-sub">${_escToast(ar)}${yr ? ` · ${yr}` : ''}</div></div></div>`;
|
||||
}).join('');
|
||||
h += '</div>';
|
||||
}
|
||||
|
|
@ -5359,7 +5359,7 @@ function _gsRenderFromState(state) {
|
|||
h += singles.map(a => {
|
||||
const ar = a.artist || (a.artists ? a.artists.join(', ') : '');
|
||||
const img = (a.image_url || '').replace(/'/g, "\\'");
|
||||
return `<div class="gsearch-item" onclick="_gsClickAlbum('${a.id}', '${_escAttr(a.name)}', '${_escAttr(ar)}', '${img}', '${activeSrc}')"><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy">` : '🎶'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div><div class="gsearch-item-sub">${_escToast(ar)}</div></div></div>`;
|
||||
return `<div class="gsearch-item" onclick="_gsClickAlbum('${a.id}', '${_escAttr(a.name)}', '${_escAttr(ar)}', '${img}', '${activeSrc}')"><div class="gsearch-item-art">${a.image_url ? `<img src="${a.image_url}" loading="lazy" onerror="this.parentElement.textContent='🎶'">` : '🎶'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(a.name)}</div><div class="gsearch-item-sub">${_escToast(ar)}</div></div></div>`;
|
||||
}).join('');
|
||||
h += '</div>';
|
||||
}
|
||||
|
|
@ -5369,7 +5369,7 @@ function _gsRenderFromState(state) {
|
|||
h += tracks.map(t => {
|
||||
const ar = t.artist || (t.artists ? t.artists.join(', ') : '');
|
||||
const dur = t.duration_ms ? `${Math.floor(t.duration_ms / 60000)}:${String(Math.floor((t.duration_ms % 60000) / 1000)).padStart(2, '0')}` : '';
|
||||
return `<div class="gsearch-track" onclick="_gsClickTrack('${_escAttr(ar)}', '${_escAttr(t.name)}', '${_escAttr(t.album || '')}', '${_escAttr(t.id || '')}', '${_escAttr(t.image_url || '')}', ${t.duration_ms || 0})"><div class="gsearch-item-art" style="width:32px;height:32px;border-radius:6px">${t.image_url ? `<img src="${t.image_url}" loading="lazy">` : '🎵'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(t.name)}</div><div class="gsearch-item-sub">${_escToast(ar)}${t.album ? ` · ${_escToast(t.album)}` : ''}</div></div><div class="gsearch-track-dur">${dur}</div><button class="gsearch-play-btn" onclick="event.stopPropagation(); _gsPlayTrack('${_escAttr(t.name)}', '${_escAttr(ar)}', '${_escAttr(t.album || '')}')" title="Stream">▶</button></div>`;
|
||||
return `<div class="gsearch-track" onclick="_gsClickTrack('${_escAttr(ar)}', '${_escAttr(t.name)}', '${_escAttr(t.album || '')}', '${_escAttr(t.id || '')}', '${_escAttr(t.image_url || '')}', ${t.duration_ms || 0})"><div class="gsearch-item-art" style="width:32px;height:32px;border-radius:6px">${t.image_url ? `<img src="${t.image_url}" loading="lazy" onerror="this.parentElement.textContent='🎵'">` : '🎵'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(t.name)}</div><div class="gsearch-item-sub">${_escToast(ar)}${t.album ? ` · ${_escToast(t.album)}` : ''}</div></div><div class="gsearch-track-dur">${dur}</div><button class="gsearch-play-btn" onclick="event.stopPropagation(); _gsPlayTrack('${_escAttr(t.name)}', '${_escAttr(ar)}', '${_escAttr(t.album || '')}')" title="Stream">▶</button></div>`;
|
||||
}).join('');
|
||||
h += '</div>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -559,9 +559,15 @@ function renderCompactSection(sectionId, listId, countId, items, mapItem) {
|
|||
placeholderClass += ' track-placeholder';
|
||||
}
|
||||
|
||||
// Fallback placeholder used when the image 404s (common for MB
|
||||
// Cover Art Archive URLs — we construct them deterministically
|
||||
// without probing first, so some will miss). Without onerror the
|
||||
// browser shows its broken-image icon.
|
||||
const placeholderHtml = `<div class="${placeholderClass}" data-lazy-image="true">${config.placeholder}</div>`;
|
||||
const escapedFallback = placeholderHtml.replace(/"/g, '"');
|
||||
const imageHtml = config.image
|
||||
? `<img src="${escapeHtml(config.image)}" class="${imageClass}" alt="${escapeHtml(config.name)}">`
|
||||
: `<div class="${placeholderClass}" data-lazy-image="true">${config.placeholder}</div>`;
|
||||
? `<img src="${escapeHtml(config.image)}" class="${imageClass}" alt="${escapeHtml(config.name)}" onerror="this.outerHTML='${escapedFallback}'">`
|
||||
: placeholderHtml;
|
||||
|
||||
const badgeHtml = config.badge
|
||||
? `<div class="enh-item-badge ${config.badge.class}">${config.badge.text}</div>`
|
||||
|
|
|
|||
Loading…
Reference in a new issue