fix: artist hero appears instantly; album names skip romaji conversion

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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-08 22:23:35 +02:00
1 parent 28489f722a
commit 8f7f1e2635
3 files changed
+43 -49

No files matched your search

+2 -2
View File
@@ -8,7 +8,7 @@ window._imgRetry = function(img, fallback) {
if (n > 3 || !img.src) { img.parentNode.innerHTML = `<div class="no-art">${fallback}</div>`; return; } if (n > 3 || !img.src) { img.parentNode.innerHTML = `<div class="no-art">${fallback}</div>`; return; }
setTimeout(() => { const s = img.src; img.src = ''; img.src = s; }, 1500 * n); 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) // Circular imports — safe (only used inside event handlers after module init)
import { playQueue, toggleStar, playPreview, parseArtists } from '../player/player.js'; import { playQueue, toggleStar, playPreview, parseArtists } from '../player/player.js';
import { showCtxMenu, openDlModal } from './ui.js'; import { showCtxMenu, openDlModal } from './ui.js';
@@ -28,7 +28,7 @@ export function makeCard(title, sub, imgUrl, fallback='💿') {
export function makeAlbumCard(a) { export function makeAlbumCard(a) {
const cv = coverUrl(a.coverArt, 512); 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-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(a); };
c.querySelector('.card-info').onclick = () => navigate('album-detail', a); c.querySelector('.card-info').onclick = () => navigate('album-detail', a);
return c; return c;
+39 -45
View File
@@ -1,6 +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 { 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 { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js';
import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js'; import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js';
import { showCtxMenu, openDlModal } from '../ui/ui.js'; import { showCtxMenu, openDlModal } from '../ui/ui.js';
@@ -87,8 +87,6 @@ export async function loadArtistDetail(artist) {
const knownItunes = artist.itunesArtistId || null; const knownItunes = artist.itunesArtistId || null;
// Fire all requests immediately so they run in parallel. // 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 imgP = hasND ? getArtistImage(artist.id) : Promise.resolve(null);
const albumP = hasND ? api('getArtist', `&id=${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); 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 iArtistP = knownItunes ? Promise.resolve([{ artistId: knownItunes }]) : itunesSearch(artist.name, 'musicArtist', 5);
const eventsP = fetchEvents(artist.name); const eventsP = fetchEvents(artist.name);
// Phase 1 — hero (fast: image usually cached from listing, getArtist is local) // Phase 1 — hero: only wait for albumP (fast local Navidrome call, no external API).
const [imgRes, albumRes] = await Promise.allSettled([imgP, albumP]); // 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) // Phase 2 — content (may be slow: Last.fm bio, iTunes, events)
const [infoRes, topRes, iArtistRes, eventsRes] = await Promise.allSettled([infoP, topP, iArtistP, eventsP]); 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' }]; : [{ 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 ── // ── Bio ──
const info = infoRes.status === 'fulfilled' ? infoRes.value?.artistInfo2 : null; const info = infoRes.status === 'fulfilled' ? infoRes.value?.artistInfo2 : null;
if(info?.biography && bioEl) { if(info?.biography && bioEl) {
@@ -384,7 +378,7 @@ export async function loadItunesAlbumDetail(album) {
export async function loadAlbumDetail(album) { export async function loadAlbumDetail(album) {
document.getElementById('album-back-btn').onclick = () => document.getElementById('album-back-btn').onclick = () =>
album._fromArtist ? navigate('artist-detail', album._fromArtist) : navigate('albums'); 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'); const artistEl = document.getElementById('album-detail-artist');
artistEl.innerHTML = parseArtists(album) artistEl.innerHTML = parseArtists(album)
.map(a => `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(a.originalName||a.name)}','${esc(a.id||'')}')">${esc(dispArtistById(a.id, a.name))}</span>`) .map(a => `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(a.originalName||a.name)}','${esc(a.id||'')}')">${esc(dispArtistById(a.id, a.name))}</span>`)
@@ -400,7 +394,7 @@ export async function loadAlbumDetail(album) {
const totalMin = Math.round(songs.reduce((s,t) => s+(t.duration||0), 0) / 60); 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.`; document.getElementById('album-detail-stats').textContent = `${songs.length} Titel · ${r.album.year||''} · ${totalMin} Min.`;
if (songs[0]) { 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]) artistEl.innerHTML = parseArtists(songs[0])
.map(a => `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(a.originalName||a.name)}','${esc(a.id||'')}')">${esc(dispArtistById(a.id, a.name))}</span>`) .map(a => `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(a.originalName||a.name)}','${esc(a.id||'')}')">${esc(dispArtistById(a.id, a.name))}</span>`)
.join('<span style="color:var(--text3)"> • </span>') || ''; .join('<span style="color:var(--text3)"> • </span>') || '';
+2 -2
View File
@@ -1,6 +1,6 @@
import { S } from '../state.js'; import { S } from '../state.js';
import { esc } from '../utils.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 { makeCard, makeAlbumCard, makeSongItem } from '../ui/components.js';
import { playQueue } from '../player/player.js'; import { playQueue } from '../player/player.js';
// Circular — safe (called inside event handlers) // Circular — safe (called inside event handlers)
@@ -60,7 +60,7 @@ async function loadHeroCard() {
${thumbUrl ? `<img class="home-hero-cover" src="${thumbUrl}" alt="${esc(album.name)}">` : ''} ${thumbUrl ? `<img class="home-hero-cover" src="${thumbUrl}" alt="${esc(album.name)}">` : ''}
<div class="home-hero-info"> <div class="home-hero-info">
<div class="home-hero-label">${heroLabel}</div> <div class="home-hero-label">${heroLabel}</div>
<div class="home-hero-title">${esc(dispAlbumName(album.name))}</div> <div class="home-hero-title">${esc(album.name || '')}</div>
<div class="home-hero-artist">${esc(dispArtistById(album.artistId, album.artist || ''))}</div> <div class="home-hero-artist">${esc(dispArtistById(album.artistId, album.artist || ''))}</div>
</div> </div>
<button class="home-hero-play" title="Abspielen"> <button class="home-hero-play" title="Abspielen">