${esc(short)}${bio.length>400
+
${esc(short)}${bio.length > 400
? ` `
: ''}
`;
}
}
+ // ── Similar artists ──
const similar = info?.similarArtist || [];
- if(similar.length) {
- const simEl = document.getElementById('artist-similar-section');
- if(simEl) {
- simEl.innerHTML = `
Ähnliche Künstler
-
${similar.map(a =>
- `
- ${esc(a.name)}
- `).join('')}
-
`;
- }
+ if(similar.length && simEl) {
+ simEl.innerHTML = `
Ähnliche Künstler
+
${similar.map(a =>
+ `
+ ${esc(a.name)}`).join('')}
`;
}
- const topSongs = topRes.status==='fulfilled' ? (topRes.value.topSongs?.song||[]) : [];
- if(topSongs.length) {
- const tsEl = document.getElementById('artist-topsongs-section');
- if(tsEl) {
- tsEl.innerHTML = `
Top Songs
`;
- const list = document.createElement('div');
- list.style.marginBottom = '20px';
+ // ── 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) => {
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 = `
${i+1}
${cv ? `

` : '
🎵
'}
${esc(s.title)}
${esc(s.album||'')}
@@ -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 = `
${heading}
`;
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);
- });
- wrap.appendChild(heading); wrap.appendChild(extraGrid);
- g.after(wrap);
+ // ── 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));
+ });
+ 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}
${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, 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 = `
`;
+ 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 = `
`;
+ 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 = `
${s.trackNumber||''}
+
${cv?`

`:'
🎵
'}
+
+
${s.playCount?s.playCount.toLocaleString():''}
`;
+ 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 = '
';
- 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 = `
-
-
-
-
${esc(artistName)}
-
`;
- 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 = `
${e.message}
`; }
+export function loadItunesArtistResults(artistId, artistName) {
+ navigate('artist-detail', { id: null, name: artistName, itunesArtistId: artistId });
}
// ─── NEW RELEASES ───
diff --git a/styles.css b/styles.css
index f055744..6f387a3 100644
--- a/styles.css
+++ b/styles.css
@@ -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)}