From 8f7f1e263527456f38920d43e7c5f4670cb79fde Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 8 Jun 2026 22:23:35 +0200 Subject: [PATCH] fix: artist hero appears instantly; album names skip romaji conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hero now only waits for getArtist (fast local Navidrome) before revealing. getArtistImage (Last.fm lookup) was the 2-4s bottleneck when uncached โ€” it now resolves async and patches in the artist photo after the hero is already visible. Album card, album detail header, and home hero now display the raw album name from Navidrome instead of converting to romaji when showRomaji is on. Co-Authored-By: Claude Sonnet 4.6 --- src/ui/components.js | 4 +-- src/views/detail.js | 84 ++++++++++++++++++++------------------------ src/views/home.js | 4 +-- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/src/ui/components.js b/src/ui/components.js index 5acdee3..ed5f4bf 100644 --- a/src/ui/components.js +++ b/src/ui/components.js @@ -8,7 +8,7 @@ window._imgRetry = function(img, fallback) { if (n > 3 || !img.src) { img.parentNode.innerHTML = `
${fallback}
`; return; } setTimeout(() => { const s = img.src; img.src = ''; img.src = s; }, 1500 * n); }; -import { coverUrl, dispTitle, dispArtistParts, dispAlbum, dispArtistById, dispAlbumName } from '../api.js'; +import { coverUrl, dispTitle, dispArtistParts, dispArtistById } from '../api.js'; // Circular imports โ€” safe (only used inside event handlers after module init) import { playQueue, toggleStar, playPreview, parseArtists } from '../player/player.js'; import { showCtxMenu, openDlModal } from './ui.js'; @@ -28,7 +28,7 @@ export function makeCard(title, sub, imgUrl, fallback='๐Ÿ’ฟ') { export function makeAlbumCard(a) { const cv = coverUrl(a.coverArt, 512); - const c = makeCard(dispAlbumName(a.name), dispArtistById(a.artistId, a.artist||''), cv, '๐Ÿ’ฟ'); + const c = makeCard(a.name || '', dispArtistById(a.artistId, a.artist||''), cv, '๐Ÿ’ฟ'); c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(a); }; c.querySelector('.card-info').onclick = () => navigate('album-detail', a); return c; diff --git a/src/views/detail.js b/src/views/detail.js index 809fe05..9833eb1 100644 --- a/src/views/detail.js +++ b/src/views/detail.js @@ -1,6 +1,6 @@ import { S } from '../state.js'; import { esc, toast } from '../utils.js'; -import { api, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, fetchRomajiIndex, dispArtistById, dispAlbumName, dispAlbum } from '../api.js'; +import { api, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, fetchRomajiIndex, dispArtistById } from '../api.js'; import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js'; import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js'; import { showCtxMenu, openDlModal } from '../ui/ui.js'; @@ -87,8 +87,6 @@ export async function loadArtistDetail(artist) { const knownItunes = artist.itunesArtistId || null; // Fire all requests immediately so they run in parallel. - // Hero only needs imgP + albumP (both fast / usually cached) โ€” await those - // first so the hero appears instantly without waiting for iTunes or events. const imgP = hasND ? getArtistImage(artist.id) : Promise.resolve(null); const albumP = hasND ? api('getArtist', `&id=${artist.id}`) : Promise.resolve(null); const infoP = hasND ? api('getArtistInfo2',`&id=${artist.id}&count=8`) : Promise.resolve(null); @@ -96,8 +94,42 @@ export async function loadArtistDetail(artist) { const iArtistP = knownItunes ? Promise.resolve([{ artistId: knownItunes }]) : itunesSearch(artist.name, 'musicArtist', 5); const eventsP = fetchEvents(artist.name); - // Phase 1 โ€” hero (fast: image usually cached from listing, getArtist is local) - const [imgRes, albumRes] = await Promise.allSettled([imgP, albumP]); + // Phase 1 โ€” hero: only wait for albumP (fast local Navidrome call, no external API). + // imgP (getArtistImage) may be slow when uncached โ€” it calls Last.fm via Navidrome. + // We reveal the hero with the album cover immediately, then patch in the artist photo async. + const [albumRes] = await Promise.allSettled([albumP]); + + const _firstAlbums = albumRes.status === 'fulfilled' ? (albumRes.value?.artist?.album || []) : []; + const _bgCover = _firstAlbums[0] ? coverUrl(_firstAlbums[0].coverArt, 512) : null; + + const _revealHero = () => requestAnimationFrame(() => { + hero.style.transition = 'opacity 0.25s ease'; + hero.style.opacity = '1'; + }); + + const bgEl = document.getElementById('artist-hero-bg'); + const pfpWrap = document.getElementById('artist-hero-pfp-wrap'); + + if (_bgCover) { + hero.style.background = ''; + const _pre = new Image(); + _pre.onload = _pre.onerror = () => { if (bgEl) bgEl.style.backgroundImage = `url('${_bgCover}')`; _revealHero(); }; + _pre.src = _bgCover; + } else { + _revealHero(); + } + + // Patch artist photo in async โ€” resolves immediately if cached, otherwise after Last.fm + imgP.then(img => { + if (!img || !pfpWrap) return; + const pfpImg = document.createElement('img'); + pfpImg.className = 'artist-hero-pfp'; + pfpImg.alt = ''; + pfpImg.onload = pfpImg.onerror = () => { if (bgEl) bgEl.style.backgroundImage = `url('${img}')`; }; + pfpImg.src = img; + pfpWrap.prepend(pfpImg); + document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none'); + }).catch(() => {}); // Phase 2 โ€” content (may be slow: Last.fm bio, iTunes, events) const [infoRes, topRes, iArtistRes, eventsRes] = await Promise.allSettled([infoP, topP, iArtistP, eventsP]); @@ -119,44 +151,6 @@ export async function loadArtistDetail(artist) { ]) : [{ status: 'rejected' }, { status: 'rejected' }]; - // โ”€โ”€ Hero โ”€โ”€ non-blocking: set bg/pfp immediately, fade in on image load, tint after - const img = imgRes.status === 'fulfilled' ? imgRes.value : null; - const _firstAlbums = albumRes.status === 'fulfilled' ? (albumRes.value?.artist?.album || []) : []; - const _bgCover = _firstAlbums[0] ? coverUrl(_firstAlbums[0].coverArt, 512) : null; - const bgSrc = img || _bgCover; - - const _revealHero = () => requestAnimationFrame(() => { - hero.style.transition = 'opacity 0.25s ease'; - hero.style.opacity = '1'; - }); - - if(img) { - if(bgSrc) hero.style.background = ''; - const bgEl = document.getElementById('artist-hero-bg'); - const pfpWrap = document.getElementById('artist-hero-pfp-wrap'); - if(pfpWrap) { - const pfpImg = document.createElement('img'); - pfpImg.className = 'artist-hero-pfp'; - pfpImg.alt = ''; - pfpImg.onload = pfpImg.onerror = () => { - // image is now in cache โ€” set bg background-image here so it renders instantly - if(bgEl && bgSrc) bgEl.style.backgroundImage = `url('${bgSrc}')`; - _revealHero(); - }; - pfpImg.src = img; - pfpWrap.prepend(pfpImg); - document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none'); - } - } else if(bgSrc) { - hero.style.background = ''; - const bgEl = document.getElementById('artist-hero-bg'); - const _pre = new Image(); - _pre.onload = _pre.onerror = () => { if(bgEl) bgEl.style.backgroundImage = `url('${bgSrc}')`; _revealHero(); }; - _pre.src = bgSrc; - } else { - _revealHero(); - } - // โ”€โ”€ Bio โ”€โ”€ const info = infoRes.status === 'fulfilled' ? infoRes.value?.artistInfo2 : null; if(info?.biography && bioEl) { @@ -384,7 +378,7 @@ export async function loadItunesAlbumDetail(album) { export async function loadAlbumDetail(album) { document.getElementById('album-back-btn').onclick = () => album._fromArtist ? navigate('artist-detail', album._fromArtist) : navigate('albums'); - document.getElementById('album-detail-name').textContent = dispAlbumName(album.name); + document.getElementById('album-detail-name').textContent = album.name || ''; const artistEl = document.getElementById('album-detail-artist'); artistEl.innerHTML = parseArtists(album) .map(a => `${esc(dispArtistById(a.id, a.name))}`) @@ -400,7 +394,7 @@ export async function loadAlbumDetail(album) { const totalMin = Math.round(songs.reduce((s,t) => s+(t.duration||0), 0) / 60); document.getElementById('album-detail-stats').textContent = `${songs.length} Titel ยท ${r.album.year||''} ยท ${totalMin} Min.`; if (songs[0]) { - document.getElementById('album-detail-name').textContent = dispAlbum(songs[0]) || dispAlbumName(album.name); + document.getElementById('album-detail-name').textContent = songs[0].album || album.name || ''; artistEl.innerHTML = parseArtists(songs[0]) .map(a => `${esc(dispArtistById(a.id, a.name))}`) .join(' โ€ข ') || ''; diff --git a/src/views/home.js b/src/views/home.js index a539e4b..df5a80a 100644 --- a/src/views/home.js +++ b/src/views/home.js @@ -1,6 +1,6 @@ import { S } from '../state.js'; import { esc } from '../utils.js'; -import { api, apiCached, apiIsCached, coverUrl, fetchRomajiIndex, dispArtistById, dispAlbumName } from '../api.js'; +import { api, apiCached, apiIsCached, coverUrl, fetchRomajiIndex, dispArtistById } from '../api.js'; import { makeCard, makeAlbumCard, makeSongItem } from '../ui/components.js'; import { playQueue } from '../player/player.js'; // Circular โ€” safe (called inside event handlers) @@ -60,7 +60,7 @@ async function loadHeroCard() { ${thumbUrl ? `${esc(album.name)}` : ''}
${heroLabel}
-
${esc(dispAlbumName(album.name))}
+
${esc(album.name || '')}
${esc(dispArtistById(album.artistId, album.artist || ''))}