// ─── MD5 (correct implementation) ─── function md5(input) { function safeAdd(x,y){var l=(x&0xffff)+(y&0xffff);return((x>>16)+(y>>16)+(l>>16)<<16)|(l&0xffff)} function rol(n,c){return(n<>>32-c)} function cmn(q,a,b,x,s,t){return safeAdd(rol(safeAdd(safeAdd(a,q),safeAdd(x,t)),s),b)} function ff(a,b,c,d,x,s,t){return cmn((b&c)|(~b&d),a,b,x,s,t)} function gg(a,b,c,d,x,s,t){return cmn((b&d)|(c&~d),a,b,x,s,t)} function hh(a,b,c,d,x,s,t){return cmn(b^c^d,a,b,x,s,t)} function ii(a,b,c,d,x,s,t){return cmn(c^(b|~d),a,b,x,s,t)} // Convert string to little-endian word array var l=input.length, m=[]; for(var i=0;i>2]|=(input.charCodeAt(i)&0xff)<<((i%4)*8); m[l>>2]|=0x80<<((l%4)*8); m[((l+8>>6)+1)*16-2]=l*8; // Initial hash values var a=1732584193,b=-271733879,c=-1732584194,d=271733878; for(var i=0;i>>(j*8))&0xff).toString(16)).slice(-2);return s} return hex(a)+hex(b)+hex(c)+hex(d); } // ─── STATE ─── let cfg={url:'',user:'',pass:'',lastfmKey:'',backendUrl:''}; let queue=[],qIdx=-1,shuffle=false,repeat=false; let searchTimer=null,globalSearchTimer=null; let downloadTarget=null,dlType='track'; let jobs={}; const audio=document.getElementById('audio-element'); const previewAudio=document.getElementById('preview-audio'); // ─── SETUP TABS ─── function switchSetupTab(t){ document.querySelectorAll('.setup-tab').forEach((el,i)=>el.classList.toggle('active',i===(t==='navidrome'?0:1))); document.querySelectorAll('.setup-pane').forEach((el,i)=>el.classList.toggle('active',i===(t==='navidrome'?0:1))); } // ─── SUBSONIC API ─── function apiUrl(method,extra=''){ const salt=Math.random().toString(36).slice(2,10); const token=md5(cfg.pass+salt); return `${cfg.url}/rest/${method}?u=${encodeURIComponent(cfg.user)}&t=${token}&s=${salt}&v=1.16.1&c=NovaSonic&f=json${extra}`; } async function api(method,extra=''){ const res=await fetch(apiUrl(method,extra)); if(!res.ok) throw new Error(`HTTP ${res.status}`); const data=await res.json(); const r=data['subsonic-response']; if(r.status!=='ok') throw new Error(r.error?.message||'API Error'); return r; } function streamUrl(id){return apiUrl('stream',`&id=${id}&format=raw`)} function downloadUrl(id){return apiUrl('download',`&id=${id}`)} function coverUrl(id,size=300){return id?apiUrl('getCoverArt',`&id=${id}&size=${size}`):null} // ─── CONNECT ─── async function doConnect(){ const url=document.getElementById('srv-url').value.trim().replace(/\/$/,''); const user=document.getElementById('srv-user').value.trim(); const pass=document.getElementById('srv-pass').value; const lfKey=document.getElementById('lastfm-key').value.trim(); const backUrl=document.getElementById('backend-url').value.trim().replace(/\/$/,''); if(!url||!user||!pass){showSetupError('Bitte Server-URL, Benutzernamen und Passwort ausfüllen.');return} const btn=document.getElementById('btn-connect'); btn.disabled=true;btn.textContent='Verbinde…'; document.getElementById('setup-error').style.display='none'; cfg={url,user,pass,lastfmKey:lfKey,backendUrl:backUrl}; try{ await api('ping'); localStorage.setItem('ns_cfg',JSON.stringify(cfg)); startApp(); }catch(e){ cfg={url:'',user:'',pass:'',lastfmKey:'',backendUrl:''}; showSetupError('Verbindung fehlgeschlagen: '+e.message); btn.disabled=false;btn.textContent='Verbinden'; } } function showSetupError(m){const el=document.getElementById('setup-error');el.textContent=m;el.style.display='block'} function disconnect(){ localStorage.removeItem('ns_cfg'); localStorage.removeItem('ns_last_played'); sessionStorage.removeItem('ns_nav'); cfg={url:'',user:'',pass:'',lastfmKey:'',backendUrl:''}; queue=[];qIdx=-1;_navStack=[];_navCursor=-1; audio.pause();audio.src=''; document.getElementById('app').style.display='none'; document.getElementById('setup-screen').style.display='flex'; } // ─── INIT ─── window.addEventListener('load',()=>{ const saved=localStorage.getItem('ns_cfg'); if(saved){cfg=JSON.parse(saved);startApp()} // Autofill URLs from Docker env (set via config.js) if(window.NS_SERVER_URL?.trim()){const el=document.getElementById('srv-url');if(el&&!el.value)el.value=window.NS_SERVER_URL.trim();} if(window.NS_BACKEND_URL?.trim()){const el=document.getElementById('backend-url');if(el&&!el.value)el.value=window.NS_BACKEND_URL.trim();} document.getElementById('srv-pass').addEventListener('keydown',e=>{if(e.key==='Enter')doConnect()}); audio.addEventListener('timeupdate',updateProgress); audio.addEventListener('ended',onEnded); audio.addEventListener('play',()=>{setPlayIcon(true);document.getElementById('player-art').classList.add('spinning');if(navigator.mediaSession)navigator.mediaSession.playbackState='playing';}); audio.addEventListener('pause',()=>{setPlayIcon(false);document.getElementById('player-art').classList.remove('spinning');if(navigator.mediaSession)navigator.mediaSession.playbackState='paused';}); previewAudio.addEventListener('timeupdate',updatePreviewProgress); previewAudio.addEventListener('ended',stopPreview); audio.volume=0.8; injectPWA(); initProgressScrub(); // Now Playing: click player-art to open const pa=document.getElementById('player-art'); if(pa)pa.addEventListener('click', openNowPlaying); // Swipe-up on player bar to open Now Playing const playerBar=document.querySelector('.player'); if(playerBar){ let _sy=0,_st=0; playerBar.addEventListener('touchstart',e=>{ if(e.target.closest('button')||e.target.closest('.progress-bar')||e.target.closest('.volume-wrap'))return; _sy=e.touches[0].clientY;_st=Date.now(); },{passive:true}); playerBar.addEventListener('touchend',e=>{ if(!_sy)return; const dy=_sy-e.changedTouches[0].clientY; const dt=Date.now()-_st; if(dy>40||(dt<300&&dy>15))openNowPlaying(); _sy=0; },{passive:true}); } }); async function startApp(){ document.getElementById('setup-screen').style.display='none'; document.getElementById('app').style.display='flex'; // Restore last navigation position (survives soft refresh) if (_navLoad() && _navCursor >= 0 && _navStack[_navCursor]) { const last = _navStack[_navCursor]; _navJumping = true; navigate(last.page, last.data); _navJumping = false; } else { navigate('home'); } loadHomeData(); // Restore last played song (survives tab close) _restoreLastPlayed(); // Restore radio mode state if(localStorage.getItem('ns_radio')==='1'){radioMode=true;_applyRadioBtn();} } function _restoreLastPlayed() { try { const saved = JSON.parse(localStorage.getItem('ns_last_played') || 'null'); if (!saved || !saved.song) return; // Restore queue state silently (don't auto-play, just show in player bar) queue = saved.queue || [saved.song]; qIdx = saved.qIdx || 0; const s = queue[qIdx]; if (!s) return; // Update player UI without starting playback document.getElementById('player-title').textContent = s.title || '—'; document.getElementById('player-artist-name').textContent = s.artist || '—'; document.getElementById('player-album-sep').style.display = s.album ? '' : 'none'; document.getElementById('player-album-name').textContent = s.album || ''; const art = document.getElementById('player-art'); const cv = coverUrl(s.coverArt, 100); art.innerHTML = cv ? `` : '
♪
'; updateStarBtn(); document.title = `${s.title} — NovaSonic`; } catch {} } // ─── NAVIGATION HISTORY ───────────────────────────────────────── // Cursor-based stack: never destructively pop — just move the cursor. // Stored in sessionStorage so soft reloads survive; tabs start fresh. let _navStack = []; // [{page, data}, ...] let _navCursor = -1; // points to current entry let _navJumping = false; // true while we're programmatically changing cursor function _navSave() { try { sessionStorage.setItem('ns_nav', JSON.stringify({stack:_navStack, cursor:_navCursor})); } catch {} } function _navLoad() { try { const s = JSON.parse(sessionStorage.getItem('ns_nav') || 'null'); if (s && Array.isArray(s.stack) && s.stack.length) { _navStack = s.stack; _navCursor = s.cursor; return true; } } catch {} return false; } function navigate(page, data=null) { // ── Core page switch ── 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'].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'].includes(page))switchNavMode('discover'); else if(['home','artists','albums','starred','playlists'].includes(page))switchNavMode('library'); // ── Load data ── 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==='album-detail' && data) loadAlbumDetail(data); if (page==='new-releases') loadNewReleases(); if (page==='playlists') loadPlaylists(); if (page==='playlist-detail' && data) loadPlaylistDetail(data); if (page==='recommendations') loadRecommendations(); // ── History stack ── if (!_navJumping) { // Truncate any forward entries (user navigated after going back) _navStack = _navStack.slice(0, _navCursor + 1); // Avoid duplicate consecutive identical entries const safe = data ? JSON.parse(JSON.stringify(data)) : null; const last = _navStack[_navCursor]; if (!last || last.page !== page || JSON.stringify(last.data) !== JSON.stringify(safe)) { _navStack.push({ page, data: safe }); } _navCursor = _navStack.length - 1; _navSave(); // Keep ONE real browser history entry ahead so popstate fires on back try { history.pushState({ nsIdx: _navCursor }, '', ''); } catch {} } } function _navGoBack() { // Dismiss overlays first (sheet → NP → then real back) if (document.getElementById('np-sheet')?.classList.contains('open')) { closeNpMenu(); return; } if (npOpen) { closeNowPlaying(); return; } if (_navCursor > 0) { _navCursor--; const entry = _navStack[_navCursor]; _navSave(); _navJumping = true; navigate(entry.page, entry.data); _navJumping = false; } // Always keep a pushState ahead so next back also fires popstate try { history.pushState({ nsIdx: _navCursor }, '', ''); } catch {} } window.addEventListener('popstate', () => _navGoBack()); document.addEventListener('keydown', e => { if (e.key !== 'Backspace') return; const t = document.activeElement?.tagName; if (t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT') return; e.preventDefault(); _navGoBack(); }); // Seed an initial browser history entry so the very first back is caught try { history.replaceState({ nsIdx: 0 }, '', ''); } catch {} async function loadArtistDetail(artist) { document.getElementById('artist-detail-name').textContent = artist.name; document.getElementById('artist-back-btn').onclick = () => navigate('artists'); const hero = document.getElementById('artist-detail-hero'); hero.style.background = 'linear-gradient(135deg, var(--surface2), var(--surface))'; // Clear Last.fm sections ['artist-bio-section','artist-similar-section','artist-topsongs-section'].forEach(id=>{ const el=document.getElementById(id); if(el)el.innerHTML=''; }); const g = document.getElementById('artist-albums-grid'); g.innerHTML = '
'; // Parallel: image + albums + last.fm info const [imgRes, albumRes, infoRes, topRes] = await Promise.allSettled([ getArtistImage(artist.id), api('getArtist', `&id=${artist.id}`), api('getArtistInfo2', `&id=${artist.id}&count=8`), api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`) ]); // Hero image const img = imgRes.status==='fulfilled' ? imgRes.value : null; if (img) { hero.style.background = `url('${img}') center/cover no-repeat`; hero.querySelector('.artist-hero-overlay').style.background = 'linear-gradient(to right,rgba(7,7,15,.9) 0%,rgba(7,7,15,.5) 60%,rgba(7,7,15,.2) 100%)'; } // Albums if (albumRes.status==='fulfilled') { g.innerHTML = ''; (albumRes.value.artist?.album || []).forEach(a=>g.appendChild(makeAlbumCard({...a,_fromArtist:artist}))); } else { g.innerHTML = `

${albumRes.reason?.message}

`; } // ── Last.fm: bio const info = infoRes.status==='fulfilled' ? infoRes.value.artistInfo2 : null; if (info?.biography) { const bioEl = document.getElementById('artist-bio-section'); // Strip Last.fm HTML links, keep plain text const bio = info.biography.replace(/]+>/g,'').replace(/<\/a>/g,'').replace(/<[^>]+>/g,'').trim(); const short = bio.length > 400 ? bio.slice(0,400)+'…' : bio; if (short && bioEl) { window._artistBioFull = bio; bioEl.innerHTML = `
Über
${esc(short)}${bio.length>400 ?` ` :''}
`; } } // ── Last.fm: similar artists const similar = info?.similarArtist || []; if (similar.length) { const simEl = document.getElementById('artist-similar-section'); if (simEl) { simEl.innerHTML = `
Ähnliche Künstler
${similar.map(a=> ` ${esc(a.name)} `).join('')}
`; } } // ── Last.fm / Navidrome: top songs const topSongs = topRes.status==='fulfilled' ? (topRes.value.topSongs?.song||[]) : []; if (topSongs.length) { const tsEl = document.getElementById('artist-topsongs-section'); if (tsEl) { tsEl.innerHTML = `
Top Songs
`; const list = document.createElement('div'); list.style.marginBottom='20px'; topSongs.forEach((s,i)=>{ const cv = coverUrl(s.coverArt, 60); const row = document.createElement('div'); row.className='topsong-item'; row.innerHTML=`
${i+1}
${cv?``:'
🎵
'}
${esc(s.title)}
${esc(s.album||'')}
${s.playCount?s.playCount.toLocaleString():'—'}
`; row.onclick=()=>playQueue(topSongs,i); row.addEventListener('contextmenu',e=>{e.preventDefault();showCtxMenu(e,s,topSongs,i)}); list.appendChild(row); }); tsEl.appendChild(list); } } } async function loadAlbums(){ const g=document.getElementById('albums-grid');g.innerHTML='
'; try{const r=await api('getAlbumList2','&type=alphabeticalByName&size=500');g.innerHTML='';(r.albumList2?.album||[]).forEach(a=>g.appendChild(makeAlbumCard(a)))} catch(e){g.innerHTML=`

${e.message}

`} } async function loadAlbumDetail(album){ document.getElementById('album-back-btn').onclick=()=>album._fromArtist?navigate('artist-detail',album._fromArtist):navigate('albums'); document.getElementById('album-detail-name').textContent=album.name; const artistEl=document.getElementById('album-detail-artist');artistEl.textContent=album.artist||''; artistEl.onclick=()=>{if(album.artistId)navigate('artist-detail',{id:album.artistId,name:album.artist})}; const cw=document.getElementById('album-cover-wrap'); const cv=coverUrl(album.coverArt,300); cw.innerHTML=cv?``:'
💿
'; const sl=document.getElementById('album-songs');sl.innerHTML='
'; try{ const r=await api('getAlbum',`&id=${album.id}`); const songs=r.album?.song||[]; const totalMin=Math.round(songs.reduce((s,t)=>s+(t.duration||0),0)/60); document.getElementById('album-detail-stats').textContent=`${songs.length} Titel · ${r.album.year||''} · ${totalMin} Min.`; document.getElementById('btn-play-album').onclick=()=>playQueue(songs,0); document.getElementById('btn-dl-album').onclick=()=>{songs.forEach((s,i)=>setTimeout(()=>dlSongLocal(s),i*400));toast(`⬇ ${songs.length} Titel Download gestartet`)}; sl.innerHTML='';songs.forEach((s,i)=>sl.appendChild(makeSongItem(s,i,songs))); }catch(e){sl.innerHTML=`

${e.message}

`} } // ─── PLAYLISTS ─── async function loadPlaylists(){ const g=document.getElementById('playlists-grid');g.innerHTML='
'; try{ const r=await api('getPlaylists'); const lists=r.playlists?.playlist||[]; g.innerHTML=''; if(!lists.length){g.innerHTML='
🎶

Keine Playlists vorhanden

';return} lists.forEach(pl=>{ const cv=coverUrl(pl.coverArt,200); const c=makeCard(pl.name,pl.songCount+' Titel',cv,'🎶'); c.querySelector('.play-btn').onclick=(e)=>{e.stopPropagation();quickPlayPlaylist(pl)}; c.onclick=()=>navigate('playlist-detail',pl); g.appendChild(c); }); }catch(e){g.innerHTML=`

${e.message}

`} } async function quickPlayPlaylist(pl){ try{const r=await api('getPlaylist',`&id=${pl.id}`);const songs=r.playlist?.entry||[];playQueue(songs,0)} catch(e){toast('Fehler: '+e.message)} } let _currentPlaylist=null; async function loadPlaylistDetail(pl){ _currentPlaylist={...pl}; document.getElementById('playlist-detail-name').textContent=pl.name; const cw=document.getElementById('playlist-cover'); const cv=coverUrl(pl.coverArt,300); cw.innerHTML=cv?``:'
🎶
'; document.getElementById('btn-edit-playlist').onclick=()=>openEditPlModal(_currentPlaylist); const sl=document.getElementById('playlist-songs');sl.innerHTML='
'; try{ const r=await api('getPlaylist',`&id=${pl.id}`); const full=r.playlist||{}; _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.`; 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='';songs.forEach((s,i)=>sl.appendChild(makeSongItem(s,i,songs))); }catch(e){sl.innerHTML=`

${e.message}

`} } // ─── EDIT PLAYLIST ─── function openEditPlModal(pl){ const overlay=document.getElementById('edit-pl-overlay'); document.getElementById('edit-pl-name').value=pl.name||''; document.getElementById('edit-pl-desc').value=pl.comment||''; const tog=document.getElementById('edit-pl-public-toggle'); tog.classList.toggle('on',!!pl.public); const cv=coverUrl(pl.coverArt,300); document.getElementById('edit-pl-cover').innerHTML=cv ?`` :'
🎶
'; overlay.classList.add('open'); } function closeEditPlModal(){document.getElementById('edit-pl-overlay').classList.remove('open')} async function saveEditPlaylist(){ const pl=_currentPlaylist;if(!pl)return; const name=document.getElementById('edit-pl-name').value.trim(); if(!name){toast('Name darf nicht leer sein');return;} const comment=document.getElementById('edit-pl-desc').value.trim(); const pub=document.getElementById('edit-pl-public-toggle').classList.contains('on'); const btn=document.querySelector('.edit-pl-btn-save'); btn.disabled=true;btn.textContent='Speichern…'; try{ await api('updatePlaylist',`&playlistId=${pl.id}&name=${encodeURIComponent(name)}&comment=${encodeURIComponent(comment)}&public=${pub}`); _currentPlaylist={..._currentPlaylist,name,comment,public:pub}; document.getElementById('playlist-detail-name').textContent=name; closeEditPlModal(); toast('Playlist aktualisiert'); }catch(e){toast('Fehler: '+e.message)} finally{btn.disabled=false;btn.textContent='Speichern';} } function editPlUploadCover(){document.getElementById('edit-pl-file').click()} async function editPlCoverSelected(input){ const file=input.files?.[0];if(!file||!_currentPlaylist)return; input.value=''; const preview=URL.createObjectURL(file); document.getElementById('edit-pl-cover').innerHTML=``; try{ const form=new FormData();form.append('image',file); const auth='Basic '+btoa(`${cfg.user}:${cfg.pass}`); const res=await fetch(`${cfg.url}/api/playlist/${_currentPlaylist.id}/image`,{method:'POST',headers:{Authorization:auth},body:form}); if(!res.ok)throw new Error(res.status); toast('Cover aktualisiert'); // Refresh cover in detail view setTimeout(()=>{ const cv=document.getElementById('playlist-cover'); const img=cv?.querySelector('img');if(img)img.src=img.src.split('?')[0]+'?t='+Date.now(); },500); }catch{ toast('Cover-Upload nicht unterstützt (Navidrome-Version)'); document.getElementById('edit-pl-cover').innerHTML= (coverUrl(_currentPlaylist.coverArt,300) ?`` :'
🎶
'); } } async function editPlRemoveCover(){ if(!_currentPlaylist)return; try{ const auth='Basic '+btoa(`${cfg.user}:${cfg.pass}`); const res=await fetch(`${cfg.url}/api/playlist/${_currentPlaylist.id}/image`,{method:'DELETE',headers:{Authorization:auth}}); if(!res.ok)throw new Error(res.status); toast('Cover entfernt'); document.getElementById('edit-pl-cover').innerHTML='
🎶
'; }catch{toast('Entfernen nicht unterstützt (Navidrome-Version)')} } // ─── RADIO MODE (endloser Shuffle aus der eigenen Bibliothek) ─── let radioMode=false; function _applyRadioBtn(){ const btn=document.getElementById('btn-radio-mode'); if(btn){btn.classList.toggle('active',radioMode);btn.style.color=radioMode?'var(--blue)':'';} const nb=document.getElementById('np-radio-btn'); if(nb){nb.classList.toggle('active',radioMode);nb.style.color=radioMode?'var(--blue)':'';} } async function toggleRadioMode(){ radioMode=!radioMode; localStorage.setItem('ns_radio',radioMode?'1':'0'); _applyRadioBtn(); if(radioMode){ await fillRadioQueue(); } } async function fillRadioQueue(){ if(!radioMode)return; try{ const r=await api('getRandomSongs','&size=20'); const newSongs=r.randomSongs?.song||[]; // Append to current queue so playback doesn't interrupt queue.push(...newSongs); renderQueue(); }catch{} } // Override onEnded to refill queue in radio mode const _origOnEnded=window.onEnded; async function loadStarred(){ const el=document.getElementById('starred-songs');el.innerHTML='
'; try{ const r=await api('getStarred2'); const songs=(r.starred2?.song||[]).map(s=>({...s,_starred:true})); el.innerHTML=''; if(!songs.length){el.innerHTML='
⭐

Noch keine Favoriten

';return} songs.forEach((s,i)=>el.appendChild(makeSongItem(s,i,songs))); }catch(e){el.innerHTML=`

${e.message}

`} } async function loadRandom(){ const el=document.getElementById('random-songs');el.innerHTML='
'; try{const r=await api('getRandomSongs','&size=50');const songs=r.randomSongs?.song||[];el.innerHTML='';songs.forEach((s,i)=>el.appendChild(makeSongItem(s,i,songs)))} catch(e){el.innerHTML=`

${e.message}

`} } // ─── LOCAL SEARCH ─── function onLocalSearch(val){ clearTimeout(searchTimer); if(!val.trim()){navigate('home');return} navigate('search'); searchTimer=setTimeout(()=>doLocalSearch(val),350); } async function doLocalSearch(q){ const el=document.getElementById('search-results');el.innerHTML='
'; try{ const r=await api('search3',`&query=${encodeURIComponent(q)}&songCount=30&albumCount=10&artistCount=6`); const res=r.searchResult3||{};el.innerHTML=''; const artists=res.artist||[],albums=res.album||[],songs=res.song||[]; if(!artists.length&&!albums.length&&!songs.length){el.innerHTML='
🔍

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(a.name,a.albumCount+' Alben',null,'🎤');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)artEl.innerHTML=`
`})});s.appendChild(g);el.appendChild(s)} if(albums.length){const s=mkSection('Alben');const g=document.createElement('div');g.className='scroll-row';albums.forEach(a=>g.appendChild(makeAlbumCard(a)));s.appendChild(g);el.appendChild(s)} if(songs.length){const s=mkSection('Titel');const g=document.createElement('div');g.className='scroll-row';songs.forEach((so,i)=>{const cv=coverUrl(so.coverArt,200);const c=makeCard(so.title,so.artist||'',cv,'🎵');c.querySelector('.play-btn').onclick=(e)=>{e.stopPropagation();playQueue(songs,i)};c.onclick=()=>playQueue(songs,i);g.appendChild(c)});s.appendChild(g);el.appendChild(s)} }catch(e){el.innerHTML=`

${e.message}

`} } function mkSection(title){const d=document.createElement('div');d.className='search-section';d.innerHTML=`
${title}
`;return d} function toggleMobileSearch(){ const bar=document.getElementById('mobile-search-bar'); const btn=document.getElementById('mobile-search-btn'); const open=bar.style.display==='none'||bar.style.display===''; bar.style.display=open?'block':'none'; btn.style.color=open?'var(--accent)':'var(--text2)'; if(open){setTimeout(()=>document.getElementById('mobile-search-input').focus(),50);} else{closeMobileSearch();} } function closeMobileSearch(){ const bar=document.getElementById('mobile-search-bar'); const inp=document.getElementById('mobile-search-input'); const btn=document.getElementById('mobile-search-btn'); bar.style.display='none'; btn.style.color='var(--text2)'; inp.value=''; navigate('home'); } let _mobileSearchTimer=null; function onMobileSearch(val){ clearTimeout(_mobileSearchTimer); if(!val.trim()){navigate('home');return;} navigate('search'); _mobileSearchTimer=setTimeout(()=>doLocalSearch(val),300); } // ─── ITUNES API ─── const ITUNES='https://itunes.apple.com'; async function itunesSearch(query,entity='musicTrack',limit=25){ const r=await fetch(`${ITUNES}/search?term=${encodeURIComponent(query)}&media=music&entity=${entity}&limit=${limit}&country=de`); const d=await r.json();return d.results||[]; } async function itunesLookupAlbum(collectionId){ const r=await fetch(`${ITUNES}/lookup?id=${collectionId}&entity=song`); const d=await r.json();return(d.results||[]).filter(x=>x.wrapperType==='track'); } async function itunesTopCharts(){ // Try Apple RSS (DE then US), then fall back to iTunes Search API const urls=['https://rss.applemarketingtools.com/api/v2/de/music/most-played/50/albums.json','https://rss.applemarketingtools.com/api/v2/us/music/most-played/50/albums.json']; for(const url of urls){try{const r=await fetch(url,{signal:AbortSignal.timeout(6000)});if(!r.ok)continue;const d=await r.json();if(d.feed?.results?.length)return d.feed.results;}catch{}} try{ const r=await fetch('https://itunes.apple.com/search?term=new+music&media=music&entity=album&limit=50&country=de'); const d=await r.json(); return(d.results||[]).map(a=>({name:a.collectionName,artistName:a.artistName,artworkUrl:a.artworkUrl100?.replace('100x100','300x300'),artworkUrl100:a.artworkUrl100})); }catch{return[]} } async function itunesTopSongs(){ const urls=['https://rss.applemarketingtools.com/api/v2/de/music/most-played/50/songs.json','https://rss.applemarketingtools.com/api/v2/us/music/most-played/50/songs.json']; for(const url of urls){try{const r=await fetch(url,{signal:AbortSignal.timeout(6000)});if(!r.ok)continue;const d=await r.json();if(d.feed?.results?.length)return d.feed.results;}catch{}} try{ const r=await fetch('https://itunes.apple.com/search?term=top+hits&media=music&entity=musicTrack&limit=50&country=de'); const d=await r.json(); return(d.results||[]).map(t=>({name:t.trackName,artistName:t.artistName,artworkUrl:t.artworkUrl100})); }catch{return[]} } // ─── GLOBAL SEARCH ─── function onGlobalSearch(val){ clearTimeout(globalSearchTimer); const el=document.getElementById('global-search-results'); if(!val.trim()){el.innerHTML='
🔍

Gib einen Suchbegriff ein

';return} el.innerHTML='
'; globalSearchTimer=setTimeout(()=>doGlobalSearch(val),400); } 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=''; 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); // Lazy-load: grab artwork from their first album 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'; // Keep overlay 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(); // Add download overlay button 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(tracks.length){ const s=mkSection('Titel'); const list=document.createElement('div');list.className='song-list'; tracks.forEach((t,i)=>list.appendChild(makeGlobalTrackItem(t,i,tracks))); s.appendChild(list);el.appendChild(s); } if(!artists.length&&!albums.length&&!tracks.length){ el.innerHTML='
🔍

Keine Ergebnisse gefunden

'; } }catch(e){el.innerHTML=`

Suche fehlgeschlagen: ${e.message}

`} } async function loadItunesAlbum(album){ try{ const tracks=await itunesLookupAlbum(album.collectionId); if(!tracks.length){toast('Keine Tracks gefunden');return} // Show in a mini overlay list in discover page 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)} } async function loadItunesArtistResults(artistId,artistName){ const el=document.getElementById('global-search-results'); el.innerHTML='
'; try{ const albums=await itunesSearch(artistName,'album',20); const artistAlbums=albums.filter(a=>a.artistId===artistId||a.artistName===artistName); el.innerHTML=''; const header=document.createElement('div'); header.innerHTML=`
${esc(artistName)}
Alben
`; el.appendChild(header); const grid=document.createElement('div');grid.className='grid grid-4'; (artistAlbums.length?artistAlbums:albums).forEach(a=>{ const art=a.artworkUrl100?.replace('100x100','300x300'); const c=makeCard(a.collectionName,a.artistName,art,'💿'); c.onclick=()=>loadItunesAlbum(a); grid.appendChild(c); }); el.appendChild(grid); }catch(e){el.innerHTML=`

${e.message}

`} } function makeGlobalTrackItem(t,idx,list){ const el=document.createElement('div');el.className='gtrack-item'; const art=t.artworkUrl60||t.artworkUrl100||''; el.innerHTML=`
${art?``:'
🎵
'}
${esc(t.trackName||t.collectionName||'—')}
${esc(t.artistName||'—')} · ${esc(t.collectionName||'')} ${t.releaseDate?'· '+new Date(t.releaseDate).getFullYear():''}
${t.previewUrl?``:''}
`; el.onclick=()=>{if(t.previewUrl)playPreview(t)}; return el; } // ─── NEW RELEASES ─── 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 itTrack={trackName:s.name,artistName:s.artistName,artworkUrl60:s.artworkUrl?.replace('100x100','60x60'),collectionName:''}; const el=document.createElement('div');el.className='gtrack-item'; el.innerHTML=`
${itTrack.artworkUrl60?``:`
🎵
`}
${esc(s.name)}
${esc(s.artistName||'')}
#${i+1}
`; el.onclick=()=>{itunesSearch(s.name+' '+s.artistName,'musicTrack',1).then(res=>{if(res[0]&&res[0].previewUrl)playPreview(res[0])})} songEl.appendChild(el); }); }catch(e){albumEl.innerHTML=`

${e.message}

`} } // ─── RECOMMENDATIONS (Last.fm + iTunes fallback) ─── async function loadRecommendations(){ const el=document.getElementById('reco-content'); const currentSong=queue[qIdx]; if(!currentSong){ el.innerHTML='
🎵

Spiele zuerst Musik in deiner Bibliothek

'; return; } el.innerHTML='
'; try{ let html=''; const artist=currentSong.artist||''; const title=currentSong.title||''; html+=`
🎧
Basierend auf
${esc(title)}
${esc(artist)}
`; el.innerHTML=html+'
'; // Try Last.fm first if(cfg.lastfmKey){ await loadLastfmRecommendations(el,artist,title,html); } else { await loadItunesFallbackRecommendations(el,artist,title,html); el.querySelector('#reco-nolastfm') && null; 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=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(e){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); } async function searchSimilarArtist(artistName){ navigate('discover'); document.getElementById('global-search-input').value=artistName; onGlobalSearch(artistName); } async function searchAndPreview(trackName,artistName){ try{const res=await itunesSearch(trackName+' '+artistName,'musicTrack',1);if(res[0]&&res[0].previewUrl)playPreview(res[0]);else toast('Kein Preview verfügbar')} catch{toast('Suche fehlgeschlagen')} } // ─── PREVIEW PLAYER ─── let previewCurrent=null; function playPreview(track){ if(!track.previewUrl){toast('Kein Preview verfügbar');return} // Pause main player, remember if it was playing so we can resume later const wasPlaying=!audio.paused; if(wasPlaying) audio.pause(); previewCurrent=track; previewCurrent._resumeMain=wasPlaying; // Inherit current volume from main volume slider previewAudio.volume=audio.volume; previewAudio.src=track.previewUrl; previewAudio.play(); document.getElementById('preview-title').innerHTML=esc(track.trackName||'—')+'30s Preview'; document.getElementById('preview-sub').textContent=(track.artistName||'')+(track.collectionName?' · '+track.collectionName:''); const art=track.artworkUrl100||track.artworkUrl60||''; document.getElementById('preview-art').innerHTML=art?``:'
♪
'; document.getElementById('preview-bar').classList.add('visible'); document.getElementById('preview-fill').style.width='0%'; } function stopPreview(resume){ const shouldResume = resume !== false && previewCurrent?._resumeMain; previewAudio.pause();previewAudio.src=''; document.getElementById('preview-bar').classList.remove('visible'); document.getElementById('preview-fill').style.width='0%'; previewCurrent=null; // Resume main player if it was playing before preview started if(shouldResume && audio.src) audio.play(); } function updatePreviewProgress(){ if(!previewAudio.duration)return; document.getElementById('preview-fill').style.width=(previewAudio.currentTime/previewAudio.duration*100)+'%'; } // ─── DOWNLOAD MODAL ─── function openDlModal(track){ if(!cfg.backendUrl){toast('⚠ Kein Backend konfiguriert. Bitte in Einstellungen eintragen.');return} downloadTarget=track;dlType='track'; document.querySelectorAll('.modal-opt').forEach(o=>o.classList.toggle('selected',o.dataset.type==='track')); const info=document.getElementById('dl-modal-info'); const art=track.artworkUrl100||track.artworkUrl60||''; info.innerHTML=`${art?``:''}
${esc(track.trackName||track.collectionName||'—')}
${esc(track.artistName||'—')}${track.collectionName?' · '+esc(track.collectionName):''}
`; document.getElementById('dl-album-sub').textContent=track.collectionName?`"${track.collectionName}" herunterladen`:'Album herunterladen'; document.getElementById('download-modal').classList.add('open'); } function closeDlModal(){document.getElementById('download-modal').classList.remove('open')} function selectDlType(el,type){ dlType=type; document.querySelectorAll('.modal-opt').forEach(o=>o.classList.remove('selected')); el.classList.add('selected'); } async function executeDownload(){ if(!downloadTarget||!cfg.backendUrl)return; closeDlModal(); const jobId='job_'+Date.now(); const payload={ type:dlType, artist:downloadTarget.artistName||'Unknown Artist', title:downloadTarget.trackName||downloadTarget.collectionName||'Unknown', album:downloadTarget.collectionName||'', artworkUrl:downloadTarget.artworkUrl100||downloadTarget.artworkUrl60||'', itunesId:downloadTarget.trackId||downloadTarget.collectionId||null, collectionId:downloadTarget.collectionId||null }; addJob(jobId,payload.title,payload.artist); try{ const res=await fetch(`${cfg.backendUrl}/api/download`,{ method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({...payload,jobId}) }); if(!res.ok)throw new Error(`HTTP ${res.status}`); pollJob(jobId); }catch(e){ updateJob(jobId,'error','Verbindung zum Backend fehlgeschlagen'); toast('❌ Backend nicht erreichbar: '+e.message); } } // ─── JOBS ─── function addJob(id,title,artist){ jobs[id]={title,artist,progress:0,status:'pending'}; renderJobs(); } function updateJob(id,status,msg,progress){ if(!jobs[id])return; jobs[id]={...jobs[id],status,msg:msg||'',progress:progress??jobs[id].progress}; renderJobs(); if(status==='done'){setTimeout(()=>{delete jobs[id];renderJobs()},5000)} if(status==='error'){setTimeout(()=>{delete jobs[id];renderJobs()},8000)} } function renderJobs(){ const panel=document.getElementById('jobs-panel'); panel.innerHTML=''; Object.entries(jobs).forEach(([id,j])=>{ const c=document.createElement('div');c.className='job-card'; const isDone=j.status==='done',isErr=j.status==='error'; const statusText=isErr?'Fehler':isDone?'✓ Fertig! Bibliothek wird gescannt…':j.msg||'Wird heruntergeladen…'; c.innerHTML=`
${esc(j.title)}
${esc(j.artist)}
${statusText}
`; panel.appendChild(c); }); } async function pollJob(jobId){ if(!jobs[jobId])return; try{ const res=await fetch(`${cfg.backendUrl}/api/status/${jobId}`); const d=await res.json(); updateJob(jobId,d.status,d.message,d.progress); if(d.status!=='done'&&d.status!=='error')setTimeout(()=>pollJob(jobId),1500); else if(d.status==='done'){toast(`✅ ${jobs[jobId]?.title||'Track'} gespeichert!`)} }catch{updateJob(jobId,'error','Status-Abfrage fehlgeschlagen')} } // ─── LOCAL DOWNLOAD (from Navidrome) ─── function dlSongLocal(s){ const a=document.createElement('a');a.href=downloadUrl(s.id); a.download=`${s.artist||'Unknown'} - ${s.title}.${s.suffix||'mp3'}`; document.body.appendChild(a);a.click();document.body.removeChild(a); toast(`⬇ ${s.title}`); } function downloadCurrent(){if(qIdx>=0&&queue[qIdx])dlSongLocal(queue[qIdx])} // ─── PLAYER ─── function playQueue(songs,startIdx){queue=songs.map(s=>({...s}));qIdx=startIdx;playCurrent();renderQueue();if(radioMode)fillRadioQueue();} function playCurrent(){ if(qIdx<0||qIdx>=queue.length)return; const s=queue[qIdx]; audio.src=streamUrl(s.id);audio.play(); updatePlayerUI(s);renderQueue(); } function updatePlayerUI(s){ document.getElementById('player-title').textContent=s.title; document.getElementById('player-artist-name').textContent=s.artist||'—'; document.getElementById('player-album-sep').style.display=s.album?'':'none'; document.getElementById('player-album-name').textContent=s.album||''; const art=document.getElementById('player-art'); const cv=coverUrl(s.coverArt,100); art.innerHTML=cv?``:'
♪
'; updateStarBtn(); document.title=`${s.title} — NovaSonic`; updateMediaSession(s); if(document.getElementById('page-recommendations').classList.contains('active'))loadRecommendations(); // Save last played to localStorage try { localStorage.setItem('ns_last_played', JSON.stringify({ song: {id:s.id,title:s.title,artist:s.artist,album:s.album,albumId:s.albumId, artistId:s.artistId,coverArt:s.coverArt,duration:s.duration, _starred:s._starred||false}, queue: queue.map(t=>({id:t.id,title:t.title,artist:t.artist,album:t.album, albumId:t.albumId,artistId:t.artistId,coverArt:t.coverArt, duration:t.duration,_starred:t._starred||false})), qIdx })); } catch {} // Sync NP overlay (direct DOM, no recursion) _syncNpDirect(s); } function _syncNpDirect(s){ if(!npOpen||!s)return; const cv=coverUrl(s.coverArt,400); const cover=document.getElementById('np-cover'); const bg=document.getElementById('np-bg'); if(cover)cover.innerHTML=cv?``:'
♪
'; if(bg)bg.style.backgroundImage=cv?`url('${cv}')`:'none'; const t=document.getElementById('np-title'); if(t)t.textContent=s.title||'—'; const an=document.getElementById('np-artist-name');if(an)an.textContent=s.artist||'—'; const asep=document.getElementById('np-album-sep');if(asep)asep.style.display=s.album?'':'none'; const aln=document.getElementById('np-album-name');if(aln)aln.textContent=s.album||''; syncNpStar(); } function updateMediaSession(s){ if(!navigator.mediaSession)return; const artwork=s.coverArt?[{src:coverUrl(s.coverArt,96),sizes:'96x96',type:'image/jpeg'},{src:coverUrl(s.coverArt,300),sizes:'300x300',type:'image/jpeg'},{src:coverUrl(s.coverArt,512),sizes:'512x512',type:'image/jpeg'}]:[]; navigator.mediaSession.metadata=new MediaMetadata({title:s.title,artist:s.artist||'',album:s.album||'',artwork}); navigator.mediaSession.setActionHandler('play',togglePlay); navigator.mediaSession.setActionHandler('pause',togglePlay); navigator.mediaSession.setActionHandler('previoustrack',prevSong); navigator.mediaSession.setActionHandler('nexttrack',nextSong); navigator.mediaSession.setActionHandler('seekbackward',d=>{audio.currentTime=Math.max(0,audio.currentTime-(d.seekOffset||10))}); navigator.mediaSession.setActionHandler('seekforward',d=>{audio.currentTime=Math.min(audio.duration||0,audio.currentTime+(d.seekOffset||10))}); navigator.mediaSession.setActionHandler('seekto',d=>{if(d.seekTime!=null)audio.currentTime=d.seekTime}); } let _msPosLast=0; function syncMediaSessionPosition(){ if(!navigator.mediaSession?.setPositionState||!audio.duration||isNaN(audio.duration))return; const now=Date.now(); if(now-_msPosLast<1000)return; _msPosLast=now; try{navigator.mediaSession.setPositionState({duration:audio.duration,playbackRate:audio.playbackRate||1,position:Math.min(audio.currentTime,audio.duration)});}catch{} } function togglePlay(){if(audio.src)audio.paused?audio.play():audio.pause()} function setPlayIcon(playing){ document.getElementById('play-icon').innerHTML=playing?'':''; // Sync NP overlay play icon (no recursion - direct DOM update) if(npOpen){ document.getElementById('np-play-icon').innerHTML=playing?'':''; const cover=document.getElementById('np-cover'); if(cover)cover.classList.toggle('playing',playing); } } function prevSong(){if(audio.currentTime>3){audio.currentTime=0;return}if(qIdx>0){qIdx--;playCurrent()}} function nextSong(){ if(shuffle){qIdx=Math.floor(Math.random()*queue.length)} else if(qIdx{if(qIdx{ document.getElementById('time-cur').textContent=formatDur(pos*audio.duration); } ); _initBarScrub( document.getElementById('np-bar'), document.getElementById('np-fill'), pos=>{ const el=document.getElementById('np-cur'); if(el)el.textContent=formatDur(pos*audio.duration); } ); } function _initBarScrub(bar,fill,onMove){ if(!bar||!fill)return; function getPos(clientX){const r=bar.getBoundingClientRect();return Math.max(0,Math.min(1,(clientX-r.left)/r.width));} function startScrub(pos){_scrubbing=true;fill.style.transition='none';fill.style.width=(pos*100)+'%';onMove(pos);} function moveScrub(pos){fill.style.width=(pos*100)+'%';onMove(pos);} function endScrub(pos){_scrubbing=false;fill.style.transition='';if(audio.duration)audio.currentTime=pos*audio.duration;} bar.addEventListener('touchstart',e=>{startScrub(getPos(e.touches[0].clientX));e.stopPropagation();},{passive:true}); bar.addEventListener('touchmove',e=>{if(_scrubbing)moveScrub(getPos(e.touches[0].clientX));},{passive:true}); bar.addEventListener('touchend',e=>{if(_scrubbing)endScrub(getPos(e.changedTouches[0].clientX));},{passive:true}); bar.addEventListener('mousedown',e=>{ startScrub(getPos(e.clientX));e.preventDefault(); const mm=e2=>{if(_scrubbing)moveScrub(getPos(e2.clientX));}; const mu=e2=>{if(_scrubbing)endScrub(getPos(e2.clientX));document.removeEventListener('mousemove',mm);document.removeEventListener('mouseup',mu);}; document.addEventListener('mousemove',mm);document.addEventListener('mouseup',mu); }); } function setVolume(v){audio.volume=v/100;previewAudio.volume=v/100} // ─── STAR ─── async function toggleStar(song,btn){ try{ await api(song._starred?'unstar':'star',`&id=${song.id}`); song._starred=!song._starred; if(btn){ btn.classList.toggle('starred',song._starred); const svg=btn.querySelector('svg'); if(svg){svg.setAttribute('fill',song._starred?'currentColor':'none')} } if(queue[qIdx]?.id===song.id)updateStarBtn(); }catch(e){toast('Fehler: '+e.message)} } function starCurrent(){if(qIdx<0||!queue[qIdx])return;toggleStar(queue[qIdx],document.getElementById('btn-star-current'))} function updateStarBtn(){ const s=queue[qIdx]; const icon=document.getElementById('star-icon'); if(!icon)return; const isStarred=s&&(s._starred||s.starred); icon.setAttribute('fill', isStarred ? 'var(--accent)' : 'none'); icon.setAttribute('stroke', isStarred ? 'var(--accent)' : 'var(--text3)'); } // ─── QUEUE ─── function renderQueue(){ const el=document.getElementById('queue-list');el.innerHTML=''; if(!queue.length){el.innerHTML='
🎵

Leer

';return} queue.forEach((s,i)=>{ const item=document.createElement('div');item.className='queue-item'+(i===qIdx?' active':''); item.innerHTML=`
${i+1}
${esc(s.title)}
${esc(s.artist||'')}
`; item.onclick=()=>{qIdx=i;playCurrent()};el.appendChild(item); }); el.querySelector('.active')?.scrollIntoView({block:'nearest'}); } let queueOpen=false; function toggleQueue(){queueOpen=!queueOpen;document.getElementById('queue-panel').classList.toggle('open',queueOpen);if(queueOpen)renderQueue()} // ─── SETTINGS ─── function openSettings(){ document.getElementById('s-url').value=cfg.url||''; document.getElementById('s-user').value=cfg.user||''; document.getElementById('s-pass').value=''; document.getElementById('s-lastfm').value=cfg.lastfmKey||''; document.getElementById('s-backend').value=cfg.backendUrl||''; document.getElementById('settings-modal').classList.add('open'); checkBackend(); } function closeSettings(){document.getElementById('settings-modal').classList.remove('open')} function saveSettings(){ const pass=document.getElementById('s-pass').value; cfg.url=document.getElementById('s-url').value.trim().replace(/\/$/,''); cfg.user=document.getElementById('s-user').value.trim(); if(pass)cfg.pass=pass; cfg.lastfmKey=document.getElementById('s-lastfm').value.trim(); cfg.backendUrl=document.getElementById('s-backend').value.trim().replace(/\/$/,''); localStorage.setItem('ns_cfg',JSON.stringify(cfg)); closeSettings();toast('✅ Gespeichert'); } async function checkBackend(){ const dot=document.getElementById('backend-status-dot'); const txt=document.getElementById('backend-status-text'); const url=document.getElementById('s-backend').value.trim()||cfg.backendUrl; if(!url){dot.className='status-dot';txt.textContent='Keine URL konfiguriert';return} dot.className='status-dot';txt.textContent='Verbinde…'; try{ const r=await fetch(`${url}/api/health`,{signal:AbortSignal.timeout(4000)}); const d=await r.json(); dot.className='status-dot ok';txt.textContent=`Verbunden · yt-dlp ${d.ytdlpVersion||'OK'} · ${d.musicPath||''}`; }catch(e){dot.className='status-dot err';txt.textContent='Nicht erreichbar: '+e.message} } // ─── CARD / ITEM MAKERS ─── function makeCard(title,sub,imgUrl,fallback='💿'){ const c=document.createElement('div');c.className='card'; c.innerHTML=`
${imgUrl?``:`
${fallback}
`}
${esc(title)}
${esc(sub||'')}
`; return c; } function makeAlbumCard(a){ const cv=coverUrl(a.coverArt,200); const c=makeCard(a.name,a.artist||'',cv,'💿'); c.querySelector('.play-btn').onclick=(e)=>{e.stopPropagation();quickPlayAlbum(a)}; c.onclick=()=>navigate('album-detail',a); return c; } function makeSongItem(s,idx,list){ const el=document.createElement('div'); if(s.starred&&!s._starred)s._starred=true; el.className='song-item'+(queue[qIdx]?.id===s.id?' playing':''); const cv=coverUrl(s.coverArt,60); el.innerHTML=`
${idx+1}
${cv?``:'
♪
'}
${esc(s.title)}
${s.artistId||s.artist?`${esc(s.artist||'')}`:''} ${s.albumId||s.album?` · ${esc(s.album||'')}`:''}
${formatDur(s.duration)}
`; const btns=el.querySelectorAll('.icon-btn'); btns[0].onclick=(e)=>{e.stopPropagation();toggleStar(s,btns[0])}; btns[1].onclick=(e)=>{e.stopPropagation();dlSongLocal(s)}; el.onclick=()=>playQueue(list,idx); el.addEventListener('contextmenu',e=>{e.preventDefault();showCtxMenu(e,s,list,idx)}); return el; } async function quickPlayAlbum(a){ try{const r=await api('getAlbum',`&id=${a.id}`);playQueue(r.album?.song||[],0)} catch(e){toast('Fehler: '+e.message)} } // Song list: navigate to artist or album from song row function s_goArtist(e,name,id){ if(!name)return; navigate('artist-detail',{id:id||name,name}); } function s_goAlbum(e,albumId,albumName,artist,artistId){ if(albumId){ navigate('album-detail',{id:albumId,name:albumName,artist,artistId:artistId||'',coverArt:''}); } } // ─── HOME ─── async function loadHomeData(){ loadAlbumRow('home-random','getAlbumList2','&type=random&size=20'); loadAlbumRow('home-newest','getAlbumList2','&type=newest&size=20'); loadAlbumRow('home-frequent','getAlbumList2','&type=frequent&size=20'); } async function loadAlbumRow(id,method,extra){ const c=document.getElementById(id); if(!c)return; c.innerHTML='
'; try{ const r=await api(method,extra); c.innerHTML=''; (r.albumList2?.album||[]).forEach(a=>c.appendChild(makeAlbumCard(a))); }catch(e){c.innerHTML=`
${e.message}
`} } // ─── ARTISTS ─── const artistImgCache={}; async function getArtistImage(artistId){ if(artistImgCache[artistId]!==undefined)return 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/')); artistImgCache[artistId]=isBlank?null:img; return artistImgCache[artistId]; }catch{artistImgCache[artistId]=null;return null;} } async function loadArtists(){ const g=document.getElementById('artists-grid'); g.innerHTML='
'; try{ const r=await api('getArtists'); const all=(r.artists?.index||[]).flatMap(i=>i.artist||[]); g.innerHTML=''; all.forEach(a=>{ const c=makeCard(a.name,a.albumCount+' Alben',null,'🎤'); 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}

`} } // ─── PWA ─── function injectPWA(){ if (!('serviceWorker' in navigator)) return; // Listen before registering so we never miss a message navigator.serviceWorker.addEventListener('message', e => { if (e.data?.type === 'SW_UPDATED') { localStorage.setItem('sw-version', e.data.version); showUpdateBanner(e.data.version); } // Version query response — covers the race where SW activated before this listener if (e.data?.type === 'VERSION') { const prev = localStorage.getItem('sw-version'); if (prev && prev !== e.data.version) showUpdateBanner(e.data.version); localStorage.setItem('sw-version', e.data.version); } }); // updateViaCache:'none' prevents the browser HTTP cache from hiding SW file changes navigator.serviceWorker.register('sw.js', { updateViaCache: 'none' }).then(reg => { reg.update().catch(() => {}); // Query active SW version right now to catch updates that happened before page JS ran const ctrl = navigator.serviceWorker.controller; if (ctrl) ctrl.postMessage({ type: 'GET_VERSION' }); }).catch(() => {}); } function showUpdateBanner(version) { if (document.getElementById('update-banner')) return; const b = document.createElement('div'); b.id = 'update-banner'; b.className = 'update-banner'; b.innerHTML = ` ✦ Update verfügbar${version ? ' ('+version+')' : ''} `; document.body.appendChild(b); } // ─── NP ACTION SHEET ───────────────────────────────────────────── function openNpMenu() { const s = queue[qIdx]; if (!s) return; // Populate song header const cv = coverUrl(s.coverArt, 80); document.getElementById('np-sheet-song').innerHTML = `
${cv ? `` : '
🎵
'}
${esc(s.title)}
${esc(s.artist||'')}${s.album?' · '+esc(s.album):''}
`; // Actions const items = [ { icon: '', label: 'Song-Info anzeigen', fn: () => { closeNpMenu(); showSongInfo(s); } }, { icon: '', label: 'Zu Playlist hinzufügen', fn: () => { closeNpMenu(); openPlModal([s]); } }, { icon: '', label: 'Zum Künstler', fn: () => { closeNpMenu(); npGoArtist(); } }, { icon: '', label: 'Zum Album', fn: () => { closeNpMenu(); closeNowPlaying(); if(s.albumId)navigate('album-detail',{id:s.albumId,name:s.album,artist:s.artist,artistId:s.artistId,coverArt:s.coverArt}); } }, { icon: '', label: s._starred||s.starred ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen', fn: () => { closeNpMenu(); npToggleStar(); } }, { icon: '', label: 'Herunterladen', fn: () => { closeNpMenu(); dlSongLocal(s); } }, ]; document.getElementById('np-sheet-items').innerHTML = items.map((item,i) => `
${item.icon}${item.label}
` ).join(''); document.querySelectorAll('.np-sheet-item').forEach((el,i) => { el.addEventListener('click', () => items[i].fn()); }); document.getElementById('np-sheet-overlay').classList.add('open'); document.getElementById('np-sheet').classList.add('open'); } function closeNpMenu() { document.getElementById('np-sheet-overlay').classList.remove('open'); document.getElementById('np-sheet').classList.remove('open'); } // ─── PLAYER BAR clicks ─────────────────────────────────────────── function playerBarGoAlbum() { const s = queue[qIdx]; if (!s) return; if (npOpen) closeNowPlaying(); if (s.albumId) navigate('album-detail', {id:s.albumId, name:s.album||'', artist:s.artist||'', artistId:s.artistId||'', coverArt:s.coverArt||''}); } function playerBarGoArtist() { const s = queue[qIdx]; if (!s || !s.artist) return; if (npOpen) closeNowPlaying(); navigate('artist-detail', {id:s.artistId||s.artist, name:s.artist}); } // ─── HELPERS ─── function formatDur(s){if(!s)return'0:00';const m=Math.floor(s/60),sec=Math.floor(s%60);return`${m}:${sec.toString().padStart(2,'0')}`} function esc(s){return(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"')} function toast(msg){const t=document.createElement('div');t.className='toast';t.textContent=msg;const c=document.getElementById('toast-container');c.appendChild(t);setTimeout(()=>t.remove(),3000)} function openSidebar(){document.getElementById('sidebar').classList.add('mobile-open');document.getElementById('mobile-overlay').classList.add('active')} function closeSidebar(){document.getElementById('sidebar').classList.remove('mobile-open');document.getElementById('mobile-overlay').classList.remove('active')} function switchNavMode(mode){ const lib=document.getElementById('bnav-library'); const disc=document.getElementById('bnav-discover'); if(!lib||!disc)return; lib.style.display=mode==='library'?'flex':'none'; disc.style.display=mode==='discover'?'flex':'none'; document.getElementById('bnav-tab-library')?.classList.toggle('active',mode==='library'); document.getElementById('bnav-tab-discover')?.classList.toggle('active',mode==='discover'); } // ─── CONTEXT MENU ─────────────────────────────────────────────── let cachedPlaylists = []; function showCtxMenu(e, song, list, idx) { const menu = document.getElementById('ctx-menu'); menu.innerHTML = ''; menu.style.display = 'block'; const items = [ { icon: '▶', label: 'Abspielen', fn: () => playQueue(list, idx) }, { icon: '+', label: 'Als Nächstes einfügen', fn: () => { queue.splice(qIdx+1, 0, {...song}); renderQueue(); toast('In Warteschlange eingefügt'); }}, { icon: '⬇', label: 'Am Ende der Queue', fn: () => { queue.push({...song}); renderQueue(); toast('Zur Queue hinzugefügt'); }}, { sep: true }, { icon: '🎶', label: 'Zu Playlist hinzufügen →', submenu: true, fn: () => openPlModal([song]) }, { icon: '⭐', label: song._starred ? 'Aus Favoriten entfernen' : 'Zu Favoriten', fn: () => { const starBtn = menu._songEl?.querySelector('.icon-btn'); // toggle via API directly api(song._starred ? 'unstar' : 'star', `&id=${song.id}`) .then(() => { song._starred = !song._starred; toast(song._starred ? '⭐ Favorit hinzugefügt' : 'Favorit entfernt'); }) .catch(err => toast('Fehler: ' + err.message)); }}, { sep: true }, { icon: 'ℹ', label: 'Song-Info', fn: () => showSongInfo(song) }, { icon: '⬇', label: 'Herunterladen', fn: () => dlSongLocal(song) }, ]; items.forEach(item => { if (item.sep) { const d=document.createElement('div'); d.className='ctx-separator'; menu.appendChild(d); return; } const el = document.createElement('div'); el.className = 'ctx-item'; el.innerHTML = `${item.icon}${item.label}`; el.onclick = () => { hideCtxMenu(); item.fn(); }; menu.appendChild(el); }); // Position: keep inside viewport const vw = window.innerWidth, vh = window.innerHeight; let x = e.clientX, y = e.clientY; menu.style.left = '-9999px'; menu.style.top = '-9999px'; menu.style.display = 'block'; const w = menu.offsetWidth, h = menu.offsetHeight; if (x + w > vw - 8) x = vw - w - 8; if (y + h > vh - 8) y = vh - h - 8; menu.style.left = x + 'px'; menu.style.top = y + 'px'; } function hideCtxMenu() { const m = document.getElementById('ctx-menu'); m.style.display = 'none'; m.innerHTML = ''; } document.addEventListener('click', e => { if (!document.getElementById('ctx-menu').contains(e.target)) hideCtxMenu(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') hideCtxMenu(); }); // ─── PLAYLIST MODAL (Add to / Create) ──────────────────────────── let plModalSongs = []; async function openPlModal(songs) { plModalSongs = songs; document.getElementById('pl-modal-title').textContent = songs.length === 1 ? `"${songs[0].title}" zu Playlist` : `${songs.length} Songs zu Playlist`; document.getElementById('pl-new-name').value = ''; const list = document.getElementById('pl-list'); list.innerHTML = '
Lädt…
'; document.getElementById('pl-modal').classList.add('open'); try { const r = await api('getPlaylists'); cachedPlaylists = r.playlists?.playlist || []; list.innerHTML = ''; if (!cachedPlaylists.length) { list.innerHTML = '
Noch keine Playlists vorhanden
'; return; } cachedPlaylists.forEach(pl => { const row = document.createElement('div'); row.className = 'pl-row'; row.innerHTML = ` ${esc(pl.name)}${pl.songCount} Titel`; row.onclick = () => addSongsToPlaylist(pl.id, pl.name); list.appendChild(row); }); } catch(e) { list.innerHTML = `
${e.message}
`; } } function closePlModal() { document.getElementById('pl-modal').classList.remove('open'); plModalSongs = []; } async function addSongsToPlaylist(plId, plName) { closePlModal(); try { // Subsonic API: updatePlaylist with songIdToAdd params const ids = plModalSongs.map(s => `&songIdToAdd=${s.id}`).join(''); await api('updatePlaylist', `&playlistId=${plId}${ids}`); toast(`✅ ${plModalSongs.length === 1 ? plModalSongs[0].title : plModalSongs.length + ' Songs'} → ${plName}`); } catch(e) { toast('Fehler: ' + e.message); } } async function createAndAddPlaylist() { const name = document.getElementById('pl-new-name').value.trim(); if (!name) { document.getElementById('pl-new-name').focus(); return; } closePlModal(); try { const ids = plModalSongs.map(s => `&songId=${s.id}`).join(''); await api('createPlaylist', `&name=${encodeURIComponent(name)}${ids}`); toast(`✅ Playlist "${name}" erstellt`); // Refresh playlist list if on that page if (document.getElementById('page-playlists').classList.contains('active')) loadPlaylists(); } catch(e) { toast('Fehler: ' + e.message); } } async function createPlaylistPrompt() { document.getElementById('pl-new-name').value = ''; plModalSongs = []; document.getElementById('pl-modal-title').textContent = 'Neue Playlist erstellen'; document.getElementById('pl-list').innerHTML = '
Füge Songs später per Rechtsklick hinzu.
'; document.getElementById('pl-modal').classList.add('open'); } // click outside closes pl-modal document.addEventListener('click', e => { const m = document.getElementById('pl-modal'); if (m.classList.contains('open') && e.target === m) closePlModal(); }); // ─── SONG INFO MODAL ───────────────────────────────────────────── function showSongInfo(s) { const cv = coverUrl(s.coverArt, 200); document.getElementById('info-cover').innerHTML = cv ? `` : '
🎵
'; document.getElementById('info-top-title').textContent = s.title || '—'; document.getElementById('info-top-sub').textContent = (s.artist || '—') + (s.album ? ' · ' + s.album : ''); const fields = [ ['Titel', s.title], ['Künstler', s.artist], ['Album', s.album], ['Jahr', s.year], ['Track #', s.track], ['Disc #', s.discNumber], ['Genre', s.genre], ['Dauer', formatDur(s.duration)], ['Bitrate', s.bitRate ? s.bitRate + ' kbps' : null], ['Format', s.suffix?.toUpperCase()], ['Dateigröße', s.size ? Math.round(s.size/1024/1024*10)/10 + ' MB' : null], ['Pfad', s.path], ['Plays', s.playCount], ].filter(([, v]) => v != null && v !== '' && v !== undefined); document.getElementById('info-rows').innerHTML = fields.map(([l,v]) => `
${l}${esc(String(v))}
` ).join(''); document.getElementById('info-modal').classList.add('open'); } function closeInfoModal() { document.getElementById('info-modal').classList.remove('open'); } document.addEventListener('click', e => { const m = document.getElementById('info-modal'); if (m.classList.contains('open') && e.target === m) closeInfoModal(); }); // ─── NOW PLAYING OVERLAY ───────────────────────────────────────── let npOpen = false; function openNowPlaying() { npOpen = true; const overlay = document.getElementById('np-overlay'); overlay.classList.add('open'); if (npQueueMode) switchNpTab('player'); syncNowPlaying(); // Attach swipe-down once if (!overlay._swipeAttached) { overlay._swipeAttached = true; let startY=0, startT=0, dragging=false; const handle = document.getElementById('np-drag-handle'); const inner = document.getElementById('np-inner'); function onStart(clientY) { startY = clientY; startT = Date.now(); dragging = true; overlay.style.transition = 'none'; } function onMove(clientY) { if (!dragging) return; const dy = Math.max(0, clientY - startY); overlay.style.transform = `translateY(${dy}px)`; } function onEnd(clientY) { if (!dragging) return; dragging = false; overlay.style.transition = ''; const dy = clientY - startY; const dt = Date.now() - startT; const fast = dt < 250 && dy > 50; const far = dy > window.innerHeight * 0.3; if (fast || far) { closeNowPlaying(); } else { overlay.style.transform = ''; } } // Touch overlay.addEventListener('touchstart', e => { // Only start drag from handle OR top 80px of overlay const rect = overlay.getBoundingClientRect(); const ty = e.touches[0].clientY - rect.top; if (ty < 100) onStart(e.touches[0].clientY); }, {passive:true}); overlay.addEventListener('touchmove', e => { onMove(e.touches[0].clientY); }, {passive:true}); overlay.addEventListener('touchend', e => onEnd(e.changedTouches[0].clientY), {passive:true}); // Mouse (desktop) handle.addEventListener('mousedown', e => { onStart(e.clientY); e.preventDefault(); }); document.addEventListener('mousemove', e => onMove(e.clientY)); document.addEventListener('mouseup', e => onEnd(e.clientY)); } } function closeNowPlaying() { npOpen = false; const overlay = document.getElementById('np-overlay'); overlay.style.transform = ''; overlay.classList.remove('open'); } function syncNowPlaying() { if (!npOpen) return; const s = queue[qIdx]; if (!s) return; _syncNpDirect(s); const shuffleBtn=document.getElementById('np-shuffle-btn'); const repeatBtn=document.getElementById('np-repeat-btn'); const volSlider=document.getElementById('np-vol-slider'); if(shuffleBtn)shuffleBtn.classList.toggle('active', shuffle); if(repeatBtn)repeatBtn.classList.toggle('active', repeat !== false); if(volSlider)volSlider.value = Math.round(audio.volume * 100); // play icon & cover pulse const playing=!audio.paused; const npIcon=document.getElementById('np-play-icon'); if(npIcon)npIcon.innerHTML=playing?'':''; const cover=document.getElementById('np-cover'); if(cover)cover.classList.toggle('playing', playing); } function syncNpStar() { if (!npOpen) return; const s = queue[qIdx]; const icon = document.getElementById('np-star-icon'); if (!icon || !s) return; const starred = s._starred || s.starred; icon.setAttribute('fill', starred ? 'var(--accent)' : 'none'); icon.setAttribute('stroke', starred ? 'var(--accent)' : 'rgba(255,255,255,.5)'); } function npToggleStar() { const s = queue[qIdx]; if (!s) return; api(s._starred || s.starred ? 'unstar' : 'star', `&id=${s.id}`) .then(() => { s._starred = !(s._starred || s.starred); s.starred = null; syncNpStar(); updateStarBtn(); }) .catch(e => toast('Fehler: ' + e.message)); } function npSeek(e){} let npQueueMode = false; function switchNpTab(tab) { npQueueMode = tab === 'queue'; document.getElementById('np-overlay').classList.toggle('queue-mode', npQueueMode); document.getElementById('np-tab-player').classList.toggle('active', !npQueueMode); document.getElementById('np-tab-queue').classList.toggle('active', npQueueMode); if (npQueueMode) renderNpQueue(); } function renderNpQueue() { const el = document.getElementById('np-queue-list'); if (!el) return; el.innerHTML = ''; if (!queue.length) { el.innerHTML = '
Warteschlange leer
'; return; } queue.forEach((s, i) => { const item = document.createElement('div'); item.className = 'np-queue-item' + (i === qIdx ? ' nq-active' : ''); const cv = coverUrl(s.coverArt, 60); item.innerHTML = `
${i + 1}
${cv ? `` : '♪'}
${esc(s.title)}
${esc(s.artist || '')}
`; item.onclick = () => { qIdx = i; playCurrent(); switchNpTab('player'); }; el.appendChild(item); }); el.querySelector('.nq-active')?.scrollIntoView({ block: 'nearest' }); } function npGoArtist() { const s = queue[qIdx]; if (!s) return; closeNowPlaying(); if (s.artistId) navigate('artist-detail', { id: s.artistId, name: s.artist }); } function npGoAlbum() { const s = queue[qIdx]; if (!s || !s.albumId) return; closeNowPlaying(); navigate('album-detail', { id: s.albumId, name: s.album||'', artist: s.artist||'', artistId: s.artistId||'', coverArt: s.coverArt||'' }); } // Hook into existing audio events to keep NP in sync