feat: unified artist page with full iTunes/Navidrome album categorization
- loadArtistDetail now works for both Navidrome artists and iTunes-only artists (id=null + itunesArtistId). loadItunesArtistResults from global search now navigates to the same artist-detail page. - Album sections: "In Library" (fully downloaded), "Unvollständig" (partial — e.g. 2/12 tracks), "Noch nicht heruntergeladen" (iTunes-only). Partial albums show X/Y badge and a direct download button on hover. - iTunes albums now fetched via lookup endpoint (up to 200 albums) instead of search (max 20), fixing the missing-albums problem. - "Alle X Songs in Library" expand button fetches all songs across albums and renders them grouped by album with play/context-menu support. - Back button uses _navGoBack() for context-aware navigation. - CSS: .section-count, .album-partial-badge, .card-dl-btn (hover reveal) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
5987e1f303
commit
72f6087043
4 files changed
+206
-133
No files matched your search
+1
-2
@@ -174,8 +174,7 @@
|
||||
<div id="artist-bio-section"></div>
|
||||
<div id="artist-similar-section"></div>
|
||||
<div id="artist-topsongs-section"></div>
|
||||
<div class="section-title" style="margin-bottom:12px;margin-top:8px">Alben</div>
|
||||
<div id="artist-albums-grid" class="grid grid-4"></div>
|
||||
<div id="artist-albums-section"></div>
|
||||
</div>
|
||||
|
||||
<!-- ALBUMS -->
|
||||
|
||||
@@ -61,6 +61,12 @@ export async function itunesLookupArtistSongs(artistId, limit=15) {
|
||||
return (d.results || []).filter(x => x.wrapperType === 'track');
|
||||
}
|
||||
|
||||
export async function itunesLookupArtistAlbums(artistId, limit=200) {
|
||||
const r = await fetch(`${ITUNES}/lookup?id=${artistId}&entity=album&limit=${limit}&country=us`);
|
||||
const d = await r.json();
|
||||
return (d.results || []).filter(x => x.wrapperType === 'collection' && x.collectionType === 'Album');
|
||||
}
|
||||
|
||||
export async function itunesTopCharts() {
|
||||
const urls = [
|
||||
'https://rss.applemarketingtools.com/api/v2/de/music/most-played/50/albums.json',
|
||||
|
||||
+195
-131
@@ -1,7 +1,7 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, formatDur, toast } from './utils.js';
|
||||
import { api, coverUrl } from './api.js';
|
||||
import { itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesTopCharts, itunesTopSongs } from './api.js';
|
||||
import { itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, itunesTopCharts, itunesTopSongs } from './api.js';
|
||||
// Circular imports — safe (only used inside function bodies)
|
||||
import { playQueue, dlSongLocal, toggleStar, playPreview } from './player.js';
|
||||
import { showCtxMenu, openEditPlModal, openPlModal, openDlModal, closeSidebar, switchNavMode, closeNowPlaying, closeNpMenu } from './ui.js';
|
||||
@@ -220,81 +220,91 @@ export async function loadAlbums() {
|
||||
// ─── ARTIST DETAIL ───
|
||||
export async function loadArtistDetail(artist) {
|
||||
document.getElementById('artist-detail-name').textContent = artist.name;
|
||||
document.getElementById('artist-back-btn').onclick = () => navigate('artists');
|
||||
document.getElementById('artist-back-btn').onclick = () => _navGoBack();
|
||||
|
||||
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))';
|
||||
['artist-bio-section','artist-similar-section','artist-topsongs-section'].forEach(id => {
|
||||
const el = document.getElementById(id); if(el) el.innerHTML = '';
|
||||
});
|
||||
const g = document.getElementById('artist-albums-grid');
|
||||
g.innerHTML = '<div class="spinner"></div>';
|
||||
document.getElementById('artist-itunes-extra')?.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 = '<div class="spinner"></div>';
|
||||
|
||||
const [imgRes, albumRes, infoRes, topRes, itunesArtistRes, itunesAlbumsRes] = await Promise.allSettled([
|
||||
getArtistImage(artist.id),
|
||||
api('getArtist', `&id=${artist.id}`),
|
||||
api('getArtistInfo2', `&id=${artist.id}&count=8`),
|
||||
api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`),
|
||||
itunesSearch(artist.name, 'musicArtist', 1),
|
||||
itunesSearch(artist.name, 'album', 20),
|
||||
const hasND = !!artist.id;
|
||||
const knownItunes = artist.itunesArtistId || null;
|
||||
|
||||
// Phase 1 — parallel
|
||||
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', 1),
|
||||
]);
|
||||
const itunesArtistId = itunesArtistRes.status==='fulfilled' ? (itunesArtistRes.value[0]?.artistId ?? null) : null;
|
||||
const itunesTracksRes = itunesArtistId
|
||||
? await Promise.allSettled([itunesLookupArtistSongs(itunesArtistId, 10)])[0]
|
||||
: { status: 'rejected' };
|
||||
|
||||
const img = imgRes.status==='fulfilled' ? imgRes.value : null;
|
||||
const itunesArtistId = knownItunes
|
||||
?? (iArtistRes.status === 'fulfilled' ? (iArtistRes.value[0]?.artistId ?? null) : null);
|
||||
|
||||
// Phase 2 — iTunes data (needs artist ID)
|
||||
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 = `url('${img}') center/cover no-repeat`;
|
||||
hero.querySelector('.artist-hero-overlay').style.background =
|
||||
'linear-gradient(to right,rgba(7,7,15,.9) 0%,rgba(7,7,15,.5) 60%,rgba(7,7,15,.2) 100%)';
|
||||
if(overlay) overlay.style.background = 'linear-gradient(to right,rgba(7,7,15,.9) 0%,rgba(7,7,15,.5) 60%,rgba(7,7,15,.2) 100%)';
|
||||
}
|
||||
|
||||
if(albumRes.status==='fulfilled') {
|
||||
g.innerHTML = '';
|
||||
(albumRes.value.artist?.album || []).forEach(a => g.appendChild(makeAlbumCard({...a,_fromArtist:artist})));
|
||||
} else {
|
||||
g.innerHTML = `<p style="color:var(--danger)">${albumRes.reason?.message}</p>`;
|
||||
}
|
||||
|
||||
const info = infoRes.status==='fulfilled' ? infoRes.value.artistInfo2 : null;
|
||||
if(info?.biography) {
|
||||
const bioEl = document.getElementById('artist-bio-section');
|
||||
// ── Bio ──
|
||||
const info = infoRes.status === 'fulfilled' ? infoRes.value?.artistInfo2 : null;
|
||||
if(info?.biography && bioEl) {
|
||||
const bio = info.biography.replace(/<a [^>]+>/g,'').replace(/<\/a>/g,'').replace(/<[^>]+>/g,'').trim();
|
||||
const short = bio.length > 400 ? bio.slice(0,400)+'…' : bio;
|
||||
if(short && bioEl) {
|
||||
if(short) {
|
||||
window._artistBioFull = bio;
|
||||
bioEl.innerHTML = `<div class="section-title" style="margin-bottom:10px">Über</div>
|
||||
<div class="artist-bio" id="artist-bio-text">${esc(short)}${bio.length>400
|
||||
<div class="artist-bio" id="artist-bio-text">${esc(short)}${bio.length > 400
|
||||
? ` <button onclick="expandArtistBio()" style="background:none;border:none;cursor:pointer;color:var(--accent);font-size:12px;padding:0;margin-left:6px">Mehr lesen</button>`
|
||||
: ''}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Similar artists ──
|
||||
const similar = info?.similarArtist || [];
|
||||
if(similar.length) {
|
||||
const simEl = document.getElementById('artist-similar-section');
|
||||
if(simEl) {
|
||||
if(similar.length && simEl) {
|
||||
simEl.innerHTML = `<div class="section-title" style="margin-bottom:10px">Ähnliche Künstler</div>
|
||||
<div class="similar-chips">${similar.map(a =>
|
||||
`<span class="similar-chip" onclick="navigate('artist-detail',{id:'${esc(a.id||a.name)}',name:'${esc(a.name)}'})">
|
||||
${esc(a.name)}
|
||||
</span>`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
${esc(a.name)}</span>`).join('')}</div>`;
|
||||
}
|
||||
|
||||
const topSongs = topRes.status==='fulfilled' ? (topRes.value.topSongs?.song||[]) : [];
|
||||
// ── 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 tsEl = document.getElementById('artist-topsongs-section');
|
||||
if(tsEl) {
|
||||
tsEl.innerHTML = `<div class="section-title" style="margin-bottom:10px">Top Songs</div>`;
|
||||
const list = document.createElement('div');
|
||||
list.style.marginBottom = '20px';
|
||||
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) => {
|
||||
const cv = coverUrl(s.coverArt, 60);
|
||||
const row = document.createElement('div');
|
||||
row.className = 'topsong-item';
|
||||
const row = document.createElement('div'); row.className = 'topsong-item';
|
||||
row.innerHTML = `<div class="topsong-num">${i+1}</div>
|
||||
<div class="topsong-thumb">${cv ? `<img src="${cv}" loading="lazy">` : '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:18px">🎵</div>'}</div>
|
||||
<div><div class="topsong-title">${esc(s.title)}</div><div style="font-size:11px;color:var(--text2)">${esc(s.album||'')}</div></div>
|
||||
@@ -304,52 +314,153 @@ export async function loadArtistDetail(artist) {
|
||||
list.appendChild(row);
|
||||
});
|
||||
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 = () => _expandArtistAllSongs(btn, ndAlbums, artist);
|
||||
tsEl.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// ── iTunes: global top tracks ──
|
||||
const itunesTracks = itunesTracksRes.status==='fulfilled'
|
||||
? (itunesTracksRes.value||[]).filter(t => t.wrapperType==='track' && t.artistName?.toLowerCase()===artist.name.toLowerCase())
|
||||
: [];
|
||||
if(itunesTracks.length) {
|
||||
const tsEl = document.getElementById('artist-topsongs-section');
|
||||
if(tsEl) {
|
||||
if(iTracks.length) {
|
||||
const heading = topSongs.length ? 'Weltweit populär (iTunes)' : 'Populäre Titel';
|
||||
const sec = document.createElement('div');
|
||||
sec.innerHTML = `<div class="section-title" style="margin-top:${topSongs.length?'8px':'0'};margin-bottom:10px">${heading}</div>`;
|
||||
const list = document.createElement('div'); list.className = 'song-list'; list.style.marginBottom = '20px';
|
||||
itunesTracks.forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, itunesTracks)));
|
||||
sec.appendChild(list);
|
||||
tsEl.appendChild(sec);
|
||||
iTracks.forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, iTracks)));
|
||||
sec.appendChild(list); tsEl.appendChild(sec);
|
||||
}
|
||||
}
|
||||
|
||||
// ── iTunes: more albums ──
|
||||
const itunesAlbums = itunesAlbumsRes.status==='fulfilled' ? (itunesAlbumsRes.value||[]) : [];
|
||||
const serverAlbumNames = new Set((albumRes.status==='fulfilled' ? (albumRes.value.artist?.album||[]) : []).map(a => a.name.toLowerCase()));
|
||||
const extraAlbums = itunesAlbums.filter(a =>
|
||||
(a.artistId === undefined || a.artistName?.toLowerCase() === artist.name.toLowerCase()) &&
|
||||
!serverAlbumNames.has(a.collectionName?.toLowerCase())
|
||||
);
|
||||
if(extraAlbums.length) {
|
||||
const g = document.getElementById('artist-albums-grid');
|
||||
if(g) {
|
||||
const wrap = document.createElement('div'); wrap.id = 'artist-itunes-extra';
|
||||
const heading = document.createElement('div');
|
||||
heading.className = 'section-title';
|
||||
heading.style.cssText = 'margin-top:24px;margin-bottom:12px';
|
||||
heading.textContent = 'Weitere Alben (iTunes)';
|
||||
const extraGrid = document.createElement('div'); extraGrid.className = 'grid grid-4';
|
||||
extraAlbums.slice(0,16).forEach(a => {
|
||||
const art = a.artworkUrl100?.replace('100x100','300x300');
|
||||
const c = makeCard(a.collectionName, String(a.releaseDate?.slice(0,4)||''), art, '💿');
|
||||
c.onclick = () => loadItunesAlbum(a);
|
||||
extraGrid.appendChild(c);
|
||||
// ── Albums ──
|
||||
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));
|
||||
});
|
||||
wrap.appendChild(heading); wrap.appendChild(extraGrid);
|
||||
g.after(wrap);
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Navidrome albums with no iTunes match go to downloaded
|
||||
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}<span class="section-count">${count}</span>`;
|
||||
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, itunes }) => 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','300x300') || coverUrl(nd.coverArt, 300);
|
||||
const c = makeCard(nd.name, `${have} / ${total} Tracks`, art, '💿');
|
||||
c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(nd); };
|
||||
c.onclick = () => 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 = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
||||
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 = () => loadItunesAlbum(ia);
|
||||
const artEl = c.querySelector('.card-art');
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'card-dl-btn'; btn.title = 'Album laden';
|
||||
btn.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// No iTunes data at all — show Navidrome albums plainly
|
||||
if(!downloaded.length && !partial.length && !notDl.length && ndAlbums.length) {
|
||||
const g = addGrid('Alben', ndAlbums.length);
|
||||
ndAlbums.forEach(a => g.appendChild(makeAlbumCard({ ...a, _fromArtist: artist })));
|
||||
}
|
||||
}
|
||||
|
||||
async function _expandArtistAllSongs(btn, albums, artist) {
|
||||
btn.disabled = true; btn.textContent = 'Lädt…';
|
||||
const results = await Promise.allSettled(albums.map(a => api('getAlbum', `&id=${a.id}`)));
|
||||
const container = document.createElement('div'); container.style.marginBottom = '16px';
|
||||
btn.replaceWith(container);
|
||||
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:8px 0 2px;border-top:1px solid var(--border);margin-top:4px';
|
||||
h.textContent = s._alb?.name || ''; container.appendChild(h);
|
||||
}
|
||||
const cv = coverUrl(s.coverArt, 60);
|
||||
const row = document.createElement('div'); row.className = 'topsong-item';
|
||||
row.innerHTML = `<div class="topsong-num">${s.trackNumber||''}</div>
|
||||
<div class="topsong-thumb">${cv?`<img src="${cv}" loading="lazy">`:'<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:16px">🎵</div>'}</div>
|
||||
<div><div class="topsong-title">${esc(s.title)}</div></div>
|
||||
<div class="topsong-plays">${s.playCount?s.playCount.toLocaleString():''}</div>`;
|
||||
row.onclick = () => playQueue(allSongs, i);
|
||||
row.addEventListener('contextmenu', e => { e.preventDefault(); showCtxMenu(e, s, allSongs, i); });
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── ALBUM DETAIL ───
|
||||
@@ -623,55 +734,8 @@ export async function loadItunesAlbum(album) {
|
||||
} catch(e) { toast('Fehler: ' + e.message); }
|
||||
}
|
||||
|
||||
export async function loadItunesArtistResults(artistId, artistName) {
|
||||
const el = document.getElementById('global-search-results');
|
||||
el.innerHTML = '<div class="spinner"></div>';
|
||||
try {
|
||||
const [albums, topTracks] = await Promise.all([
|
||||
itunesSearch(artistName, 'album', 20),
|
||||
itunesLookupArtistSongs(artistId, 15),
|
||||
]);
|
||||
const artistAlbums = albums.filter(a => a.artistId===artistId || a.artistName===artistName);
|
||||
const artistTracks = topTracks.filter(t => t.wrapperType==='track' && (t.artistId===artistId || t.artistName===artistName));
|
||||
const coverImg = artistAlbums[0]?.artworkUrl100?.replace('100x100','600x600') || '';
|
||||
el.innerHTML = '';
|
||||
|
||||
// Hero header
|
||||
const hero = document.createElement('div');
|
||||
hero.style.cssText = 'position:relative;border-radius:16px;overflow:hidden;margin-bottom:20px;min-height:120px;background:var(--surface2)';
|
||||
if(coverImg) hero.style.background = `url('${coverImg}') center/cover no-repeat`;
|
||||
hero.innerHTML = `
|
||||
<div style="position:absolute;inset:0;background:linear-gradient(to right,rgba(7,7,15,.92) 0%,rgba(7,7,15,.5) 70%,transparent 100%)"></div>
|
||||
<div style="position:relative;padding:24px 20px;display:flex;align-items:center;gap:12px">
|
||||
<button class="back-btn" style="margin:0;flex-shrink:0" onclick="onGlobalSearch(document.getElementById('global-search-input').value)">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Zurück
|
||||
</button>
|
||||
<div style="font-size:22px;font-weight:800;text-shadow:0 2px 8px rgba(0,0,0,.6)">${esc(artistName)}</div>
|
||||
</div>`;
|
||||
el.appendChild(hero);
|
||||
|
||||
// Top songs
|
||||
if(artistTracks.length) {
|
||||
const s = mkSection('Populäre Titel');
|
||||
const list = document.createElement('div'); list.className = 'song-list';
|
||||
artistTracks.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, artistTracks)));
|
||||
s.appendChild(list); el.appendChild(s);
|
||||
}
|
||||
|
||||
// Albums
|
||||
const finalAlbums = artistAlbums.length ? artistAlbums : albums;
|
||||
if(finalAlbums.length) {
|
||||
const s = mkSection('Alben');
|
||||
const grid = document.createElement('div'); grid.className = 'grid grid-4';
|
||||
finalAlbums.forEach(a => {
|
||||
const art = a.artworkUrl100?.replace('100x100','300x300');
|
||||
const c = makeCard(a.collectionName, a.artistName, art, '💿');
|
||||
c.onclick = () => loadItunesAlbum(a);
|
||||
grid.appendChild(c);
|
||||
});
|
||||
s.appendChild(grid); el.appendChild(s);
|
||||
}
|
||||
} catch(e) { el.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
||||
export function loadItunesArtistResults(artistId, artistName) {
|
||||
navigate('artist-detail', { id: null, name: artistName, itunesArtistId: artistId });
|
||||
}
|
||||
|
||||
// ─── NEW RELEASES ───
|
||||
|
||||
@@ -622,6 +622,10 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
|
||||
.lyrics-line.past{color:rgba(255,255,255,.45)}
|
||||
.lyrics-line.active{color:#fff;font-size:21px;font-weight:600}
|
||||
.lyrics-plain{font-size:15px;line-height:1.9;color:rgba(255,255,255,.65);white-space:pre-wrap;text-align:center}
|
||||
.section-count{font-size:11px;font-weight:500;color:var(--text3);background:var(--surface2);padding:2px 8px;border-radius:10px;flex-shrink:0}
|
||||
.album-partial-badge{position:absolute;bottom:5px;left:5px;background:rgba(0,0,0,.72);color:#fff;font-size:9px;font-weight:700;padding:2px 6px;border-radius:4px;pointer-events:none;z-index:2}
|
||||
.card-dl-btn{position:absolute;top:5px;right:5px;z-index:3;width:26px;height:26px;border-radius:50%;background:rgba(0,0,0,.65);border:none;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0;transition:opacity .2s;backdrop-filter:blur(6px)}
|
||||
.card:hover .card-dl-btn,.card-dl-btn:focus{opacity:1}
|
||||
.np-queue-item{display:flex;align-items:center;gap:10px;padding:9px 6px;border-radius:10px;cursor:pointer;transition:background .15s}
|
||||
.np-queue-item:hover{background:rgba(255,255,255,.06)}
|
||||
.np-queue-item.nq-active .nqi-title{color:var(--accent)}
|
||||
|
||||
Reference in new issue
Block a user