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 ? `
` : ''}
${heroLabel}
-
${esc(dispAlbumName(album.name))}
+
${esc(album.name || '')}
${esc(dispArtistById(album.artistId, album.artist || ''))}