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 { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js'; import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js'; import { showCtxMenu, openDlModal } from '../ui/ui.js'; // Circular — safe (called inside event handlers / function bodies) import { navigate, _navGoBack } from './navigation.js'; import { getArtistImage } from './home.js'; // ─── QUICK PLAY HELPERS ─── export async function quickPlayAlbum(a) { try { const r = await api('getAlbum', `&id=${a.id}`); playQueue(r.album?.song || [], 0); } catch(e) { toast('Fehler: ' + e.message); } } export async function quickPlayArtist(a) { try { toast('Lade Songs…'); const r = await api('getArtist', `&id=${a.id}`); const albums = r.artist?.album || []; if (!albums.length) { toast('Keine Alben gefunden'); return; } const results = await Promise.all(albums.map(al => api('getAlbum', `&id=${al.id}`))); const songs = results.flatMap(ar => ar.album?.song || []); if (songs.length) playQueue(songs, 0); else toast('Keine Songs gefunden'); } catch(e) { toast('Fehler: ' + e.message); } } export async function quickPlayPlaylist(pl) { try { const r = await api('getPlaylist', `&id=${pl.id}`); playQueue(r.playlist?.entry || [], 0); } catch(e) { toast('Fehler: ' + e.message); } } // ─── SHARED TOPSONG ROW ─── function _makeTopsongRow(s, num, subtitle, list, idx) { const cv = coverUrl(s.coverArt, 128); if (s.starred && !s._starred) s._starred = true; const row = document.createElement('div'); row.className = 'topsong-item'; row.innerHTML = `
${num}
${cv ? `` : '
🎵
'}
${esc(s.title)}
${subtitle ? `
${esc(subtitle)}
` : ''}
${s.playCount ? s.playCount.toLocaleString() : '—'}
`; row.onclick = () => playQueue(list, idx, { noShuffle: true }); row.addEventListener('contextmenu', e => { e.preventDefault(); showCtxMenu(e, s, list, idx); }); const btns = row.querySelectorAll('.icon-btn'); btns[0].onclick = e => { e.stopPropagation(); toggleStar(s, btns[0]); }; btns[1].onclick = e => { e.stopPropagation(); showCtxMenu(e, s, list, idx); }; return row; } // ─── ARTIST DETAIL ─── export async function loadArtistDetail(artist) { const nameEl = document.getElementById('artist-detail-name'); nameEl.textContent = artist.name; document.getElementById('artist-back-btn').onclick = () => _navGoBack(); if (S.cfg.showRomaji) { fetchRomajiIndex() .then(() => { nameEl.textContent = dispArtistById(artist.id, artist.name); }) .catch(() => {}); } const hero = document.getElementById('artist-detail-hero'); const bioEl = document.getElementById('artist-bio-section'); const simEl = document.getElementById('artist-similar-section'); const tsEl = document.getElementById('artist-topsongs-section'); const albEl = document.getElementById('artist-albums-section'); hero.style.background = 'linear-gradient(135deg, var(--surface2), var(--surface))'; hero.querySelector('.artist-hero-img')?.remove(); const overlay = hero.querySelector('.artist-hero-overlay'); if(overlay) overlay.style.background = ''; [bioEl, simEl, tsEl, albEl].forEach(el => { if(el) el.innerHTML = ''; }); if(albEl) albEl.innerHTML = '
'; const hasND = !!artist.id; const knownItunes = artist.itunesArtistId || null; const [imgRes, albumRes, infoRes, topRes, iArtistRes] = await Promise.allSettled([ hasND ? getArtistImage(artist.id) : Promise.resolve(null), hasND ? api('getArtist', `&id=${artist.id}`) : Promise.resolve(null), hasND ? api('getArtistInfo2',`&id=${artist.id}&count=8`) : Promise.resolve(null), hasND ? api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`) : Promise.resolve(null), knownItunes ? Promise.resolve([{ artistId: knownItunes }]) : itunesSearch(artist.name, 'musicArtist', 5), ]); const _normWords = s => new Set((s || '').toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(Boolean)); const _queryWords = _normWords(artist.name); const iArtistResults = iArtistRes.status === 'fulfilled' ? (iArtistRes.value || []) : []; const iArtistMatch = iArtistResults.find(r => { if (_queryWords.size === 0) return true; const rw = _normWords(r.artistName); return [..._queryWords].every(w => rw.has(w)); }); const itunesArtistId = knownItunes ?? iArtistMatch?.artistId ?? null; const [iAlbumsRes, iTracksRes] = itunesArtistId ? await Promise.allSettled([ itunesLookupArtistAlbums(itunesArtistId), itunesLookupArtistSongs(itunesArtistId, 15), ]) : [{ status: 'rejected' }, { status: 'rejected' }]; // ── Hero ── const img = imgRes.status === 'fulfilled' ? imgRes.value : null; if(img) { hero.style.background = ''; const heroImg = document.createElement('img'); heroImg.className = 'artist-hero-img'; heroImg.alt = ''; heroImg.src = img; hero.insertBefore(heroImg, hero.firstChild); if(overlay) overlay.style.background = 'linear-gradient(to right,rgba(7,7,15,.95) 0%,rgba(7,7,15,.35) 55%,rgba(7,7,15,.75) 100%)'; } // ── Bio ── const info = infoRes.status === 'fulfilled' ? infoRes.value?.artistInfo2 : null; if(info?.biography && bioEl) { const bio = info.biography.replace(/]+>/g,'').replace(/<\/a>/g,'').replace(/<[^>]+>/g,'').trim(); const short = bio.length > 400 ? bio.slice(0,400)+'…' : bio; if(short) { window._artistBioFull = bio; bioEl.innerHTML = `
Über
${esc(short)}${bio.length > 400 ? ` ` : ''}
`; } } // ── Similar artists ── const similar = info?.similarArtist || []; if(similar.length && simEl) { simEl.innerHTML = `
Ähnliche Künstler
${similar.map(a => ` ${esc(a.name)}`).join('')}
`; } // ── Top Songs ── const ndAlbums = albumRes.status === 'fulfilled' ? (albumRes.value?.artist?.album || []) : []; const topSongs = topRes.status === 'fulfilled' ? (topRes.value?.topSongs?.song || []) : []; const iTracks = iTracksRes.status === 'fulfilled' ? (iTracksRes.value || []).filter(t => t.wrapperType === 'track') : []; const iAlbums = iAlbumsRes.status === 'fulfilled' ? (iAlbumsRes.value || []) : []; if((topSongs.length || iTracks.length) && tsEl) { tsEl.innerHTML = ''; if(topSongs.length) { const h = document.createElement('div'); h.className = 'section-title'; h.style.marginBottom = '10px'; h.textContent = 'Top Songs'; tsEl.appendChild(h); const list = document.createElement('div'); list.style.marginBottom = '8px'; topSongs.forEach((s, i) => list.appendChild(_makeTopsongRow(s, i + 1, s.album || '', topSongs, i))); tsEl.appendChild(list); if(ndAlbums.length) { const total = ndAlbums.reduce((n, a) => n + (a.songCount || 0), 0); const btn = document.createElement('button'); btn.className = 'btn-outline'; btn.style.cssText = 'font-size:12px;padding:7px 14px;margin-bottom:16px;margin-top:4px'; btn.textContent = `Alle ${total} Songs in Library`; btn.onclick = () => navigate('artist-all-songs', { albums: ndAlbums, artist }); tsEl.appendChild(btn); } } if(iTracks.length) { const heading = topSongs.length ? 'Weltweit populär (iTunes)' : 'Populäre Titel'; const sec = document.createElement('div'); sec.innerHTML = `
${heading}
`; const list = document.createElement('div'); list.className = 'song-list'; list.style.marginBottom = '20px'; iTracks.forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, iTracks))); sec.appendChild(list); tsEl.appendChild(sec); } } // ── Action bar ── const actionBar = document.getElementById('artist-action-bar'); if(actionBar) { actionBar.innerHTML = ''; if(topSongs.length || ndAlbums.length) { const playTopBtn = document.createElement('button'); playTopBtn.className = 'artist-action-btn'; playTopBtn.innerHTML = ` Top Songs`; playTopBtn.onclick = () => topSongs.length ? playQueue(topSongs, 0) : quickPlayArtist(artist); const shuffleBtn = document.createElement('button'); shuffleBtn.className = 'artist-action-btn'; shuffleBtn.innerHTML = ` Shuffle`; shuffleBtn.onclick = async () => { S.shuffle = true; localStorage.setItem('ns_shuffle', '1'); import('../player/player.js').then(({ _applyShuffleRepeat }) => _applyShuffleRepeat()); await quickPlayArtist(artist); }; actionBar.appendChild(playTopBtn); actionBar.appendChild(shuffleBtn); } } if(albEl) { albEl.innerHTML = ''; _renderArtistAlbums(albEl, ndAlbums, iAlbums, artist); } } function _normAlbum(s) { return (s || '').toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim(); } function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) { const downloaded = [], partial = [], notDl = []; const usedNdIds = new Set(); for(const ia of iAlbums) { const iName = _normAlbum(ia.collectionName); const iCount = ia.trackCount || 0; const match = ndAlbums.find(a => { const n = _normAlbum(a.name); return n === iName || (n.length > 3 && iName.startsWith(n)) || (iName.length > 3 && n.startsWith(iName)); }); if(!match) { notDl.push(ia); } else if(iCount > 0 && match.songCount >= iCount) { downloaded.push({ nd: match, itunes: ia }); usedNdIds.add(match.id); } else { partial.push({ nd: match, itunes: ia, have: match.songCount || 0, total: iCount }); usedNdIds.add(match.id); } } for(const na of ndAlbums) { if(!usedNdIds.has(na.id)) downloaded.push({ nd: na, itunes: null }); } downloaded.sort((a, b) => (b.nd.year||0) - (a.nd.year||0)); partial.sort((a, b) => (b.nd.year||0) - (a.nd.year||0)); notDl.sort((a, b) => new Date(b.releaseDate||0) - new Date(a.releaseDate||0)); function addGrid(title, count) { const h = document.createElement('div'); h.className = 'section-title'; h.style.cssText = 'margin-bottom:12px;margin-top:20px;display:flex;align-items:center;gap:8px'; h.innerHTML = `${title}${count}`; el.appendChild(h); const g = document.createElement('div'); g.className = 'grid grid-4'; el.appendChild(g); return g; } if(downloaded.length) { const g = addGrid('In Library', downloaded.length); downloaded.forEach(({ nd }) => g.appendChild(makeAlbumCard({ ...nd, _fromArtist: artist }))); } if(partial.length) { const g = addGrid('Unvollständig', partial.length); partial.forEach(({ nd, itunes, have, total }) => { const art = itunes?.artworkUrl100?.replace('100x100','512x512') || coverUrl(nd.coverArt, 512); const c = makeCard(nd.name, `${have} / ${total} Tracks`, art, '💿'); c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(nd); }; c.querySelector('.card-info').onclick = () => itunes ? navigate('itunes-album-detail', itunes) : navigate('album-detail', { ...nd, _fromArtist: artist }); const artEl = c.querySelector('.card-art'); const badge = document.createElement('div'); badge.className = 'album-partial-badge'; badge.textContent = `${have}/${total}`; artEl.appendChild(badge); if(itunes) { const btn = document.createElement('button'); btn.className = 'card-dl-btn'; btn.title = 'Fehlende Tracks laden'; btn.innerHTML = ``; btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: artist.name, trackName: nd.name, collectionName: nd.name, artworkUrl100: itunes.artworkUrl100||'', isAlbum: true, itunesData: itunes, collectionId: itunes.collectionId }); }; artEl.appendChild(btn); } g.appendChild(c); }); } if(notDl.length) { const g = addGrid('Noch nicht heruntergeladen', notDl.length); notDl.forEach(ia => { const art = ia.artworkUrl100?.replace('100x100','300x300'); const c = makeCard(ia.collectionName, ia.releaseDate?.slice(0,4)||'', art, '💿'); c.onclick = () => navigate('itunes-album-detail', ia); const artEl = c.querySelector('.card-art'); const btn = document.createElement('button'); btn.className = 'card-dl-btn'; btn.title = 'Album laden'; btn.innerHTML = ``; btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: ia.artistName, trackName: ia.collectionName, collectionName: ia.collectionName, artworkUrl100: ia.artworkUrl100||'', isAlbum: true, itunesData: ia, collectionId: ia.collectionId }); }; artEl.appendChild(btn); g.appendChild(c); }); } if(!downloaded.length && !partial.length && !notDl.length && ndAlbums.length) { const g = addGrid('Alben', ndAlbums.length); ndAlbums.forEach(a => g.appendChild(makeAlbumCard({ ...a, _fromArtist: artist }))); } } export async function loadArtistAllSongs({ albums, artist }) { document.getElementById('artist-songs-title').textContent = `Alle Songs · ${artist.name}`; document.getElementById('artist-songs-back-btn').onclick = () => _navGoBack(); const el = document.getElementById('artist-songs-list'); el.innerHTML = '
'; const results = await Promise.allSettled(albums.map(a => api('getAlbum', `&id=${a.id}`))); el.innerHTML = ''; const allSongs = results .filter(r => r.status === 'fulfilled') .flatMap(r => { const alb = r.value?.album; return (alb?.song || []).map(s => ({ ...s, _alb: alb })); }) .sort((a, b) => { if(a._alb?.year !== b._alb?.year) return (b._alb?.year||0) - (a._alb?.year||0); if(a._alb?.id !== b._alb?.id) return (a._alb?.name||'').localeCompare(b._alb?.name||''); return (a.trackNumber||0) - (b.trackNumber||0); }); let curId = ''; allSongs.forEach((s, i) => { if(s._alb?.id !== curId) { curId = s._alb?.id || ''; const h = document.createElement('div'); h.style.cssText = 'font-size:12px;font-weight:700;color:var(--text2);padding:12px 0 4px;border-top:1px solid var(--border);margin-top:4px'; h.textContent = s._alb?.name || ''; el.appendChild(h); } el.appendChild(_makeTopsongRow(s, s.trackNumber || '', null, allSongs, i)); }); } export async function loadItunesAlbumDetail(album) { document.getElementById('itunes-album-back-btn').onclick = () => _navGoBack(); document.getElementById('itunes-album-detail-name').textContent = album.collectionName || '—'; document.getElementById('itunes-album-detail-artist').textContent = album.artistName || ''; const cw = document.getElementById('itunes-album-cover-wrap'); const art = (album.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600'); cw.innerHTML = art ? `` : '
💿
'; const dlBtn = document.getElementById('itunes-album-dl-btn'); dlBtn.onclick = () => openDlModal({ artistName: album.artistName, trackName: album.collectionName, collectionName: album.collectionName, artworkUrl100: album.artworkUrl100||'', isAlbum: true, itunesData: album, collectionId: album.collectionId }); const sl = document.getElementById('itunes-album-songs'); sl.innerHTML = '
'; try { const tracks = await itunesLookupAlbum(album.collectionId); const year = album.releaseDate ? new Date(album.releaseDate).getFullYear() : ''; document.getElementById('itunes-album-detail-stats').textContent = `${tracks.length} Titel${year ? ' · ' + year : ''}`; sl.innerHTML = ''; if(!tracks.length) { sl.innerHTML = '
Keine Tracks gefunden
'; return; } tracks.forEach((t, i) => sl.appendChild(makeGlobalTrackItem(t, i, tracks))); } catch(e) { sl.innerHTML = `

${e.message}

`; } } // ─── ALBUM DETAIL ─── 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); const artistEl = document.getElementById('album-detail-artist'); artistEl.innerHTML = parseArtists(album) .map(a => `${esc(dispArtistById(a.id, a.name))}`) .join(' • ') || ''; const cw = document.getElementById('album-cover-wrap'); const cv = coverUrl(album.coverArt, 512); cw.innerHTML = cv ? `` : '
💿
'; const sl = document.getElementById('album-songs'); sl.innerHTML = '
'; try { const r = await api('getAlbum', `&id=${album.id}`); const songs = r.album?.song || []; 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); artistEl.innerHTML = parseArtists(songs[0]) .map(a => `${esc(dispArtistById(a.id, a.name))}`) .join(' • ') || ''; } document.getElementById('btn-play-album').onclick = () => playQueue(songs, 0); document.getElementById('btn-dl-album').onclick = () => { songs.forEach((s,i) => setTimeout(() => dlSongLocal(s), i*400)); toast(`⬇ ${songs.length} Titel Download gestartet`); }; sl.innerHTML = ''; songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs))); } catch(e) { sl.innerHTML = `

${e.message}

`; } } // ─── HELPERS ─── export function expandArtistBio() { const el = document.getElementById('artist-bio-text'); if(el && window._artistBioFull) el.textContent = window._artistBioFull; }