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, toggleStar, parseArtists } 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 { fetchRomajiIndex, dispArtistById, dispAlbumName, 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 ? `${esc(album.name)}` : ''}
${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; c.innerHTML = '
'; try { const r = await api(method, extra); 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'); g.innerHTML = '
'; try { const r = await api('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'); g.innerHTML = '
'; try { const r = await api('getAlbumList2', `&type=${type}&size=500`); 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.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'); 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, 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 = '
⭐

Noch keine Favoriten

'; 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 = '
🔍

Keine Ergebnisse

'; 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||'')}
#${i+1}
`; 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}); }