import { S } from './state.js';
import { esc, formatDur, toast } from './utils.js';
import { api, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, itunesTopCharts, itunesTopSongs } from './api.js';
// Circular imports — safe (only used inside function bodies)
import { playQueue, dlSongLocal, playPreview } from './player.js';
import { showCtxMenu, openEditPlModal, openDlModal, closeSidebar, switchNavMode } from './ui.js';
import { closeNowPlaying, closeNpMenu } from './nowplaying.js';
import { makeCard, makeAlbumCard, makeSongItem, makeGlobalTrackItem, mkSection } from './components.js';
import { loadSpotifyImporter } from './spotify.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 navigate(page, data=null) {
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==='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();
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} 👋`;
loadAlbumRow('home-recent', 'getAlbumList2', '&type=recent&size=20');
loadAlbumRow('home-random', 'getAlbumList2', '&type=random&size=20');
loadAlbumRow('home-newest', 'getAlbumList2', '&type=newest&size=20');
loadAlbumRow('home-frequent', 'getAlbumList2', '&type=frequent&size=20');
}
export async function loadAlbumRow(id, method, extra) {
const c = document.getElementById(id);
if(!c) return;
c.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) => {
const cv = coverUrl(s.coverArt, 60);
const row = document.createElement('div'); row.className = 'topsong-item';
row.innerHTML = `${i+1}
${cv ? `

` : '
🎵
'}
${esc(s.title)}
${esc(s.album||'')}
${s.playCount ? s.playCount.toLocaleString() : '—'}
`;
row.onclick = () => playQueue(topSongs, i);
row.addEventListener('contextmenu', e => { e.preventDefault(); showCtxMenu(e, s, topSongs, i); });
list.appendChild(row);
});
tsEl.appendChild(list);
if(ndAlbums.length) {
const total = ndAlbums.reduce((n, a) => n + (a.songCount || 0), 0);
const btn = document.createElement('button');
btn.className = 'btn-outline'; btn.style.cssText = 'font-size:12px;padding:7px 14px;margin-bottom:16px;margin-top:4px';
btn.textContent = `Alle ${total} Songs in Library`;
btn.onclick = () => 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.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','300x300') || coverUrl(nd.coverArt, 300);
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);
}
const cv = coverUrl(s.coverArt, 60);
const row = document.createElement('div'); row.className = 'topsong-item';
row.innerHTML = `${s.trackNumber||''}
${cv?`

`:'
🎵
'}
${s.playCount?s.playCount.toLocaleString():''}
`;
row.onclick = () => playQueue(allSongs, i);
row.addEventListener('contextmenu', e => { e.preventDefault(); showCtxMenu(e, s, allSongs, i); });
el.appendChild(row);
});
}
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 = album.name;
const artistEl = document.getElementById('album-detail-artist');
const albumArtistParts = (album.artist || '').split(/\s*(?:\/|•)\s*/).filter(Boolean);
artistEl.innerHTML = albumArtistParts.map((name, i) => {
const aid = i === 0 ? (album.artistId || '') : '';
return `${esc(name)}`;
}).join(' • ') || '';
const cw = document.getElementById('album-cover-wrap');
const cv = coverUrl(album.coverArt, 300);
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.`;
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, { noAlbumLink: true })));
} catch(e) { sl.innerHTML = `${e.message}
`; }
}
// ─── PLAYLISTS ───
export async function loadPlaylists() {
const g = document.getElementById('playlists-grid');
g.innerHTML = '';
try {
const r = await api('getPlaylists');
const lists = r.playlists?.playlist || [];
g.innerHTML = '';
if(!lists.length) { g.innerHTML = '🎶
Keine Playlists vorhanden
'; return; }
lists.forEach(pl => {
const cv = coverUrl(pl.coverArt, 200);
const c = makeCard(pl.name, pl.songCount+' Titel', cv, '🎶');
c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayPlaylist(pl); };
c.querySelector('.card-info').onclick = () => navigate('playlist-detail', pl);
g.appendChild(c);
});
} 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, 300);
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 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 = '';
sl.classList.add('pl-draggable');
songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs, { noArtistLink: true, noAlbumLink: true })));
_initPlaylistDragDrop(sl, songs, S._currentPlaylist);
} catch(e) { sl.innerHTML = `${e.message}
`; }
}
function _initPlaylistDragDrop(container, songs, pl) {
let dragSrc = null;
let saveTimer = 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];
item.draggable = true; // set upfront so browser registers it before any event
item.addEventListener('dragstart', e => {
if (!e.target.closest('.pl-drag-handle')) { e.preventDefault(); return; }
dragSrc = item;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', ''); // required by Firefox
setTimeout(() => item.classList.add('dragging'), 0);
});
item.addEventListener('dragend', () => {
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');
const allItems = Array.from(container.children);
const fromIdx = allItems.indexOf(dragSrc);
const toIdx = allItems.indexOf(item);
container.insertBefore(dragSrc, fromIdx < toIdx ? item.nextSibling : item);
// Sync songs array to new DOM order
const newOrder = Array.from(container.children).map(el => el._songRef);
songs.splice(0, songs.length, ...newOrder);
// Update track numbers
Array.from(container.children).forEach((el, idx) => {
const n = el.querySelector('.song-num');
if (n) n.textContent = idx + 1;
});
// Debounced save to Navidrome
clearTimeout(saveTimer);
saveTimer = setTimeout(() => _savePlaylistOrder(pl, songs), 800);
});
});
}
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 ───
export async function loadStarred() {
const el = document.getElementById('starred-songs');
el.innerHTML = '';
try {
const r = await api('getStarred2');
const songs = (r.starred2?.song || []).map(s => ({...s,_starred:true}));
el.innerHTML = '';
if(!songs.length) { el.innerHTML = ''; return; }
songs.forEach((s,i) => el.appendChild(makeSongItem(s, i, songs)));
} catch(e) { el.innerHTML = `${e.message}
`; }
}
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}
`; }
}
// ─── 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) {
const el = document.getElementById('search-results');
el.innerHTML = '';
try {
const r = await api('search3', `&query=${encodeURIComponent(q)}&songCount=30&albumCount=10&artistCount=6`);
const res = r.searchResult3 || {};
el.innerHTML = '';
const artists = res.artist||[], albums = res.album||[], songs = res.song||[];
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(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 s = mkSection('Titel');
const g = document.createElement('div'); g.className = 'scroll-row';
songs.forEach((so,i) => {
const cv = coverUrl(so.coverArt, 200);
const c = makeCard(so.title, so.artist||'', cv, '🎵');
c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); playQueue(songs, i); };
c.onclick = () => playQueue(songs, i);
g.appendChild(c);
});
s.appendChild(g); el.appendChild(s);
}
} catch(e) { el.innerHTML = `${e.message}
`; }
}
// ─── MOBILE SEARCH ───
export function onMobileSearch(val) {
clearTimeout(S._mobileSearchTimer);
if(!val.trim()) { navigate('home'); return; }
navigate('search');
S._mobileSearchTimer = setTimeout(() => doLocalSearch(val), 300);
}
// ─── 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});
}