From 2a2e5ea4957e3c13ff0fd28637ab0686959c661d Mon Sep 17 00:00:00 2001 From: Honey Date: Sun, 7 Jun 2026 13:50:36 +0200 Subject: [PATCH] refactor: split views/views.js into 6 focused modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit navigation.js — navigate(), history, closeHeaderSearch, _navGoBack home.js — home page, genres, album rows, loadArtists/loadAlbums, getArtistImage detail.js — artist/album/iTunes detail pages, quickPlay helpers, expandArtistBio playlists.js — playlists, playlist detail, reorder, starred, random search.js — local/header/global search, iTunes helpers, s_goArtist, playerBarGoX discover.js — new releases, recommendations (Last.fm + iTunes fallback) views.js — barrel re-export; all external imports unchanged Co-Authored-By: Claude Sonnet 4.6 --- src/views/detail.js | 375 +++++++++++ src/views/discover.js | 138 ++++ src/views/home.js | 241 +++++++ src/views/navigation.js | 100 +++ src/views/playlists.js | 250 ++++++++ src/views/search.js | 252 ++++++++ src/views/views.js | 1354 +-------------------------------------- 7 files changed, 1364 insertions(+), 1346 deletions(-) create mode 100644 src/views/detail.js create mode 100644 src/views/discover.js create mode 100644 src/views/home.js create mode 100644 src/views/navigation.js create mode 100644 src/views/playlists.js create mode 100644 src/views/search.js 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 = `
${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; + + 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), + ]); + + 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; + const rw = _normWords(r.artistName); + return [..._queryWords].every(w => rw.has(w)); + }); + const itunesArtistId = knownItunes ?? iArtistMatch?.artistId ?? null; + + 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); + } + } + + 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||'')}
+
+
+ #${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); +} 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 ? `${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 {} +} + +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 = '
⭐

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}

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

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 ─── +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 ? `${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; - 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 = '
⭐

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}); -} +// 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';