diff --git a/src/views/detail.js b/src/views/detail.js
new file mode 100644
index 0000000..986ed02
--- /dev/null
+++ b/src/views/detail.js
@@ -0,0 +1,375 @@
+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 = `
]+>/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;
+}
diff --git a/src/views/discover.js b/src/views/discover.js
new file mode 100644
index 0000000..aecb9c8
--- /dev/null
+++ b/src/views/discover.js
@@ -0,0 +1,138 @@
+import { S } from '../state.js';
+import { esc } from '../utils.js';
+import { itunesSearch, itunesTopCharts, itunesTopSongs } from '../api.js';
+import { makeCard, makeGlobalTrackItem } from '../ui/components.js';
+import { playPreview } from '../player/player.js';
+import { openDlModal } from '../ui/ui.js';
+// Circular — safe (called inside event handlers)
+import { loadItunesAlbum, searchAndPreview } from './search.js';
+
+// ─── NEW RELEASES ───
+export async function loadNewReleases() {
+ const albumEl = document.getElementById('nr-albums');
+ const songEl = document.getElementById('nr-songs');
+ albumEl.innerHTML = ''; songEl.innerHTML = '';
+ try {
+ const [albums, songs] = await Promise.all([itunesTopCharts(), itunesTopSongs()]);
+ albumEl.innerHTML = '';
+ albums.slice(0,30).forEach(a => {
+ const art = a.artworkUrl100?.replace('100x100','300x300') || a.artworkUrl;
+ const c = makeCard(a.name, a.artistName, art, '💿');
+ c.onclick = () => itunesSearch(a.name+' '+a.artistName,'album',5).then(res => { if(res[0]) loadItunesAlbum(res[0]); });
+ albumEl.appendChild(c);
+ });
+ songs.slice(0,40).forEach((s, i) => {
+ const el = document.createElement('div'); el.className = 'gtrack-item';
+ const art = s.artworkUrl?.replace('100x100','60x60');
+ el.innerHTML = `
+ ${art ? `

` : `
🎵
`}
+
+
${esc(s.name)}
+
${esc(s.artistName||'')}
+
+ `;
+ el.onclick = () => { itunesSearch(s.name+' '+s.artistName,'musicTrack',1).then(res => { if(res[0]?.previewUrl) playPreview(res[0]); }); };
+ songEl.appendChild(el);
+ });
+ } catch(e) { albumEl.innerHTML = `${e.message}
`; }
+}
+
+// ─── RECOMMENDATIONS ───
+export async function loadRecommendations() {
+ const el = document.getElementById('reco-content');
+ const currentSong = S.queue[S.qIdx];
+ if(!currentSong) {
+ el.innerHTML = '🎵
Spiele zuerst Musik in deiner Bibliothek
';
+ return;
+ }
+ el.innerHTML = '';
+ try {
+ const artist = currentSong.artist || '';
+ const title = currentSong.title || '';
+ let html = `
+
🎧
+
Basierend auf
${esc(title)}
${esc(artist)}
+
`;
+ el.innerHTML = html + '';
+ if(S.cfg.lastfmKey) {
+ await _loadLastfmRecommendations(el, artist, title, html);
+ } else {
+ await _loadItunesFallbackRecommendations(el, artist, title, html);
+ const note = document.createElement('div');
+ note.style.cssText = 'margin-top:16px;padding:12px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;font-size:12px;color:var(--text3)';
+ note.innerHTML = '💡 Tipp: Füge einen Last.fm API Key in den Einstellungen hinzu für bessere Empfehlungen & Similar Artists.';
+ el.appendChild(note);
+ }
+ } catch(e) { el.innerHTML = `${e.message}
`; }
+}
+
+async function _loadLastfmRecommendations(el, artist, title, html) {
+ const BASE = 'https://ws.audioscrobbler.com/2.0';
+ const key = S.cfg.lastfmKey;
+ try {
+ const [simArtists, topTracks, simTracks] = await Promise.all([
+ fetch(`${BASE}/?method=artist.getSimilar&artist=${encodeURIComponent(artist)}&api_key=${key}&format=json&limit=12`).then(r=>r.json()),
+ fetch(`${BASE}/?method=artist.getTopTracks&artist=${encodeURIComponent(artist)}&api_key=${key}&format=json&limit=15`).then(r=>r.json()),
+ fetch(`${BASE}/?method=track.getSimilar&artist=${encodeURIComponent(artist)}&track=${encodeURIComponent(title)}&api_key=${key}&format=json&limit=15`).then(r=>r.json()),
+ ]);
+ el.innerHTML = html;
+ const simArtistList = simArtists.similarartists?.artist || [];
+ const topTrackList = topTracks.toptracks?.track || [];
+ const simTrackList = simTracks.similartracks?.track || [];
+ if(simArtistList.length) {
+ el.innerHTML += `Ähnliche Künstler
`;
+ el.innerHTML += simArtistList.map(a => ``).join('');
+ el.innerHTML += '
';
+ }
+ if(simTrackList.length) {
+ const sec = document.createElement('div');
+ sec.innerHTML = `Ähnliche Tracks
`;
+ const list = document.createElement('div'); list.className = 'song-list';
+ simTrackList.slice(0,15).forEach(t => {
+ const row = document.createElement('div'); row.className = 'gtrack-item';
+ row.innerHTML = `
+ ${esc(t.name)}
${esc(t.artist?.name||'')}
+ `;
+ row.onclick = () => searchAndPreview(t.name, t.artist?.name||'');
+ list.appendChild(row);
+ });
+ sec.appendChild(list); el.appendChild(sec);
+ }
+ if(topTrackList.length) {
+ const sec = document.createElement('div'); sec.style.marginTop = '20px';
+ sec.innerHTML = `Top Tracks von ${esc(artist)}
`;
+ const list = document.createElement('div'); list.className = 'song-list';
+ topTrackList.slice(0,10).forEach(t => {
+ const row = document.createElement('div'); row.className = 'gtrack-item';
+ row.innerHTML = `
+ ${esc(t.name)}
${esc(t.artist?.name||artist)}
+ `;
+ row.onclick = () => searchAndPreview(t.name, artist);
+ list.appendChild(row);
+ });
+ sec.appendChild(list); el.appendChild(sec);
+ }
+ } catch { await _loadItunesFallbackRecommendations(el, artist, title, html); }
+}
+
+async function _loadItunesFallbackRecommendations(el, artist, title, html) {
+ const results = await itunesSearch(artist, 'musicTrack', 20);
+ el.innerHTML = html;
+ const sec = document.createElement('div');
+ sec.innerHTML = `Mehr von ${esc(artist)}
`;
+ const list = document.createElement('div'); list.className = 'song-list';
+ results.filter(t => t.artistName===artist).slice(0,15).forEach(t => list.appendChild(makeGlobalTrackItem(t, 0, [t])));
+ if(!list.children.length) results.slice(0,15).forEach(t => list.appendChild(makeGlobalTrackItem(t, 0, [t])));
+ sec.appendChild(list); el.appendChild(sec);
+}
diff --git a/src/views/home.js b/src/views/home.js
new file mode 100644
index 0000000..6f15b38
--- /dev/null
+++ b/src/views/home.js
@@ -0,0 +1,241 @@
+import { S } from '../state.js';
+import { esc } from '../utils.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)
+import { navigate, _navGoBack } from './navigation.js';
+
+// ─── ARTIST IMAGE ───
+export async function getArtistImage(artistId) {
+ if(S.artistImgCache[artistId] !== undefined) return S.artistImgCache[artistId];
+ try {
+ const r = await api('getArtistInfo2', `&id=${artistId}&count=0`);
+ const info = r.artistInfo2 || {};
+ const img = info.largeImageUrl || info.mediumImageUrl || info.smallImageUrl || null;
+ const isBlank = img && (img.includes('2a96cbd8b46e442fc41c2b86b821562f') || img.endsWith('/noimage/'));
+ S.artistImgCache[artistId] = isBlank ? null : img;
+ return S.artistImgCache[artistId];
+ } catch { S.artistImgCache[artistId] = null; return null; }
+}
+
+// ─── HOME ───
+export async function loadHomeData() {
+ const hour = new Date().getHours();
+ const greeting = hour < 12 ? 'Guten Morgen' : hour < 18 ? 'Guten Tag' : 'Guten Abend';
+ const name = S.cfg.user ? `, ${S.cfg.user}` : '';
+ const el = document.getElementById('home-greeting');
+ if (el) el.textContent = `${greeting}${name} 👋`;
+
+ loadHeroCard();
+ loadGenreSection();
+ loadAlbumRow('home-newest', 'getAlbumList2', '&type=newest&size=20');
+ loadAlbumRow('home-recent', 'getAlbumList2', '&type=recent&size=20');
+ loadAlbumRow('home-random', 'getAlbumList2', '&type=random&size=20');
+ loadAlbumRow('home-frequent', 'getAlbumList2', '&type=frequent&size=20');
+}
+
+async function loadHeroCard() {
+ const el = document.getElementById('home-hero');
+ if (!el) return;
+ try {
+ const [freqRes, recRes] = await Promise.allSettled([
+ api('getAlbumList2', '&type=frequent&size=5'),
+ api('getAlbumList2', '&type=recent&size=5'),
+ ]);
+ const freqList = freqRes.status === 'fulfilled' ? (freqRes.value.albumList2?.album || []) : [];
+ const recList = recRes.status === 'fulfilled' ? (recRes.value.albumList2?.album || []) : [];
+ const freqIds = new Set(freqList.map(a => a.id));
+ const seen = new Set();
+ const pool = [...freqList, ...recList].filter(a => a.id && !seen.has(a.id) && seen.add(a.id));
+ const album = pool.length ? pool[Math.floor(Math.random() * pool.length)] : null;
+ if (!album) return;
+ const heroLabel = freqIds.has(album.id) ? 'Meistgespielt' : 'Zuletzt gehört';
+ const bgUrl = coverUrl(album.coverArt, 1024);
+ const thumbUrl = coverUrl(album.coverArt, 512);
+ el.innerHTML = `
+ ${bgUrl ? `
` : ''}
+
+ ${thumbUrl ? `

` : ''}
+
+
${heroLabel}
+
${esc(album.name)}
+
${esc(album.artist || '')}
+
+
+
`;
+ el.querySelector('.home-hero-cover')?.addEventListener('click', () => navigate('album-detail', album));
+ el.querySelector('.home-hero-artist').onclick = () => navigate('album-detail', album);
+ el.querySelector('.home-hero-play').onclick = () => quickPlayAlbum(album);
+ el.classList.add('loaded');
+ } catch {}
+}
+
+async function quickPlayAlbum(a) {
+ try { const r = await api('getAlbum', `&id=${a.id}`); playQueue(r.album?.song || [], 0); }
+ catch {}
+}
+
+const _GENRE_COLORS = ['#e8445a','#3b82f6','#f97316','#22c55e','#a855f7','#06b6d4','#eab308','#ec4899','#14b8a6','#f43f5e','#8b5cf6','#84cc16'];
+function _genreColor(name) {
+ let h = 0;
+ for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
+ return _GENRE_COLORS[h % _GENRE_COLORS.length];
+}
+
+let _allGenres = [];
+let _genresExpanded = false;
+const GENRE_LIMIT = () => window.innerWidth >= 700 ? 24 : 6;
+
+async function loadGenreSection() {
+ const el = document.getElementById('home-genres');
+ const more = document.getElementById('home-genres-more');
+ if (!el) return;
+ try {
+ const r = await api('getGenres');
+ _allGenres = (r.genres?.genre || [])
+ .filter(g => (g.songCount || 0) > 0)
+ .sort((a, b) => (b.songCount || 0) - (a.songCount || 0));
+ _renderGenres();
+ if (_allGenres.length > GENRE_LIMIT() && more) more.style.display = '';
+ } catch { el.style.display = 'none'; }
+ if (S.cfg.backendUrl && _allGenres.length) {
+ fetch(`${S.cfg.backendUrl}/api/genre-playcounts?ndUrl=${encodeURIComponent(S.cfg.url)}&user=${encodeURIComponent(S.cfg.user)}&pass=${encodeURIComponent(S.cfg.pass)}`)
+ .then(r => r.ok ? r.json() : null)
+ .then(plays => {
+ if (!plays || !_allGenres.length) return;
+ _allGenres.sort((a, b) => (plays[b.value] || 0) - (plays[a.value] || 0));
+ _renderGenres();
+ })
+ .catch(() => {});
+ }
+}
+
+function _renderGenres() {
+ const el = document.getElementById('home-genres');
+ if (!el) return;
+ const list = _genresExpanded ? _allGenres : _allGenres.slice(0, GENRE_LIMIT());
+ el.innerHTML = '';
+ list.forEach(g => {
+ const card = document.createElement('div');
+ card.className = 'genre-card';
+ card.title = `${g.songCount} Titel`;
+ const color = _genreColor(g.value);
+ card.innerHTML = `
+
+ ${esc(g.value)}
+ `;
+ card.querySelector('.genre-card-play').onclick = (e) => { e.stopPropagation(); playGenre(g.value); };
+ card.onclick = () => navigate('genre-detail', g);
+ el.appendChild(card);
+ });
+}
+
+export function toggleAllGenres() {
+ _genresExpanded = !_genresExpanded;
+ const more = document.getElementById('home-genres-more');
+ if (more) more.textContent = _genresExpanded ? 'Weniger' : 'Alle anzeigen';
+ _renderGenres();
+}
+
+async function playGenre(genre) {
+ try {
+ const r = await api('getSongsByGenre', `&genre=${encodeURIComponent(genre)}&count=200`);
+ const songs = r.songsByGenre?.song || [];
+ if (!songs.length) return;
+ playQueue([...songs].sort(() => Math.random() - 0.5), 0);
+ } catch {}
+}
+
+export async function loadGenreDetail(g) {
+ const genre = g.value || g;
+ const color = _genreColor(genre);
+ document.getElementById('genre-detail-name').textContent = genre;
+ document.getElementById('genre-detail-stats').textContent = g.songCount ? `${g.songCount} Titel` : '';
+ const coverWrap = document.getElementById('genre-cover-wrap');
+ coverWrap.style.background = `linear-gradient(135deg, ${color}33, ${color}11)`;
+ coverWrap.style.borderRadius = '10px';
+ coverWrap.style.border = `2px solid ${color}55`;
+ document.getElementById('genre-back-btn').onclick = () => _navGoBack();
+
+ const list = document.getElementById('genre-songs');
+ list.innerHTML = '';
+
+ let songs = [];
+ try {
+ const r = await api('getSongsByGenre', `&genre=${encodeURIComponent(genre)}&count=500`);
+ songs = r.songsByGenre?.song || [];
+ } catch { list.innerHTML = 'Fehler beim Laden
'; return; }
+
+ document.getElementById('genre-detail-stats').textContent = `${songs.length} Titel`;
+ document.getElementById('btn-play-genre').onclick = () => {
+ playQueue([...songs].sort(() => Math.random() - 0.5), 0);
+ };
+
+ list.innerHTML = '';
+ songs.forEach((s, i) => list.appendChild(makeSongItem(s, i, songs)));
+}
+
+export async function loadAlbumRow(id, method, extra) {
+ const c = document.getElementById(id);
+ if(!c) return;
+ if (!apiIsCached(method, extra)) c.innerHTML = '';
+ try {
+ const [r] = await Promise.all([
+ apiCached(method, extra),
+ S.cfg.showRomaji ? fetchRomajiIndex().catch(() => {}) : Promise.resolve(),
+ ]);
+ c.innerHTML = '';
+ (r.albumList2?.album || []).forEach(a => c.appendChild(makeAlbumCard(a)));
+ } catch(e) { c.innerHTML = `${e.message}
`; }
+}
+
+// ─── ARTISTS ───
+export async function loadArtists() {
+ const g = document.getElementById('artists-grid');
+ if (!apiIsCached('getArtists')) g.innerHTML = '';
+ try {
+ const r = await apiCached('getArtists');
+ const all = (r.artists?.index || []).flatMap(i => i.artist || []);
+ if (S.cfg.showRomaji) await fetchRomajiIndex().catch(() => {});
+ g.innerHTML = '';
+ all.forEach(a => {
+ const c = makeCard(dispArtistById(a.id, a.name), a.albumCount+' Alben', null, '🎤');
+ c.classList.add('artist-card');
+ c.onclick = () => navigate('artist-detail', a);
+ g.appendChild(c);
+ getArtistImage(a.id).then(imgUrl => {
+ if(!imgUrl) return;
+ const artEl = c.querySelector('.card-art');
+ if(!artEl) return;
+ const overlay = artEl.querySelector('.play-overlay');
+ const img = document.createElement('img');
+ img.src = imgUrl; img.loading = 'lazy'; img.style.cssText = 'width:100%;height:100%;object-fit:cover;transition:transform .4s';
+ artEl.innerHTML = ''; artEl.appendChild(img); if(overlay) artEl.appendChild(overlay);
+ c.addEventListener('mouseenter', () => img.style.transform = 'scale(1.05)');
+ c.addEventListener('mouseleave', () => img.style.transform = 'scale(1)');
+ });
+ });
+ } catch(e) { g.innerHTML = `${e.message}
`; }
+}
+
+// ─── ALBUMS ───
+let _albumSort = 'alphabeticalByName';
+
+export async function loadAlbums(type = _albumSort) {
+ _albumSort = type;
+ document.querySelectorAll('#albums-sort-chips .sort-chip').forEach(c =>
+ c.classList.toggle('active', c.dataset.sort === type));
+ const g = document.getElementById('albums-grid');
+ const extra = `&type=${type}&size=500`;
+ if (!apiIsCached('getAlbumList2', extra)) g.innerHTML = '';
+ try {
+ const r = await apiCached('getAlbumList2', extra);
+ g.innerHTML = '';
+ (r.albumList2?.album || []).forEach(a => g.appendChild(makeAlbumCard(a)));
+ } catch(e) { g.innerHTML = `${e.message}
`; }
+}
diff --git a/src/views/navigation.js b/src/views/navigation.js
new file mode 100644
index 0000000..79773a8
--- /dev/null
+++ b/src/views/navigation.js
@@ -0,0 +1,100 @@
+import { S } from '../state.js';
+import { closeSidebar, switchNavMode } from '../ui/ui.js';
+import { closeNowPlaying, closeNpMenu } from '../player/nowplaying.js';
+import { loadSpotifyImporter } from './spotify.js';
+import { loadSoundCloudImporter } from './soundcloud.js';
+// Circular — safe (all calls happen inside function bodies)
+import { loadHomeData, loadAlbumRow, loadArtists, loadAlbums, loadGenreDetail } from './home.js';
+import { loadArtistDetail, loadArtistAllSongs, loadAlbumDetail, loadItunesAlbumDetail } from './detail.js';
+import { loadPlaylists, loadPlaylistDetail, loadStarred, loadRandom } from './playlists.js';
+import { loadNewReleases, loadRecommendations } from './discover.js';
+
+// ─── NAVIGATION HISTORY ───
+export function _navSave() {
+ try { sessionStorage.setItem('ns_nav', JSON.stringify({stack: S._navStack, cursor: S._navCursor})); } catch {}
+}
+
+export function _navLoad() {
+ try {
+ const s = JSON.parse(sessionStorage.getItem('ns_nav') || 'null');
+ if(s && Array.isArray(s.stack) && s.stack.length) {
+ S._navStack = s.stack;
+ S._navCursor = s.cursor;
+ return true;
+ }
+ } catch {}
+ return false;
+}
+
+export function closeHeaderSearch() {
+ const overlay = document.getElementById('header-search-overlay');
+ const input = document.getElementById('header-search-input');
+ const clearBtn = document.getElementById('header-search-clear');
+ if (overlay) overlay.style.display = 'none';
+ if (input) input.value = '';
+ if (clearBtn) clearBtn.style.display = 'none';
+ clearTimeout(S._headerSearchTimer);
+}
+
+export function navigate(page, data=null) {
+ closeHeaderSearch();
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
+ document.querySelectorAll('.nav-item,.bnav-item').forEach(n => n.classList.remove('active','active-blue'));
+ const cls = ['discover','new-releases','recommendations','spotify-importer'].includes(page) ? 'active-blue' : 'active';
+ document.querySelectorAll(`[data-page="${page}"]`).forEach(el => el.classList.add(cls));
+ const pageEl = document.getElementById(`page-${page}`);
+ if(pageEl) { pageEl.classList.add('active'); document.getElementById('main-content').scrollTop = 0; }
+ closeSidebar();
+ if(['discover','new-releases','recommendations','random','spotify-importer'].includes(page)) switchNavMode('discover');
+ else if(['home','artists','albums','starred','playlists'].includes(page)) switchNavMode('library');
+
+ if(page==='home') loadHomeData();
+ if(page==='artists' && !document.getElementById('artists-grid').children.length) loadArtists();
+ if(page==='albums' && !document.getElementById('albums-grid').children.length) loadAlbums();
+ if(page==='starred') loadStarred();
+ if(page==='random') loadRandom();
+ if(page==='artist-detail' && data) loadArtistDetail(data);
+ if(page==='artist-all-songs' && data) loadArtistAllSongs(data);
+ if(page==='album-detail' && data) loadAlbumDetail(data);
+ if(page==='genre-detail' && data) loadGenreDetail(data);
+ if(page==='itunes-album-detail'&& data) loadItunesAlbumDetail(data);
+ if(page==='new-releases') loadNewReleases();
+ if(page==='playlists') loadPlaylists();
+ if(page==='playlist-detail' && data) loadPlaylistDetail(data);
+ if(page==='recommendations') loadRecommendations();
+ if(page==='spotify-importer') { loadSpotifyImporter(); loadSoundCloudImporter(); }
+ if(page==='audiobooks') {
+ import('./abs.js').then(m => {
+ if (S.abs.currentLibraryId) m.loadAbsBooks(S.abs.currentLibraryId);
+ });
+ }
+ if(page==='abs-book-detail' && data) {
+ import('./abs.js').then(m => m.loadAbsBookDetail(data.id));
+ }
+
+ if(!S._navJumping) {
+ S._navStack = S._navStack.slice(0, S._navCursor + 1);
+ const safe = data ? JSON.parse(JSON.stringify(data)) : null;
+ const last = S._navStack[S._navCursor];
+ if(!last || last.page !== page || JSON.stringify(last.data) !== JSON.stringify(safe)) {
+ S._navStack.push({ page, data: safe });
+ }
+ S._navCursor = S._navStack.length - 1;
+ _navSave();
+ try { history.pushState({ nsIdx: S._navCursor }, '', ''); } catch {}
+ }
+}
+
+export function _navGoBack() {
+ if(document.getElementById('np-sheet')?.classList.contains('open')) { closeNpMenu(); return; }
+ if(S.npOpen) { closeNowPlaying(); return; }
+ if(S._navCursor > 0) {
+ S._navCursor--;
+ const entry = S._navStack[S._navCursor];
+ _navSave();
+ S._navJumping = true;
+ navigate(entry.page, entry.data);
+ S._navJumping = false;
+ }
+ try { history.pushState({ nsIdx: S._navCursor }, '', ''); } catch {}
+}
diff --git a/src/views/playlists.js b/src/views/playlists.js
new file mode 100644
index 0000000..2316bfa
--- /dev/null
+++ b/src/views/playlists.js
@@ -0,0 +1,250 @@
+import { S } from '../state.js';
+import { esc, toast } from '../utils.js';
+import { api, apiCached, apiIsCached, coverUrl, romajiLookup } from '../api.js';
+import { makeCard, makeSongItem } from '../ui/components.js';
+import { playQueue } from '../player/player.js';
+import { openEditPlModal } from '../ui/ui.js';
+// Circular — safe (called inside event handlers)
+import { navigate } from './navigation.js';
+import { quickPlayPlaylist } from './detail.js';
+
+// ─── PLAYLISTS ───
+export async function loadPlaylists() {
+ const g = document.getElementById('playlists-grid');
+ if (!apiIsCached('getPlaylists')) g.innerHTML = '';
+ try {
+ const r = await apiCached('getPlaylists', '', 3 * 60 * 1000);
+ const lists = r.playlists?.playlist || [];
+ g.innerHTML = '';
+ if(!lists.length) { g.innerHTML = '🎶
Keine Playlists vorhanden
'; return; }
+ lists.forEach(pl => {
+ const cv = coverUrl(pl.coverArt, 512);
+ const c = makeCard(pl.name, pl.songCount+' Titel', cv, '🎶');
+ c.dataset.plId = pl.id;
+ c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayPlaylist(pl); };
+ c.querySelector('.card-info').onclick = () => navigate('playlist-detail', pl);
+ g.appendChild(c);
+ });
+ lists.forEach(pl => {
+ api('getPlaylist', `&id=${pl.id}`).then(r => {
+ const count = (r.playlist?.entry || []).length;
+ const card = g.querySelector(`[data-pl-id="${pl.id}"]`);
+ if (card) card.querySelector('.card-sub').textContent = count + ' Titel';
+ }).catch(() => {});
+ });
+ } catch(e) { g.innerHTML = `${e.message}
`; }
+}
+
+export async function loadPlaylistDetail(pl) {
+ S._currentPlaylist = {...pl};
+ document.getElementById('playlist-detail-name').textContent = pl.name;
+ const cw = document.getElementById('playlist-cover');
+ const cv = coverUrl(pl.coverArt, 512);
+ cw.innerHTML = cv ? `
` : '🎶
';
+ document.getElementById('btn-edit-playlist').onclick = () => openEditPlModal(S._currentPlaylist);
+ const sl = document.getElementById('playlist-songs');
+ sl.innerHTML = '';
+ try {
+ const r = await api('getPlaylist', `&id=${pl.id}`);
+ const full = r.playlist || {};
+ S._currentPlaylist = {...pl, ...full, id: pl.id};
+ const songs = full.entry || [];
+ const totalMin = Math.round(songs.reduce((s,t) => s+(t.duration||0), 0) / 60);
+ document.getElementById('playlist-detail-stats').textContent = `${songs.length} Titel · ${totalMin} Min.`;
+ const overviewCard = document.querySelector(`#playlists-grid [data-pl-id="${pl.id}"]`);
+ if (overviewCard) overviewCard.querySelector('.card-sub').textContent = songs.length + ' Titel';
+ const descEl = document.getElementById('playlist-detail-desc');
+ if(descEl) { descEl.textContent = full.comment||''; descEl.style.display = full.comment ? '' : 'none'; }
+ document.getElementById('btn-play-playlist').onclick = () => playQueue(songs, 0);
+ document.getElementById('btn-shuffle-playlist').onclick = () => {
+ playQueue([...songs].sort(() => Math.random()-.5), 0);
+ };
+ sl.innerHTML = '';
+ const plSearch = document.getElementById('playlist-search');
+ if (plSearch) plSearch.value = '';
+ songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs, { noArtistLink: true })));
+ _setupReorderMode(sl, songs, S._currentPlaylist);
+ } catch(e) { sl.innerHTML = `${e.message}
`; }
+}
+
+export function filterPlaylist(val) {
+ const q = val.trim().toLowerCase();
+ document.querySelectorAll('#playlist-songs .song-item').forEach(item => {
+ const title = (item.querySelector('.song-title')?.textContent || '').toLowerCase();
+ const artist = (item.querySelector('.song-artist')?.textContent || '').toLowerCase();
+ if (!q || title.includes(q) || artist.includes(q)) { item.style.display = ''; return; }
+ const rm = romajiLookup(item.dataset.songId);
+ item.style.display = ((rm?.titleRomaji||'').toLowerCase().includes(q) || (rm?.artistRomaji||'').toLowerCase().includes(q)) ? '' : 'none';
+ });
+}
+
+// ─── PLAYLIST REORDER ───
+let _reorderCtx = null;
+
+function _setupReorderMode(container, songs, pl) {
+ if (_reorderCtx) _exitReorderMode(false);
+ _reorderCtx = { container, songs, pl };
+ const btn = document.getElementById('btn-reorder-playlist');
+ if (btn) btn.onclick = plReorderEnter;
+ const bar = document.getElementById('pl-reorder-bar');
+ if (bar) bar.style.display = 'none';
+}
+
+export function plReorderEnter() {
+ if (!_reorderCtx || _reorderCtx.active) return;
+ _reorderCtx.active = true;
+ const { container, songs } = _reorderCtx;
+ container.classList.add('pl-draggable');
+ container.addEventListener('contextmenu', _blockCtxMenu, true);
+
+ let dragSrc = null, touchDragSrc = null;
+
+ Array.from(container.children).forEach((item, i) => {
+ const handle = document.createElement('div');
+ handle.className = 'pl-drag-handle';
+ handle.innerHTML = '';
+ item.insertBefore(handle, item.firstChild);
+ item._songRef = songs[i];
+
+ let fromHandle = false;
+ handle.addEventListener('mousedown', () => { fromHandle = true; item.draggable = true; });
+ item.addEventListener('dragstart', e => {
+ if (!fromHandle) { e.preventDefault(); item.draggable = false; return; }
+ fromHandle = false; dragSrc = item;
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('text/plain', '');
+ setTimeout(() => item.classList.add('dragging'), 0);
+ });
+ item.addEventListener('dragend', () => {
+ fromHandle = false; item.draggable = false; dragSrc = null;
+ item.classList.remove('dragging');
+ container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
+ });
+ item.addEventListener('dragover', e => {
+ e.preventDefault(); e.dataTransfer.dropEffect = 'move';
+ if (item === dragSrc) return;
+ container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
+ item.classList.add('drag-over');
+ });
+ item.addEventListener('dragleave', e => { if (!item.contains(e.relatedTarget)) item.classList.remove('drag-over'); });
+ item.addEventListener('drop', e => {
+ e.preventDefault(); e.stopPropagation();
+ if (!dragSrc || item === dragSrc) return;
+ item.classList.remove('drag-over');
+ _doReorder(container, songs, dragSrc, item);
+ });
+
+ handle.addEventListener('touchstart', e => {
+ e.preventDefault(); e.stopPropagation();
+ touchDragSrc = item; item.classList.add('dragging');
+ }, { passive: false });
+ handle.addEventListener('touchmove', e => {
+ if (!touchDragSrc) return;
+ e.preventDefault();
+ const t = e.touches[0];
+ container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
+ const over = _itemAtPoint(container, t.clientX, t.clientY, touchDragSrc);
+ if (over) over.classList.add('drag-over');
+ }, { passive: false });
+ handle.addEventListener('touchend', e => {
+ if (!touchDragSrc) return;
+ const t = e.changedTouches[0];
+ touchDragSrc.classList.remove('dragging');
+ container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
+ const over = _itemAtPoint(container, t.clientX, t.clientY, touchDragSrc);
+ if (over) _doReorder(container, songs, touchDragSrc, over);
+ touchDragSrc = null;
+ }, { passive: false });
+ });
+
+ const bar = document.getElementById('pl-reorder-bar');
+ if (bar) bar.style.display = 'flex';
+}
+
+function _blockCtxMenu(e) { e.preventDefault(); e.stopPropagation(); }
+
+function _itemAtPoint(container, x, y, exclude) {
+ return (document.elementsFromPoint(x, y) || [])
+ .find(el => el.classList?.contains('song-item') && el !== exclude && container.contains(el)) || null;
+}
+
+function _doReorder(container, songs, src, target) {
+ const all = Array.from(container.children);
+ const from = all.indexOf(src), to = all.indexOf(target);
+ container.insertBefore(src, from < to ? target.nextSibling : target);
+ const newOrder = Array.from(container.children).map(el => el._songRef);
+ songs.splice(0, songs.length, ...newOrder);
+ Array.from(container.children).forEach((el, i) => {
+ const n = el.querySelector('.song-num'); if (n) n.textContent = i + 1;
+ });
+}
+
+export function plReorderSave() { _exitReorderMode(true); }
+export function plReorderCancel() { _exitReorderMode(false); }
+
+function _exitReorderMode(save) {
+ if (!_reorderCtx) return;
+ const { container, songs, pl } = _reorderCtx;
+ _reorderCtx.active = false;
+ container.classList.remove('pl-draggable');
+ container.removeEventListener('contextmenu', _blockCtxMenu, true);
+ container.querySelectorAll('.pl-drag-handle').forEach(h => h.remove());
+ const bar = document.getElementById('pl-reorder-bar');
+ if (bar) bar.style.display = 'none';
+ if (save) _savePlaylistOrder(pl, songs);
+}
+
+async function _savePlaylistOrder(pl, songs) {
+ try {
+ const songIds = songs.map(s => `&songId=${s.id}`).join('');
+ await api('createPlaylist', `&playlistId=${pl.id}&name=${encodeURIComponent(pl.name)}${songIds}`);
+ toast('✅ Reihenfolge gespeichert');
+ } catch(e) { toast('Fehler beim Speichern: ' + e.message); }
+}
+
+// ─── STARRED / RANDOM ───
+let _starredSort = 'newest';
+
+export async function loadStarred(sort = _starredSort) {
+ _starredSort = sort;
+ document.querySelectorAll('#starred-sort-chips .sort-chip').forEach(c =>
+ c.classList.toggle('active', c.dataset.sort === sort));
+ const el = document.getElementById('starred-songs');
+ el.innerHTML = '';
+ const searchEl = document.getElementById('starred-search');
+ if (searchEl) searchEl.value = '';
+ try {
+ const r = await api('getStarred2');
+ let songs = (r.starred2?.song || []).map(s => ({...s, _starred: true}));
+ if (sort === 'newest') songs.sort((a, b) => new Date(b.starred||0) - new Date(a.starred||0));
+ else if (sort === 'oldest') songs.sort((a, b) => new Date(a.starred||0) - new Date(b.starred||0));
+ else if (sort === 'az') songs.sort((a, b) => (a.title||'').localeCompare(b.title||''));
+ else if (sort === 'artist') songs.sort((a, b) => (a.artist||'').localeCompare(b.artist||''));
+ else if (sort === 'plays') songs.sort((a, b) => (b.playCount||0) - (a.playCount||0));
+ el.innerHTML = '';
+ if(!songs.length) { el.innerHTML = ''; return; }
+ songs.forEach((s,i) => el.appendChild(makeSongItem(s, i, songs, { noArtistLink: true })));
+ } catch(e) { el.innerHTML = `${e.message}
`; }
+}
+
+export function filterStarred(val) {
+ const q = val.trim().toLowerCase();
+ document.querySelectorAll('#starred-songs .song-item').forEach(item => {
+ const title = (item.querySelector('.song-title')?.textContent || '').toLowerCase();
+ const artist = (item.querySelector('.song-artist')?.textContent || '').toLowerCase();
+ if (!q || title.includes(q) || artist.includes(q)) { item.style.display = ''; return; }
+ const rm = romajiLookup(item.dataset.songId);
+ item.style.display = ((rm?.titleRomaji||'').toLowerCase().includes(q) || (rm?.artistRomaji||'').toLowerCase().includes(q)) ? '' : 'none';
+ });
+}
+
+export async function loadRandom() {
+ const el = document.getElementById('random-songs');
+ el.innerHTML = '';
+ try {
+ const r = await api('getRandomSongs', '&size=50');
+ const songs = r.randomSongs?.song || [];
+ el.innerHTML = '';
+ songs.forEach((s,i) => el.appendChild(makeSongItem(s, i, songs)));
+ } catch(e) { el.innerHTML = `${e.message}
`; }
+}
diff --git a/src/views/search.js b/src/views/search.js
new file mode 100644
index 0000000..7965367
--- /dev/null
+++ b/src/views/search.js
@@ -0,0 +1,252 @@
+import { S } from '../state.js';
+import { esc, toast } from '../utils.js';
+import { api, itunesSearch, itunesLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js';
+import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem, mkSection } from '../ui/components.js';
+import { playPreview } from '../player/player.js';
+import { openDlModal } from '../ui/ui.js';
+import { closeNowPlaying } from '../player/nowplaying.js';
+// Circular — safe (called inside function bodies)
+import { navigate } from './navigation.js';
+import { getArtistImage } from './home.js';
+
+// ─── ROMAJI → HIRAGANA ───
+let _wanakana = null;
+async function _romajiToHiragana(q) {
+ if (!/^[a-z\s'-]+$/i.test(q.trim())) return null;
+ try {
+ if (!_wanakana) _wanakana = await import('https://cdn.jsdelivr.net/npm/wanakana@5/esm/wanakana.js');
+ const h = _wanakana.toHiragana(q.trim(), { passRomaji: false });
+ return /[-ゟ]/.test(h) ? h : null;
+ } catch { return null; }
+}
+
+function _mergeUniq(a, b) {
+ const seen = new Set(a.map(x => x.id));
+ return [...a, ...(b || []).filter(x => !seen.has(x.id))];
+}
+
+// ─── LOCAL SEARCH ───
+export function onLocalSearch(val) {
+ clearTimeout(S.searchTimer);
+ if(!val.trim()) { navigate('home'); return; }
+ navigate('search');
+ S.searchTimer = setTimeout(() => doLocalSearch(val), 350);
+}
+
+export async function doLocalSearch(q, el) {
+ if (!el) el = document.getElementById('search-results');
+ el.innerHTML = '';
+ try {
+ const hiragana = await _romajiToHiragana(q);
+ const isRomaji = /^[a-z\s'-]+$/i.test(q.trim());
+ const [r, r2, romajiIdx] = await Promise.all([
+ api('search3', `&query=${encodeURIComponent(q)}&songCount=30&albumCount=10&artistCount=6`),
+ hiragana ? api('search3', `&query=${encodeURIComponent(hiragana)}&songCount=20&albumCount=6&artistCount=4`) : Promise.resolve(null),
+ isRomaji ? fetchRomajiIndex() : Promise.resolve(null),
+ ]);
+ const res = r.searchResult3 || {};
+ const res2 = r2?.searchResult3 || {};
+ el.innerHTML = '';
+ const artists = _mergeUniq(res.artist||[], res2.artist);
+ const albums = _mergeUniq(res.album ||[], res2.album);
+ const qLow = q.trim().toLowerCase();
+ const romajiHits = romajiIdx ? romajiIdx.filter(e => e.r.includes(qLow)) : [];
+ const songs = _mergeUniq(_mergeUniq(res.song||[], res2.song), romajiHits);
+
+ if(!artists.length && !albums.length && !songs.length) {
+ el.innerHTML = ''; return;
+ }
+ if(artists.length) {
+ const s = mkSection('Künstler');
+ const g = document.createElement('div'); g.className = 'scroll-row';
+ artists.forEach(a => {
+ const c = makeCard(dispArtistById(a.id, a.name), a.albumCount+' Alben', null, '🎤');
+ c.classList.add('artist-card');
+ c.onclick = () => navigate('artist-detail', a);
+ g.appendChild(c);
+ getArtistImage(a.id).then(img => {
+ if(!img) return;
+ const artEl = c.querySelector('.card-art');
+ if(!artEl) return;
+ artEl.innerHTML = `
`;
+ });
+ });
+ s.appendChild(g); el.appendChild(s);
+ }
+ if(albums.length) {
+ const s = mkSection('Alben');
+ const g = document.createElement('div'); g.className = 'scroll-row';
+ albums.forEach(a => g.appendChild(makeAlbumCard(a)));
+ s.appendChild(g); el.appendChild(s);
+ }
+ if(songs.length) {
+ const sec = mkSection('Titel');
+ const list = document.createElement('div'); list.className = 'song-list';
+ songs.forEach((so,i) => list.appendChild(makeSongItem(so, i, songs)));
+ sec.appendChild(list); el.appendChild(sec);
+ }
+ } catch(e) { el.innerHTML = `${e.message}
`; }
+}
+
+// ─── HEADER SEARCH ───
+export function onHeaderSearch(val) {
+ const overlay = document.getElementById('header-search-overlay');
+ const clearBtn = document.getElementById('header-search-clear');
+ if (clearBtn) clearBtn.style.display = val ? 'block' : 'none';
+ clearTimeout(S._headerSearchTimer);
+ if (!val.trim()) { if (overlay) overlay.style.display = 'none'; return; }
+ if (overlay) {
+ const header = document.getElementById('mobile-header');
+ if (header) overlay.style.top = header.offsetHeight + 'px';
+ overlay.style.display = 'block';
+ }
+ const el = document.getElementById('header-search-results');
+ S._headerSearchTimer = setTimeout(() => doLocalSearch(val, el), 300);
+}
+
+export function onMobileSearch(val) { onHeaderSearch(val); }
+
+// ─── GLOBAL (ITUNES) SEARCH ───
+export function onGlobalSearch(val) {
+ clearTimeout(S.globalSearchTimer);
+ const el = document.getElementById('global-search-results');
+ if(!val.trim()) { el.innerHTML = '🔍
Gib einen Suchbegriff ein
'; return; }
+ el.innerHTML = '';
+ S.globalSearchTimer = setTimeout(() => doGlobalSearch(val), 400);
+}
+
+export async function doGlobalSearch(q) {
+ const el = document.getElementById('global-search-results');
+ try {
+ const [tracks, albums, artists] = await Promise.all([
+ itunesSearch(q,'musicTrack',25),
+ itunesSearch(q,'album',10),
+ itunesSearch(q,'musicArtist',6),
+ ]);
+ el.innerHTML = '';
+ const trackResults = tracks.filter(t => t.wrapperType === 'track');
+
+ if(artists.length) {
+ const s = mkSection('Künstler');
+ const row = document.createElement('div'); row.className = 'scroll-row';
+ artists.forEach(a => {
+ const c = makeCard(a.artistName, 'iTunes Künstler', null, '🎤');
+ c.onclick = () => loadItunesArtistResults(a.artistId, a.artistName);
+ row.appendChild(c);
+ itunesSearch(a.artistName,'album',1).then(res => {
+ if(!res[0]) return;
+ const imgUrl = res[0].artworkUrl100?.replace('100x100','300x300');
+ if(!imgUrl) return;
+ const artEl = c.querySelector('.card-art'); if(!artEl) return;
+ const img = document.createElement('img');
+ img.src = imgUrl; img.loading = 'lazy'; img.style.cssText = 'width:100%;height:100%;object-fit:cover;transition:transform .4s';
+ const overlay = artEl.querySelector('.play-overlay');
+ artEl.innerHTML = ''; artEl.appendChild(img); if(overlay) artEl.appendChild(overlay);
+ img.onerror = () => { artEl.innerHTML = '🎤
'; if(overlay) artEl.appendChild(overlay); };
+ c.addEventListener('mouseenter', () => img.style.transform = 'scale(1.05)');
+ c.addEventListener('mouseleave', () => img.style.transform = 'scale(1)');
+ }).catch(() => {});
+ });
+ s.appendChild(row); el.appendChild(s);
+ }
+ if(albums.length) {
+ const s = mkSection('Alben');
+ const row = document.createElement('div'); row.className = 'scroll-row';
+ albums.forEach(a => {
+ const art = a.artworkUrl100?.replace('100x100','300x300');
+ const c = makeCard(a.collectionName, a.artistName, art, '💿');
+ c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); loadItunesAlbum(a); };
+ c.querySelector('.dl-btn-overlay')?.remove();
+ const dlOverlay = document.createElement('button');
+ dlOverlay.className = 'dl-btn-overlay';
+ dlOverlay.title = 'Auf Server laden';
+ dlOverlay.innerHTML = ``;
+ dlOverlay.onclick = (e) => { e.stopPropagation(); openDlModal({artistName:a.artistName,trackName:a.collectionName,artworkUrl100:a.artworkUrl100,collectionName:a.collectionName,isAlbum:true,itunesData:a}); };
+ c.querySelector('.play-overlay').appendChild(dlOverlay);
+ c.onclick = () => loadItunesAlbum(a);
+ row.appendChild(c);
+ });
+ s.appendChild(row); el.appendChild(s);
+ }
+ if(trackResults.length) {
+ const s = mkSection('Titel');
+ const list = document.createElement('div'); list.className = 'song-list';
+ trackResults.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, trackResults)));
+ s.appendChild(list); el.appendChild(s);
+ }
+ if(!artists.length && !albums.length && !trackResults.length) {
+ el.innerHTML = '🔍
Keine Ergebnisse gefunden
';
+ }
+ } catch(e) { el.innerHTML = `Suche fehlgeschlagen: ${e.message}
`; }
+}
+
+export async function loadItunesAlbum(album) {
+ try {
+ const tracks = await itunesLookupAlbum(album.collectionId);
+ if(!tracks.length) { toast('Keine Tracks gefunden'); return; }
+ const el = document.getElementById('global-search-results');
+ const s = document.createElement('div'); s.className = 'search-section';
+ s.innerHTML = `
+
+
+
${esc(album.collectionName)}
+
${esc(album.artistName)}
+
+
+
+ Tracks
`;
+ const list = document.createElement('div'); list.className = 'song-list';
+ tracks.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, tracks)));
+ s.appendChild(list);
+ el.innerHTML = ''; el.appendChild(s);
+ } catch(e) { toast('Fehler: ' + e.message); }
+}
+
+export function loadItunesArtistResults(artistId, artistName) {
+ navigate('artist-detail', { id: null, name: artistName, itunesArtistId: artistId });
+}
+
+// ─── SEARCH HELPERS ───
+export function searchSimilarArtist(artistName) {
+ navigate('discover');
+ document.getElementById('global-search-input').value = artistName;
+ onGlobalSearch(artistName);
+}
+
+export async function searchAndPreview(trackName, artistName) {
+ try {
+ const res = await itunesSearch(trackName+' '+artistName, 'musicTrack', 1);
+ if(res[0]?.previewUrl) playPreview(res[0]); else toast('Kein Preview verfügbar');
+ } catch { toast('Suche fehlgeschlagen'); }
+}
+
+// ─── NAVIGATION SHORTCUTS ───
+export async function s_goArtist(e, name, id) {
+ if(!name) return;
+ if(id) { navigate('artist-detail', {id, name}); return; }
+ try {
+ const r = await api('search3', `&query=${encodeURIComponent(name)}&artistCount=5&albumCount=0&songCount=0`);
+ const artists = r.searchResult3?.artist || [];
+ const match = artists.find(a => a.name.toLowerCase() === name.toLowerCase()) || artists[0];
+ if(match) navigate('artist-detail', {id: match.id, name: match.name});
+ else toast('Künstler nicht gefunden');
+ } catch { toast('Suche fehlgeschlagen'); }
+}
+
+export function s_goAlbum(e, albumId, albumName, artist, artistId) {
+ if(albumId) navigate('album-detail', {id:albumId, name:albumName, artist, artistId:artistId||'', coverArt:''});
+}
+
+export function playerBarGoAlbum() {
+ const s = S.queue[S.qIdx];
+ if(!s) return;
+ if(S.npOpen) closeNowPlaying();
+ if(s.albumId) navigate('album-detail', {id:s.albumId, name:s.album||'', artist:s.artist||'', artistId:s.artistId||'', coverArt:s.coverArt||''});
+}
+
+export function playerBarGoArtist() {
+ const s = S.queue[S.qIdx];
+ if(!s || !s.artist) return;
+ if(S.npOpen) closeNowPlaying();
+ navigate('artist-detail', {id: s.artistId||s.artist, name: s.artist});
+}
diff --git a/src/views/views.js b/src/views/views.js
index 836e637..e51fb65 100644
--- a/src/views/views.js
+++ b/src/views/views.js
@@ -1,1346 +1,8 @@
-import { S } from '../state.js';
-import { esc, formatDur, toast } from '../utils.js';
-import { api, apiCached, apiIsCached, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, itunesTopCharts, itunesTopSongs } from '../api.js';
-// Circular imports — safe (only used inside function bodies)
-import { playQueue, dlSongLocal, playPreview, toggleStar, parseArtists } from '../player/player.js';
-import { showCtxMenu, openEditPlModal, openDlModal, closeSidebar, switchNavMode } from '../ui/ui.js';
-import { closeNowPlaying, closeNpMenu } from '../player/nowplaying.js';
-import { makeCard, makeAlbumCard, makeSongItem, makeGlobalTrackItem, mkSection } from '../ui/components.js';
-import { fetchRomajiIndex, dispArtistById, dispAlbumName, dispAlbum, romajiLookup } from '../api.js';
-import { loadSpotifyImporter } from './spotify.js';
-import { loadSoundCloudImporter } from './soundcloud.js';
-
-// ─── NAVIGATION HISTORY ───
-export function _navSave() {
- try { sessionStorage.setItem('ns_nav', JSON.stringify({stack: S._navStack, cursor: S._navCursor})); } catch {}
-}
-
-export function _navLoad() {
- try {
- const s = JSON.parse(sessionStorage.getItem('ns_nav') || 'null');
- if(s && Array.isArray(s.stack) && s.stack.length) {
- S._navStack = s.stack;
- S._navCursor = s.cursor;
- return true;
- }
- } catch {}
- return false;
-}
-
-export function closeHeaderSearch() {
- const overlay = document.getElementById('header-search-overlay');
- const input = document.getElementById('header-search-input');
- const clearBtn = document.getElementById('header-search-clear');
- if (overlay) overlay.style.display = 'none';
- if (input) input.value = '';
- if (clearBtn) clearBtn.style.display = 'none';
- clearTimeout(S._headerSearchTimer);
-}
-
-export function navigate(page, data=null) {
- closeHeaderSearch();
- document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
- document.querySelectorAll('.nav-item,.bnav-item').forEach(n => n.classList.remove('active','active-blue'));
- const cls = ['discover','new-releases','recommendations','spotify-importer'].includes(page) ? 'active-blue' : 'active';
- document.querySelectorAll(`[data-page="${page}"]`).forEach(el => el.classList.add(cls));
- const pageEl = document.getElementById(`page-${page}`);
- if(pageEl) { pageEl.classList.add('active'); document.getElementById('main-content').scrollTop = 0; }
- closeSidebar();
- if(['discover','new-releases','recommendations','random','spotify-importer'].includes(page)) switchNavMode('discover');
- else if(['home','artists','albums','starred','playlists'].includes(page)) switchNavMode('library');
- // ABS pages don't switch the bottom nav mode (they have their own sidebar section)
-
- if(page==='home') loadHomeData();
- if(page==='artists' && !document.getElementById('artists-grid').children.length) loadArtists();
- if(page==='albums' && !document.getElementById('albums-grid').children.length) loadAlbums();
- if(page==='starred') loadStarred();
- if(page==='random') loadRandom();
- if(page==='artist-detail' && data) loadArtistDetail(data);
- if(page==='artist-all-songs' && data) loadArtistAllSongs(data);
- if(page==='album-detail' && data) loadAlbumDetail(data);
- if(page==='genre-detail' && data) loadGenreDetail(data);
- if(page==='itunes-album-detail'&& data) loadItunesAlbumDetail(data);
- if(page==='new-releases') loadNewReleases();
- if(page==='playlists') loadPlaylists();
- if(page==='playlist-detail' && data) loadPlaylistDetail(data);
- if(page==='recommendations') loadRecommendations();
- if(page==='spotify-importer') { loadSpotifyImporter(); loadSoundCloudImporter(); }
- if(page==='audiobooks') {
- import('./abs.js').then(m => {
- if (S.abs.currentLibraryId) m.loadAbsBooks(S.abs.currentLibraryId);
- });
- }
- if(page==='abs-book-detail' && data) {
- import('./abs.js').then(m => m.loadAbsBookDetail(data.id));
- }
-
- if(!S._navJumping) {
- S._navStack = S._navStack.slice(0, S._navCursor + 1);
- const safe = data ? JSON.parse(JSON.stringify(data)) : null;
- const last = S._navStack[S._navCursor];
- if(!last || last.page !== page || JSON.stringify(last.data) !== JSON.stringify(safe)) {
- S._navStack.push({ page, data: safe });
- }
- S._navCursor = S._navStack.length - 1;
- _navSave();
- try { history.pushState({ nsIdx: S._navCursor }, '', ''); } catch {}
- }
-}
-
-export function _navGoBack() {
- if(document.getElementById('np-sheet')?.classList.contains('open')) { closeNpMenu(); return; }
- if(S.npOpen) { closeNowPlaying(); return; }
- if(S._navCursor > 0) {
- S._navCursor--;
- const entry = S._navStack[S._navCursor];
- _navSave();
- S._navJumping = true;
- navigate(entry.page, entry.data);
- S._navJumping = false;
- }
- try { history.pushState({ nsIdx: S._navCursor }, '', ''); } catch {}
-}
-
-// ─── ARTIST IMAGE ───
-export async function getArtistImage(artistId) {
- if(S.artistImgCache[artistId] !== undefined) return S.artistImgCache[artistId];
- try {
- const r = await api('getArtistInfo2', `&id=${artistId}&count=0`);
- const info = r.artistInfo2 || {};
- const img = info.largeImageUrl || info.mediumImageUrl || info.smallImageUrl || null;
- const isBlank = img && (img.includes('2a96cbd8b46e442fc41c2b86b821562f') || img.endsWith('/noimage/'));
- S.artistImgCache[artistId] = isBlank ? null : img;
- return S.artistImgCache[artistId];
- } catch { S.artistImgCache[artistId] = null; return null; }
-}
-
-// ─── HOME ───
-export async function loadHomeData() {
- const hour = new Date().getHours();
- const greeting = hour < 12 ? 'Guten Morgen' : hour < 18 ? 'Guten Tag' : 'Guten Abend';
- const name = S.cfg.user ? `, ${S.cfg.user}` : '';
- const el = document.getElementById('home-greeting');
- if (el) el.textContent = `${greeting}${name} 👋`;
-
- loadHeroCard();
- loadGenreSection();
- loadAlbumRow('home-newest', 'getAlbumList2', '&type=newest&size=20');
- loadAlbumRow('home-recent', 'getAlbumList2', '&type=recent&size=20');
- loadAlbumRow('home-random', 'getAlbumList2', '&type=random&size=20');
- loadAlbumRow('home-frequent', 'getAlbumList2', '&type=frequent&size=20');
-}
-
-async function loadHeroCard() {
- const el = document.getElementById('home-hero');
- if (!el) return;
- try {
- // Combine recent + frequent into a pool and pick one randomly.
- // This gives new users (no play history) recent albums, and returning users a mix.
- const [freqRes, recRes] = await Promise.allSettled([
- api('getAlbumList2', '&type=frequent&size=5'),
- api('getAlbumList2', '&type=recent&size=5'),
- ]);
- const freqList = freqRes.status === 'fulfilled' ? (freqRes.value.albumList2?.album || []) : [];
- const recList = recRes.status === 'fulfilled' ? (recRes.value.albumList2?.album || []) : [];
- // Deduplicate by id, tag source so we can show the right label
- const freqIds = new Set(freqList.map(a => a.id));
- const seen = new Set();
- const pool = [...freqList, ...recList].filter(a => a.id && !seen.has(a.id) && seen.add(a.id));
- const album = pool.length ? pool[Math.floor(Math.random() * pool.length)] : null;
- if (!album) return;
- const heroLabel = freqIds.has(album.id) ? 'Meistgespielt' : 'Zuletzt gehört';
- const bgUrl = coverUrl(album.coverArt, 1024);
- const thumbUrl = coverUrl(album.coverArt, 512);
- el.innerHTML = `
- ${bgUrl ? `
` : ''}
-
- ${thumbUrl ? `

` : ''}
-
-
${heroLabel}
-
${esc(album.name)}
-
${esc(album.artist || '')}
-
-
-
`;
- el.querySelector('.home-hero-cover')?.addEventListener('click', () => navigate('album-detail', album));
- el.querySelector('.home-hero-artist').onclick = () => navigate('album-detail', album);
- el.querySelector('.home-hero-play').onclick = () => quickPlayAlbum(album);
- el.classList.add('loaded');
- } catch { /* hero is optional — silent fail */ }
-}
-
-const _GENRE_COLORS = ['#e8445a','#3b82f6','#f97316','#22c55e','#a855f7','#06b6d4','#eab308','#ec4899','#14b8a6','#f43f5e','#8b5cf6','#84cc16'];
-function _genreColor(name) {
- let h = 0;
- for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
- return _GENRE_COLORS[h % _GENRE_COLORS.length];
-}
-
-let _allGenres = [];
-let _genresExpanded = false;
-const GENRE_LIMIT = () => window.innerWidth >= 700 ? 24 : 6;
-
-async function loadGenreSection() {
- const el = document.getElementById('home-genres');
- const more = document.getElementById('home-genres-more');
- if (!el) return;
- try {
- const r = await api('getGenres');
- _allGenres = (r.genres?.genre || [])
- .filter(g => (g.songCount || 0) > 0)
- .sort((a, b) => (b.songCount || 0) - (a.songCount || 0));
- _renderGenres();
- if (_allGenres.length > GENRE_LIMIT() && more) more.style.display = '';
- } catch { el.style.display = 'none'; }
- // Re-sort by total play count once backend data arrives (non-blocking)
- if (S.cfg.backendUrl && _allGenres.length) {
- fetch(`${S.cfg.backendUrl}/api/genre-playcounts?ndUrl=${encodeURIComponent(S.cfg.url)}&user=${encodeURIComponent(S.cfg.user)}&pass=${encodeURIComponent(S.cfg.pass)}`)
- .then(r => r.ok ? r.json() : null)
- .then(plays => {
- if (!plays || !_allGenres.length) return;
- _allGenres.sort((a, b) => (plays[b.value] || 0) - (plays[a.value] || 0));
- _renderGenres();
- })
- .catch(() => {});
- }
-}
-
-function _renderGenres() {
- const el = document.getElementById('home-genres');
- if (!el) return;
- const list = _genresExpanded ? _allGenres : _allGenres.slice(0, GENRE_LIMIT());
- el.innerHTML = '';
- list.forEach(g => {
- const card = document.createElement('div');
- card.className = 'genre-card';
- card.title = `${g.songCount} Titel`;
- const color = _genreColor(g.value);
- card.innerHTML = `
-
- ${esc(g.value)}
- `;
- card.querySelector('.genre-card-play').onclick = (e) => { e.stopPropagation(); playGenre(g.value); };
- card.onclick = () => navigate('genre-detail', g);
- el.appendChild(card);
- });
-}
-
-export function toggleAllGenres() {
- _genresExpanded = !_genresExpanded;
- const more = document.getElementById('home-genres-more');
- if (more) more.textContent = _genresExpanded ? 'Weniger' : 'Alle anzeigen';
- _renderGenres();
-}
-
-async function playGenre(genre) {
- try {
- const r = await api('getSongsByGenre', `&genre=${encodeURIComponent(genre)}&count=200`);
- const songs = r.songsByGenre?.song || [];
- if (!songs.length) return;
- const shuffled = [...songs].sort(() => Math.random() - 0.5);
- playQueue(shuffled, 0);
- } catch {}
-}
-
-async function loadGenreDetail(g) {
- const genre = g.value || g;
- const color = _genreColor(genre);
- document.getElementById('genre-detail-name').textContent = genre;
- document.getElementById('genre-detail-stats').textContent = g.songCount ? `${g.songCount} Titel` : '';
- const coverWrap = document.getElementById('genre-cover-wrap');
- coverWrap.style.background = `linear-gradient(135deg, ${color}33, ${color}11)`;
- coverWrap.style.borderRadius = '10px';
- coverWrap.style.border = `2px solid ${color}55`;
- document.getElementById('genre-back-btn').onclick = () => _navGoBack();
-
- const list = document.getElementById('genre-songs');
- list.innerHTML = '';
-
- let songs = [];
- try {
- const r = await api('getSongsByGenre', `&genre=${encodeURIComponent(genre)}&count=500`);
- songs = r.songsByGenre?.song || [];
- } catch { list.innerHTML = 'Fehler beim Laden
'; return; }
-
- document.getElementById('genre-detail-stats').textContent = `${songs.length} Titel`;
- document.getElementById('btn-play-genre').onclick = () => {
- const shuffled = [...songs].sort(() => Math.random() - 0.5);
- playQueue(shuffled, 0);
- };
-
- list.innerHTML = '';
- songs.forEach((s, i) => list.appendChild(makeSongItem(s, i, songs)));
-}
-
-export async function loadAlbumRow(id, method, extra) {
- const c = document.getElementById(id);
- if(!c) return;
- if (!apiIsCached(method, extra)) c.innerHTML = '';
- try {
- const [r] = await Promise.all([
- apiCached(method, extra),
- S.cfg.showRomaji ? fetchRomajiIndex().catch(() => {}) : Promise.resolve(),
- ]);
- c.innerHTML = '';
- (r.albumList2?.album || []).forEach(a => c.appendChild(makeAlbumCard(a)));
- } catch(e) { c.innerHTML = `${e.message}
`; }
-}
-
-// ─── ARTISTS ───
-export async function loadArtists() {
- const g = document.getElementById('artists-grid');
- if (!apiIsCached('getArtists')) g.innerHTML = '';
- try {
- const r = await apiCached('getArtists');
- const all = (r.artists?.index || []).flatMap(i => i.artist || []);
- if (S.cfg.showRomaji) await fetchRomajiIndex().catch(() => {});
- g.innerHTML = '';
- all.forEach(a => {
- const c = makeCard(dispArtistById(a.id, a.name), a.albumCount+' Alben', null, '🎤');
- c.classList.add('artist-card');
- c.onclick = () => navigate('artist-detail', a);
- g.appendChild(c);
- getArtistImage(a.id).then(imgUrl => {
- if(!imgUrl) return;
- const artEl = c.querySelector('.card-art');
- if(!artEl) return;
- const overlay = artEl.querySelector('.play-overlay');
- const img = document.createElement('img');
- img.src = imgUrl; img.loading = 'lazy'; img.style.cssText = 'width:100%;height:100%;object-fit:cover;transition:transform .4s';
- artEl.innerHTML = ''; artEl.appendChild(img); if(overlay) artEl.appendChild(overlay);
- c.addEventListener('mouseenter', () => img.style.transform = 'scale(1.05)');
- c.addEventListener('mouseleave', () => img.style.transform = 'scale(1)');
- });
- });
- } catch(e) { g.innerHTML = `${e.message}
`; }
-}
-
-// ─── ALBUMS ───
-let _albumSort = 'alphabeticalByName';
-
-export async function loadAlbums(type = _albumSort) {
- _albumSort = type;
- document.querySelectorAll('#albums-sort-chips .sort-chip').forEach(c =>
- c.classList.toggle('active', c.dataset.sort === type));
- const g = document.getElementById('albums-grid');
- const extra = `&type=${type}&size=500`;
- if (!apiIsCached('getAlbumList2', extra)) g.innerHTML = '';
- try {
- const r = await apiCached('getAlbumList2', extra);
- g.innerHTML = '';
- (r.albumList2?.album || []).forEach(a => g.appendChild(makeAlbumCard(a)));
- } catch(e) { g.innerHTML = `${e.message}
`; }
-}
-
-// ─── SHARED TOPSONG ROW ───
-// Used by loadArtistDetail (top songs) and loadArtistAllSongs.
-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))';
- 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;
-
- // 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', 5),
- ]);
-
- // Verify name match — iTunes ranks by popularity, so "WRLD Tunes" would otherwise
- // return "Juice WRLD" first. Require all query words to appear in the iTunes name.
- 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; // non-ASCII name (e.g. Japanese) — trust iTunes ranking
- const rw = _normWords(r.artistName);
- return [..._queryWords].every(w => rw.has(w));
- });
- const itunesArtistId = knownItunes ?? iArtistMatch?.artistId ?? 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`;
- 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);
- }
- }
-
- // ── 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','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);
- });
- }
-
- // 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 })));
- }
-}
-
-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.`;
- // Re-render header after songs load — by now overrides cache is ready and songs carry sortAlbum/sortArtist
- 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}
`; }
-}
-
-// ─── PLAYLISTS ───
-export async function loadPlaylists() {
- const g = document.getElementById('playlists-grid');
- if (!apiIsCached('getPlaylists')) g.innerHTML = '';
- try {
- const r = await apiCached('getPlaylists', '', 3 * 60 * 1000);
- const lists = r.playlists?.playlist || [];
- g.innerHTML = '';
- if(!lists.length) { g.innerHTML = '🎶
Keine Playlists vorhanden
'; return; }
- lists.forEach(pl => {
- const cv = coverUrl(pl.coverArt, 512);
- const c = makeCard(pl.name, pl.songCount+' Titel', cv, '🎶');
- c.dataset.plId = pl.id;
- c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayPlaylist(pl); };
- c.querySelector('.card-info').onclick = () => navigate('playlist-detail', pl);
- g.appendChild(c);
- });
- // Navidrome's getPlaylists songCount is often stale — fetch each real count in the background.
- lists.forEach(pl => {
- api('getPlaylist', `&id=${pl.id}`).then(r => {
- const count = (r.playlist?.entry || []).length;
- const card = g.querySelector(`[data-pl-id="${pl.id}"]`);
- if (card) card.querySelector('.card-sub').textContent = count + ' Titel';
- }).catch(() => {});
- });
- } catch(e) { g.innerHTML = `${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); }
-}
-
-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 loadPlaylistDetail(pl) {
- S._currentPlaylist = {...pl};
- document.getElementById('playlist-detail-name').textContent = pl.name;
- const cw = document.getElementById('playlist-cover');
- const cv = coverUrl(pl.coverArt, 512);
- cw.innerHTML = cv ? `
` : '🎶
';
- document.getElementById('btn-edit-playlist').onclick = () => openEditPlModal(S._currentPlaylist);
- const sl = document.getElementById('playlist-songs');
- sl.innerHTML = '';
- try {
- const r = await api('getPlaylist', `&id=${pl.id}`);
- const full = r.playlist || {};
- S._currentPlaylist = {...pl, ...full, id: pl.id};
- const songs = full.entry || [];
- const totalMin = Math.round(songs.reduce((s,t) => s+(t.duration||0), 0) / 60);
- document.getElementById('playlist-detail-stats').textContent = `${songs.length} Titel · ${totalMin} Min.`;
- // Navidrome's getPlaylists can return a stale songCount — backpatch the overview card
- // with the real count we just got from getPlaylist so the grid always shows truth.
- const overviewCard = document.querySelector(`#playlists-grid [data-pl-id="${pl.id}"]`);
- if (overviewCard) overviewCard.querySelector('.card-sub').textContent = songs.length + ' Titel';
- const descEl = document.getElementById('playlist-detail-desc');
- if(descEl) { descEl.textContent = full.comment||''; descEl.style.display = full.comment ? '' : 'none'; }
- document.getElementById('btn-play-playlist').onclick = () => playQueue(songs, 0);
- document.getElementById('btn-shuffle-playlist').onclick = () => {
- const shuffled = [...songs].sort(() => Math.random()-.5);
- playQueue(shuffled, 0);
- };
- sl.innerHTML = '';
- const plSearch = document.getElementById('playlist-search');
- if (plSearch) plSearch.value = '';
- songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs, { noArtistLink: true })));
- _setupReorderMode(sl, songs, S._currentPlaylist);
- } catch(e) { sl.innerHTML = `${e.message}
`; }
-}
-
-export function filterPlaylist(val) {
- const q = val.trim().toLowerCase();
- document.querySelectorAll('#playlist-songs .song-item').forEach(item => {
- const title = (item.querySelector('.song-title')?.textContent || '').toLowerCase();
- const artist = (item.querySelector('.song-artist')?.textContent || '').toLowerCase();
- if (!q || title.includes(q) || artist.includes(q)) { item.style.display = ''; return; }
- const rm = romajiLookup(item.dataset.songId);
- item.style.display = ((rm?.titleRomaji||'').toLowerCase().includes(q) || (rm?.artistRomaji||'').toLowerCase().includes(q)) ? '' : 'none';
- });
-}
-
-// ─── PLAYLIST REORDER MODE ───────────────────────────────────────────────
-let _reorderCtx = null;
-
-function _setupReorderMode(container, songs, pl) {
- // Exit any previous reorder mode silently when loading a new playlist.
- if (_reorderCtx) _exitReorderMode(false);
- _reorderCtx = { container, songs, pl };
- const btn = document.getElementById('btn-reorder-playlist');
- if (btn) btn.onclick = plReorderEnter;
- const bar = document.getElementById('pl-reorder-bar');
- if (bar) bar.style.display = 'none';
-}
-
-export function plReorderEnter() {
- if (!_reorderCtx || _reorderCtx.active) return;
- _reorderCtx.active = true;
- const { container, songs } = _reorderCtx;
- container.classList.add('pl-draggable');
- // Block context-menu (long-press) on all items while reordering.
- container.addEventListener('contextmenu', _blockCtxMenu, true);
-
- let dragSrc = null;
- let touchDragSrc = null;
-
- Array.from(container.children).forEach((item, i) => {
- const handle = document.createElement('div');
- handle.className = 'pl-drag-handle';
- handle.innerHTML = '';
- item.insertBefore(handle, item.firstChild);
- item._songRef = songs[i];
-
- // ── Desktop drag ──
- let fromHandle = false;
- handle.addEventListener('mousedown', () => { fromHandle = true; item.draggable = true; });
- item.addEventListener('dragstart', e => {
- if (!fromHandle) { e.preventDefault(); item.draggable = false; return; }
- fromHandle = false; dragSrc = item;
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData('text/plain', '');
- setTimeout(() => item.classList.add('dragging'), 0);
- });
- item.addEventListener('dragend', () => {
- fromHandle = false; item.draggable = false; dragSrc = null;
- item.classList.remove('dragging');
- container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
- });
- item.addEventListener('dragover', e => {
- e.preventDefault(); e.dataTransfer.dropEffect = 'move';
- if (item === dragSrc) return;
- container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
- item.classList.add('drag-over');
- });
- item.addEventListener('dragleave', e => { if (!item.contains(e.relatedTarget)) item.classList.remove('drag-over'); });
- item.addEventListener('drop', e => {
- e.preventDefault(); e.stopPropagation();
- if (!dragSrc || item === dragSrc) return;
- item.classList.remove('drag-over');
- _doReorder(container, songs, dragSrc, item);
- });
-
- // ── Touch drag ──
- handle.addEventListener('touchstart', e => {
- e.preventDefault(); e.stopPropagation();
- touchDragSrc = item; item.classList.add('dragging');
- }, { passive: false });
- handle.addEventListener('touchmove', e => {
- if (!touchDragSrc) return;
- e.preventDefault();
- const t = e.touches[0];
- container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
- const over = _itemAtPoint(container, t.clientX, t.clientY, touchDragSrc);
- if (over) over.classList.add('drag-over');
- }, { passive: false });
- handle.addEventListener('touchend', e => {
- if (!touchDragSrc) return;
- const t = e.changedTouches[0];
- touchDragSrc.classList.remove('dragging');
- container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
- const over = _itemAtPoint(container, t.clientX, t.clientY, touchDragSrc);
- if (over) _doReorder(container, songs, touchDragSrc, over);
- touchDragSrc = null;
- }, { passive: false });
- });
-
- const bar = document.getElementById('pl-reorder-bar');
- if (bar) bar.style.display = 'flex';
-}
-
-function _blockCtxMenu(e) { e.preventDefault(); e.stopPropagation(); }
-
-function _itemAtPoint(container, x, y, exclude) {
- return (document.elementsFromPoint(x, y) || [])
- .find(el => el.classList?.contains('song-item') && el !== exclude && container.contains(el)) || null;
-}
-
-function _doReorder(container, songs, src, target) {
- const all = Array.from(container.children);
- const from = all.indexOf(src), to = all.indexOf(target);
- container.insertBefore(src, from < to ? target.nextSibling : target);
- const newOrder = Array.from(container.children).map(el => el._songRef);
- songs.splice(0, songs.length, ...newOrder);
- Array.from(container.children).forEach((el, i) => {
- const n = el.querySelector('.song-num'); if (n) n.textContent = i + 1;
- });
-}
-
-export function plReorderSave() { _exitReorderMode(true); }
-export function plReorderCancel() { _exitReorderMode(false); }
-
-function _exitReorderMode(save) {
- if (!_reorderCtx) return;
- const { container, songs, pl } = _reorderCtx;
- _reorderCtx.active = false;
- container.classList.remove('pl-draggable');
- container.removeEventListener('contextmenu', _blockCtxMenu, true);
- container.querySelectorAll('.pl-drag-handle').forEach(h => h.remove());
- const bar = document.getElementById('pl-reorder-bar');
- if (bar) bar.style.display = 'none';
- if (save) _savePlaylistOrder(pl, songs);
-}
-
-async function _savePlaylistOrder(pl, songs) {
- try {
- const songIds = songs.map(s => `&songId=${s.id}`).join('');
- await api('createPlaylist', `&playlistId=${pl.id}&name=${encodeURIComponent(pl.name)}${songIds}`);
- toast('✅ Reihenfolge gespeichert');
- } catch(e) { toast('Fehler beim Speichern: ' + e.message); }
-}
-
-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); }
-}
-
-// ─── STARRED / RANDOM ───
-let _starredSort = 'newest';
-
-export async function loadStarred(sort = _starredSort) {
- _starredSort = sort;
- document.querySelectorAll('#starred-sort-chips .sort-chip').forEach(c =>
- c.classList.toggle('active', c.dataset.sort === sort));
- const el = document.getElementById('starred-songs');
- el.innerHTML = '';
- const searchEl = document.getElementById('starred-search');
- if (searchEl) searchEl.value = '';
- try {
- const r = await api('getStarred2');
- let songs = (r.starred2?.song || []).map(s => ({...s, _starred: true}));
- if (sort === 'newest') songs.sort((a, b) => new Date(b.starred||0) - new Date(a.starred||0));
- else if (sort === 'oldest') songs.sort((a, b) => new Date(a.starred||0) - new Date(b.starred||0));
- else if (sort === 'az') songs.sort((a, b) => (a.title||'').localeCompare(b.title||''));
- else if (sort === 'artist') songs.sort((a, b) => (a.artist||'').localeCompare(b.artist||''));
- else if (sort === 'plays') songs.sort((a, b) => (b.playCount||0) - (a.playCount||0));
- el.innerHTML = '';
- if(!songs.length) { el.innerHTML = ''; return; }
- songs.forEach((s,i) => el.appendChild(makeSongItem(s, i, songs, { noArtistLink: true })));
- } catch(e) { el.innerHTML = `${e.message}
`; }
-}
-
-export function filterStarred(val) {
- const q = val.trim().toLowerCase();
- document.querySelectorAll('#starred-songs .song-item').forEach(item => {
- const title = (item.querySelector('.song-title')?.textContent || '').toLowerCase();
- const artist = (item.querySelector('.song-artist')?.textContent || '').toLowerCase();
- if (!q || title.includes(q) || artist.includes(q)) { item.style.display = ''; return; }
- const rm = romajiLookup(item.dataset.songId);
- item.style.display = ((rm?.titleRomaji||'').toLowerCase().includes(q) || (rm?.artistRomaji||'').toLowerCase().includes(q)) ? '' : 'none';
- });
-}
-
-export async function loadRandom() {
- const el = document.getElementById('random-songs');
- el.innerHTML = '';
- try {
- const r = await api('getRandomSongs', '&size=50');
- const songs = r.randomSongs?.song || [];
- el.innerHTML = '';
- songs.forEach((s,i) => el.appendChild(makeSongItem(s, i, songs)));
- } catch(e) { el.innerHTML = `${e.message}
`; }
-}
-
-// ─── ROMAJI → HIRAGANA (lazy-loaded) ───
-let _wanakana = null;
-async function _romajiToHiragana(q) {
- // Only attempt conversion if query is pure ASCII (likely romaji)
- if (!/^[a-z\s'-]+$/i.test(q.trim())) return null;
- try {
- if (!_wanakana) _wanakana = await import('https://cdn.jsdelivr.net/npm/wanakana@5/esm/wanakana.js');
- const h = _wanakana.toHiragana(q.trim(), { passRomaji: false });
- // If conversion produced actual hiragana (not just the same ASCII), use it
- return /[-ゟ]/.test(h) ? h : null;
- } catch { return null; }
-}
-
-function _mergeUniq(a, b) {
- const seen = new Set(a.map(x => x.id));
- return [...a, ...(b || []).filter(x => !seen.has(x.id))];
-}
-
-// ─── LOCAL SEARCH ───
-export function onLocalSearch(val) {
- clearTimeout(S.searchTimer);
- if(!val.trim()) { navigate('home'); return; }
- navigate('search');
- S.searchTimer = setTimeout(() => doLocalSearch(val), 350);
-}
-
-export async function doLocalSearch(q, el) {
- if (!el) el = document.getElementById('search-results');
- el.innerHTML = '';
- try {
- const hiragana = await _romajiToHiragana(q);
- const isRomaji = /^[a-z\s'-]+$/i.test(q.trim());
- const [r, r2, romajiIdx] = await Promise.all([
- api('search3', `&query=${encodeURIComponent(q)}&songCount=30&albumCount=10&artistCount=6`),
- hiragana ? api('search3', `&query=${encodeURIComponent(hiragana)}&songCount=20&albumCount=6&artistCount=4`) : Promise.resolve(null),
- isRomaji ? fetchRomajiIndex() : Promise.resolve(null),
- ]);
- const res = r.searchResult3 || {};
- const res2 = r2?.searchResult3 || {};
- el.innerHTML = '';
- const artists = _mergeUniq(res.artist||[], res2.artist);
- const albums = _mergeUniq(res.album ||[], res2.album);
-
- // Merge Navidrome results with romaji index matches
- const qLow = q.trim().toLowerCase();
- const romajiHits = romajiIdx
- ? romajiIdx.filter(e => e.r.includes(qLow))
- : [];
- const songs = _mergeUniq(_mergeUniq(res.song||[], res2.song), romajiHits);
-
- if(!artists.length && !albums.length && !songs.length) {
- el.innerHTML = ''; return;
- }
- if(artists.length) {
- const s = mkSection('Künstler');
- const g = document.createElement('div'); g.className = 'scroll-row';
- artists.forEach(a => {
- const c = makeCard(dispArtistById(a.id, a.name), a.albumCount+' Alben', null, '🎤');
- c.classList.add('artist-card');
- c.onclick = () => navigate('artist-detail', a);
- g.appendChild(c);
- getArtistImage(a.id).then(img => {
- if(!img) return;
- const artEl = c.querySelector('.card-art');
- if(!artEl) return;
- artEl.innerHTML = `
`;
- });
- });
- s.appendChild(g); el.appendChild(s);
- }
- if(albums.length) {
- const s = mkSection('Alben');
- const g = document.createElement('div'); g.className = 'scroll-row';
- albums.forEach(a => g.appendChild(makeAlbumCard(a)));
- s.appendChild(g); el.appendChild(s);
- }
- if(songs.length) {
- const sec = mkSection('Titel');
- const list = document.createElement('div'); list.className = 'song-list';
- songs.forEach((so,i) => list.appendChild(makeSongItem(so, i, songs)));
- sec.appendChild(list); el.appendChild(sec);
- }
- } catch(e) { el.innerHTML = `${e.message}
`; }
-}
-
-// ─── HEADER SEARCH (persistent inline bar) ───
-export function onHeaderSearch(val) {
- const overlay = document.getElementById('header-search-overlay');
- const clearBtn = document.getElementById('header-search-clear');
- if (clearBtn) clearBtn.style.display = val ? 'block' : 'none';
- clearTimeout(S._headerSearchTimer);
- if (!val.trim()) { if (overlay) overlay.style.display = 'none'; return; }
- if (overlay) {
- // position:fixed — anchor top to actual header bottom so it works with any header height
- const header = document.getElementById('mobile-header');
- if (header) overlay.style.top = header.offsetHeight + 'px';
- overlay.style.display = 'block';
- }
- const el = document.getElementById('header-search-results');
- S._headerSearchTimer = setTimeout(() => doLocalSearch(val, el), 300);
-}
-
-// ─── MOBILE SEARCH (legacy no-op — replaced by inline header search) ───
-export function onMobileSearch(val) { onHeaderSearch(val); }
-
-// ─── GLOBAL (ITUNES) SEARCH ───
-export function onGlobalSearch(val) {
- clearTimeout(S.globalSearchTimer);
- const el = document.getElementById('global-search-results');
- if(!val.trim()) { el.innerHTML = '🔍
Gib einen Suchbegriff ein
'; return; }
- el.innerHTML = '';
- S.globalSearchTimer = setTimeout(() => doGlobalSearch(val), 400);
-}
-
-export async function doGlobalSearch(q) {
- const el = document.getElementById('global-search-results');
- try {
- const [tracks, albums, artists] = await Promise.all([
- itunesSearch(q,'musicTrack',25),
- itunesSearch(q,'album',10),
- itunesSearch(q,'musicArtist',6)
- ]);
- el.innerHTML = '';
-
- const trackResults = tracks.filter(t => t.wrapperType === 'track');
- const albumResults = albums;
-
- if(artists.length) {
- const s = mkSection('Künstler');
- const row = document.createElement('div'); row.className = 'scroll-row';
- artists.forEach(a => {
- const c = makeCard(a.artistName, 'iTunes Künstler', null, '🎤');
- c.onclick = () => loadItunesArtistResults(a.artistId, a.artistName);
- row.appendChild(c);
- itunesSearch(a.artistName,'album',1).then(res => {
- if(!res[0]) return;
- const imgUrl = res[0].artworkUrl100?.replace('100x100','300x300');
- if(!imgUrl) return;
- const artEl = c.querySelector('.card-art'); if(!artEl) return;
- const img = document.createElement('img');
- img.src = imgUrl; img.loading = 'lazy'; img.style.cssText = 'width:100%;height:100%;object-fit:cover;transition:transform .4s';
- const overlay = artEl.querySelector('.play-overlay');
- artEl.innerHTML = ''; artEl.appendChild(img); if(overlay) artEl.appendChild(overlay);
- img.onerror = () => { artEl.innerHTML = '🎤
'; if(overlay) artEl.appendChild(overlay); };
- c.addEventListener('mouseenter', () => img.style.transform = 'scale(1.05)');
- c.addEventListener('mouseleave', () => img.style.transform = 'scale(1)');
- }).catch(() => {});
- });
- s.appendChild(row); el.appendChild(s);
- }
- if(albumResults.length) {
- const s = mkSection('Alben');
- const row = document.createElement('div'); row.className = 'scroll-row';
- albumResults.forEach(a => {
- const art = a.artworkUrl100?.replace('100x100','300x300');
- const c = makeCard(a.collectionName, a.artistName, art, '💿');
- c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); loadItunesAlbum(a); };
- c.querySelector('.dl-btn-overlay')?.remove();
- const dlOverlay = document.createElement('button');
- dlOverlay.className = 'dl-btn-overlay';
- dlOverlay.title = 'Auf Server laden';
- dlOverlay.innerHTML = ``;
- dlOverlay.onclick = (e) => { e.stopPropagation(); openDlModal({artistName:a.artistName,trackName:a.collectionName,artworkUrl100:a.artworkUrl100,collectionName:a.collectionName,isAlbum:true,itunesData:a}); };
- c.querySelector('.play-overlay').appendChild(dlOverlay);
- c.onclick = () => loadItunesAlbum(a);
- row.appendChild(c);
- });
- s.appendChild(row); el.appendChild(s);
- }
- if(trackResults.length) {
- const s = mkSection('Titel');
- const list = document.createElement('div'); list.className = 'song-list';
- trackResults.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, trackResults)));
- s.appendChild(list); el.appendChild(s);
- }
- if(!artists.length && !albumResults.length && !trackResults.length) {
- el.innerHTML = '🔍
Keine Ergebnisse gefunden
';
- }
- } catch(e) { el.innerHTML = `Suche fehlgeschlagen: ${e.message}
`; }
-}
-
-export async function loadItunesAlbum(album) {
- try {
- const tracks = await itunesLookupAlbum(album.collectionId);
- if(!tracks.length) { toast('Keine Tracks gefunden'); return; }
- const el = document.getElementById('global-search-results');
- const s = document.createElement('div'); s.className = 'search-section';
- s.innerHTML = `
-
-
-
${esc(album.collectionName)}
-
${esc(album.artistName)}
-
-
-
- Tracks
`;
- const list = document.createElement('div'); list.className = 'song-list';
- tracks.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, tracks)));
- s.appendChild(list);
- el.innerHTML = ''; el.appendChild(s);
- } catch(e) { toast('Fehler: ' + e.message); }
-}
-
-export function loadItunesArtistResults(artistId, artistName) {
- navigate('artist-detail', { id: null, name: artistName, itunesArtistId: artistId });
-}
-
-// ─── NEW RELEASES ───
-export async function loadNewReleases() {
- const albumEl = document.getElementById('nr-albums');
- const songEl = document.getElementById('nr-songs');
- albumEl.innerHTML = ''; songEl.innerHTML = '';
- try {
- const [albums, songs] = await Promise.all([itunesTopCharts(), itunesTopSongs()]);
- albumEl.innerHTML = '';
- albums.slice(0,30).forEach(a => {
- const art = a.artworkUrl100?.replace('100x100','300x300') || a.artworkUrl;
- const c = makeCard(a.name, a.artistName, art, '💿');
- c.onclick = () => itunesSearch(a.name+' '+a.artistName,'album',5).then(res => { if(res[0]) loadItunesAlbum(res[0]); });
- albumEl.appendChild(c);
- });
- songs.slice(0,40).forEach((s, i) => {
- const el = document.createElement('div'); el.className = 'gtrack-item';
- const art = s.artworkUrl?.replace('100x100','60x60');
- el.innerHTML = `
- ${art ? `

` : `
🎵
`}
-
-
${esc(s.name)}
-
${esc(s.artistName||'')}
-
- `;
- el.onclick = () => { itunesSearch(s.name+' '+s.artistName,'musicTrack',1).then(res => { if(res[0]?.previewUrl) playPreview(res[0]); }); };
- songEl.appendChild(el);
- });
- } catch(e) { albumEl.innerHTML = `${e.message}
`; }
-}
-
-// ─── RECOMMENDATIONS ───
-export async function loadRecommendations() {
- const el = document.getElementById('reco-content');
- const currentSong = S.queue[S.qIdx];
- if(!currentSong) {
- el.innerHTML = '🎵
Spiele zuerst Musik in deiner Bibliothek
';
- return;
- }
- el.innerHTML = '';
- try {
- const artist = currentSong.artist || '';
- const title = currentSong.title || '';
- let html = `
-
🎧
-
Basierend auf
${esc(title)}
${esc(artist)}
-
`;
- el.innerHTML = html + '';
- if(S.cfg.lastfmKey) {
- await loadLastfmRecommendations(el, artist, title, html);
- } else {
- await loadItunesFallbackRecommendations(el, artist, title, html);
- const note = document.createElement('div');
- note.style.cssText = 'margin-top:16px;padding:12px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;font-size:12px;color:var(--text3)';
- note.innerHTML = '💡 Tipp: Füge einen Last.fm API Key in den Einstellungen hinzu für bessere Empfehlungen & Similar Artists.';
- el.appendChild(note);
- }
- } catch(e) { el.innerHTML = `${e.message}
`; }
-}
-
-async function loadLastfmRecommendations(el, artist, title, html) {
- const BASE = 'https://ws.audioscrobbler.com/2.0';
- const key = S.cfg.lastfmKey;
- try {
- const [simArtists, topTracks, simTracks] = await Promise.all([
- fetch(`${BASE}/?method=artist.getSimilar&artist=${encodeURIComponent(artist)}&api_key=${key}&format=json&limit=12`).then(r=>r.json()),
- fetch(`${BASE}/?method=artist.getTopTracks&artist=${encodeURIComponent(artist)}&api_key=${key}&format=json&limit=15`).then(r=>r.json()),
- fetch(`${BASE}/?method=track.getSimilar&artist=${encodeURIComponent(artist)}&track=${encodeURIComponent(title)}&api_key=${key}&format=json&limit=15`).then(r=>r.json()),
- ]);
- el.innerHTML = html;
- const simArtistList = simArtists.similarartists?.artist || [];
- const topTrackList = topTracks.toptracks?.track || [];
- const simTrackList = simTracks.similartracks?.track || [];
- if(simArtistList.length) {
- el.innerHTML += `Ähnliche Künstler
`;
- el.innerHTML += simArtistList.map(a => ``).join('');
- el.innerHTML += '
';
- }
- if(simTrackList.length) {
- const sec = document.createElement('div');
- sec.innerHTML = `Ähnliche Tracks
`;
- const list = document.createElement('div'); list.className = 'song-list';
- simTrackList.slice(0,15).forEach(t => {
- const row = document.createElement('div'); row.className = 'gtrack-item';
- row.innerHTML = `
- ${esc(t.name)}
${esc(t.artist?.name||'')}
- `;
- row.onclick = () => searchAndPreview(t.name, t.artist?.name||'');
- list.appendChild(row);
- });
- sec.appendChild(list); el.appendChild(sec);
- }
- if(topTrackList.length) {
- const sec = document.createElement('div'); sec.style.marginTop = '20px';
- sec.innerHTML = `Top Tracks von ${esc(artist)}
`;
- const list = document.createElement('div'); list.className = 'song-list';
- topTrackList.slice(0,10).forEach(t => {
- const row = document.createElement('div'); row.className = 'gtrack-item';
- row.innerHTML = `
- ${esc(t.name)}
${esc(t.artist?.name||artist)}
- `;
- row.onclick = () => searchAndPreview(t.name, artist);
- list.appendChild(row);
- });
- sec.appendChild(list); el.appendChild(sec);
- }
- } catch { await loadItunesFallbackRecommendations(el, artist, title, html); }
-}
-
-async function loadItunesFallbackRecommendations(el, artist, title, html) {
- const results = await itunesSearch(artist, 'musicTrack', 20);
- el.innerHTML = html;
- const sec = document.createElement('div');
- sec.innerHTML = `Mehr von ${esc(artist)}
`;
- const list = document.createElement('div'); list.className = 'song-list';
- results.filter(t => t.artistName===artist).slice(0,15).forEach(t => list.appendChild(makeGlobalTrackItem(t, 0, [t])));
- if(!list.children.length) results.slice(0,15).forEach(t => list.appendChild(makeGlobalTrackItem(t, 0, [t])));
- sec.appendChild(list); el.appendChild(sec);
-}
-
-// ─── HELPERS ───
-export function expandArtistBio() {
- const el = document.getElementById('artist-bio-text');
- if(el && window._artistBioFull) el.textContent = window._artistBioFull;
-}
-
-export function searchSimilarArtist(artistName) {
- navigate('discover');
- document.getElementById('global-search-input').value = artistName;
- onGlobalSearch(artistName);
-}
-
-export async function searchAndPreview(trackName, artistName) {
- try {
- const res = await itunesSearch(trackName+' '+artistName, 'musicTrack', 1);
- if(res[0]?.previewUrl) playPreview(res[0]); else toast('Kein Preview verfügbar');
- } catch { toast('Suche fehlgeschlagen'); }
-}
-
-export async function s_goArtist(e, name, id) {
- if(!name) return;
- if(id) { navigate('artist-detail', {id, name}); return; }
- try {
- const r = await api('search3', `&query=${encodeURIComponent(name)}&artistCount=5&albumCount=0&songCount=0`);
- const artists = r.searchResult3?.artist || [];
- const match = artists.find(a => a.name.toLowerCase() === name.toLowerCase()) || artists[0];
- if(match) navigate('artist-detail', {id: match.id, name: match.name});
- else toast('Künstler nicht gefunden');
- } catch { toast('Suche fehlgeschlagen'); }
-}
-
-export function s_goAlbum(e, albumId, albumName, artist, artistId) {
- if(albumId) navigate('album-detail', {id:albumId, name:albumName, artist, artistId:artistId||'', coverArt:''});
-}
-
-export function playerBarGoAlbum() {
- const s = S.queue[S.qIdx];
- if(!s) return;
- if(S.npOpen) closeNowPlaying();
- if(s.albumId) navigate('album-detail', {id:s.albumId, name:s.album||'', artist:s.artist||'', artistId:s.artistId||'', coverArt:s.coverArt||''});
-}
-
-export function playerBarGoArtist() {
- const s = S.queue[S.qIdx];
- if(!s || !s.artist) return;
- if(S.npOpen) closeNowPlaying();
- navigate('artist-detail', {id: s.artistId||s.artist, name: s.artist});
-}
+// Barrel — re-exports everything so external imports (main.js, player.js, etc.)
+// continue to use a single entry point without path changes.
+export * from './navigation.js';
+export * from './home.js';
+export * from './detail.js';
+export * from './playlists.js';
+export * from './search.js';
+export * from './discover.js';