diff --git a/app.js b/app.js deleted file mode 100644 index 3709016..0000000 --- a/app.js +++ /dev/null @@ -1,1856 +0,0 @@ -// ─── 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.`; - 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='';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';} -} - -let _ndToken=null; -async function ndAuth(){ - if(_ndToken)return _ndToken; - const res=await fetch(`${cfg.url}/auth/login`,{ - method:'POST', - headers:{'Content-Type':'application/json'}, - body:JSON.stringify({username:cfg.user,password:cfg.pass}) - }); - if(!res.ok)throw new Error('Navidrome login fehlgeschlagen'); - const d=await res.json(); - _ndToken=d.token; - return _ndToken; -} - -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 token=await ndAuth(); - const form=new FormData();form.append('image',file,file.name); - const res=await fetch(`${cfg.url}/api/playlist/${_currentPlaylist.id}/image`,{ - method:'POST', - headers:{'X-Nd-Authorization':'Bearer '+token}, - body:form - }); - if(!res.ok)throw new Error(res.status); - toast('Cover aktualisiert'); - setTimeout(()=>{ - const bust=coverUrl(_currentPlaylist.coverArt,300)+'&_cb='+Date.now(); - const detailImg=document.getElementById('playlist-cover'); - if(detailImg)detailImg.innerHTML=``; - document.getElementById('edit-pl-cover').innerHTML=``; - },800); - }catch(e){ - _ndToken=null; - toast('Cover-Upload fehlgeschlagen: '+e.message); - document.getElementById('edit-pl-cover').innerHTML= - (coverUrl(_currentPlaylist.coverArt,300) - ?`` - :'
🎶
'); - } -} - -async function editPlRemoveCover(){ - if(!_currentPlaylist)return; - try{ - const token=await ndAuth(); - const res=await fetch(`${cfg.url}/api/playlist/${_currentPlaylist.id}/image`,{ - method:'DELETE',headers:{'X-Nd-Authorization':'Bearer '+token} - }); - if(!res.ok)throw new Error(res.status); - toast('Cover entfernt'); - _currentPlaylist.coverArt=null; - document.getElementById('edit-pl-cover').innerHTML='
🎶
'; - document.getElementById('playlist-cover').innerHTML='
🎶
'; - }catch(e){_ndToken=null;toast('Fehler: '+e.message)} -} - -// ─── 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 - - - diff --git a/icon.svg b/icon.svg deleted file mode 100644 index 42b0a97..0000000 --- a/icon.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/manifest.json b/manifest.json deleted file mode 100644 index 1e2b74d..0000000 --- a/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "NovaSonic", - "short_name": "NovaSonic", - "description": "Self-hosted music streaming PWA", - "start_url": "/", - "scope": "/", - "display": "standalone", - "orientation": "portrait-primary", - "background_color": "#07070f", - "theme_color": "#07070f", - "icons": [ - { - "src": "icon.svg", - "sizes": "any", - "type": "image/svg+xml", - "purpose": "any" - }, - { - "src": "icon.svg", - "sizes": "any", - "type": "image/svg+xml", - "purpose": "maskable" - } - ] -} diff --git a/sw.js b/sw.js deleted file mode 100644 index 1c99d91..0000000 --- a/sw.js +++ /dev/null @@ -1,40 +0,0 @@ -const CACHE = 'ns-v37'; -const PRECACHE = ['/', 'index.html', 'styles.css', 'app.js', 'manifest.json', 'icon.svg']; -const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; - -self.addEventListener('install', e => { - e.waitUntil(caches.open(CACHE).then(c => c.addAll(PRECACHE))); - self.skipWaiting(); -}); - -self.addEventListener('activate', e => { - e.waitUntil( - caches.keys() - .then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))) - .then(() => self.clients.claim()) - .then(() => self.clients.matchAll({ type: 'window', includeUncontrolled: true })) - .then(clients => clients.forEach(c => c.postMessage({ type: 'SW_UPDATED', version: CACHE }))) - ); -}); - -self.addEventListener('message', e => { - if (e.data?.type === 'GET_VERSION') { - e.source.postMessage({ type: 'VERSION', version: CACHE }); - } -}); - -self.addEventListener('fetch', e => { - if (!e.request.url.startsWith(self.location.origin)) return; - if (SKIP_PATHS.some(p => e.request.url.includes(p))) return; - e.respondWith( - fetch(e.request) - .then(res => { - if (res.status === 200 && e.request.method === 'GET') { - const clone = res.clone(); - caches.open(CACHE).then(c => c.put(e.request, clone)); - } - return res; - }) - .catch(() => caches.match(e.request).then(r => r || Response.error())) - ); -});