- New 'Spotify Importer' page under Entdecken (sidebar + mobile nav) - Backend: POST /api/spotify/resolve — resolves track/album/playlist URLs via Spotify API - Backend: new 'playlist' download type accepting pre-resolved track list (no iTunes re-fetch) - Frontend: paste Spotify link → preview with cover, type badge, track list - Track: single download or full album via iTunes fallback - Album: download all tracks with Spotify metadata - Playlist: download all tracks + optional 'Als Navidrome-Playlist speichern' checkbox - Navidrome playlist creation: searches library by title/artist after download, creates playlist - Spotify Client ID + Secret stored in app settings (setup screen + settings modal) - iTunes metadata tags kept for all downloaded files (existing pipeline unchanged) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
8.6 KiB
JavaScript
199 lines
8.6 KiB
JavaScript
import { S } from './state.js';
|
|
import { toast } from './utils.js';
|
|
import { api } from './api.js';
|
|
import { injectPWA } from './pwa.js';
|
|
import {
|
|
playQueue, playCurrent, togglePlay, prevSong, nextSong, onEnded,
|
|
toggleShuffle, toggleRepeat, toggleQueue, setVolume, seekTo,
|
|
initProgressScrub, updateProgress, updatePreviewProgress, stopPreview,
|
|
toggleRadioMode, starCurrent, downloadCurrent, renderQueue, playPreview,
|
|
toggleStar, dlSongLocal, _applyRadioBtn, _restoreLastPlayed, setPlayIcon,
|
|
npToggleStar,
|
|
} from './player.js';
|
|
import {
|
|
navigate, _navGoBack, _navLoad, loadHomeData, onLocalSearch, onMobileSearch,
|
|
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
|
|
searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio,
|
|
loadRandom, loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
|
|
} from './views.js';
|
|
import {
|
|
openNowPlaying, closeNowPlaying, switchNpTab, npGoArtist, npGoAlbum,
|
|
openNpMenu, closeNpMenu, openSettings, closeSettings, saveSettings,
|
|
checkBackend, openSidebar, closeSidebar, switchNavMode, showCtxMenu,
|
|
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist,
|
|
createPlaylistPrompt, showSongInfo, closeInfoModal, openDlModal, closeDlModal,
|
|
selectDlType, executeDownload, executeSpotifyDownload, openEditPlModal, closeEditPlModal,
|
|
saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover,
|
|
toggleMobileSearch, closeMobileSearch, switchSetupTab,
|
|
} from './ui.js';
|
|
|
|
// ─── 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(/\/$/,'');
|
|
const spId = document.getElementById('spotify-client-id').value.trim();
|
|
const spSecret = document.getElementById('spotify-client-secret').value.trim();
|
|
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';
|
|
S.cfg = {url, user, pass, lastfmKey:lfKey, backendUrl:backUrl, spotifyClientId:spId, spotifyClientSecret:spSecret};
|
|
try {
|
|
await api('ping');
|
|
localStorage.setItem('ns_cfg', JSON.stringify(S.cfg));
|
|
startApp();
|
|
} catch(e) {
|
|
S.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');
|
|
S.cfg = {url:'', user:'', pass:'', lastfmKey:'', backendUrl:'', spotifyClientId:'', spotifyClientSecret:''};
|
|
S.queue=[]; S.qIdx=-1; S._navStack=[]; S._navCursor=-1;
|
|
S.audio.pause(); S.audio.src='';
|
|
document.getElementById('app').style.display='none';
|
|
document.getElementById('setup-screen').style.display='flex';
|
|
}
|
|
|
|
async function startApp() {
|
|
document.getElementById('setup-screen').style.display='none';
|
|
document.getElementById('app').style.display='flex';
|
|
if(_navLoad() && S._navCursor >= 0 && S._navStack[S._navCursor]) {
|
|
const last = S._navStack[S._navCursor];
|
|
S._navJumping = true;
|
|
navigate(last.page, last.data);
|
|
S._navJumping = false;
|
|
} else {
|
|
navigate('home');
|
|
}
|
|
loadHomeData();
|
|
_restoreLastPlayed();
|
|
if(localStorage.getItem('ns_radio')==='1') { S.radioMode=true; _applyRadioBtn(); }
|
|
}
|
|
|
|
// ─── INIT ───
|
|
window.addEventListener('load', () => {
|
|
// Wire up audio elements from DOM
|
|
S.audio = document.getElementById('audio-element');
|
|
S.previewAudio = document.getElementById('preview-audio');
|
|
|
|
const saved = localStorage.getItem('ns_cfg');
|
|
if(saved) { S.cfg = JSON.parse(saved); startApp(); }
|
|
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(); });
|
|
S.audio.addEventListener('timeupdate', updateProgress);
|
|
S.audio.addEventListener('ended', onEnded);
|
|
S.audio.addEventListener('play', () => {
|
|
setPlayIcon(true);
|
|
document.getElementById('player-art').classList.add('spinning');
|
|
if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing';
|
|
});
|
|
S.audio.addEventListener('pause', () => {
|
|
setPlayIcon(false);
|
|
document.getElementById('player-art').classList.remove('spinning');
|
|
if(navigator.mediaSession) navigator.mediaSession.playbackState = 'paused';
|
|
});
|
|
S.previewAudio.addEventListener('timeupdate', updatePreviewProgress);
|
|
S.previewAudio.addEventListener('ended', stopPreview);
|
|
S.audio.volume = 0.8;
|
|
|
|
injectPWA();
|
|
initProgressScrub();
|
|
|
|
// Click player-art to open Now Playing
|
|
document.getElementById('player-art')?.addEventListener('click', openNowPlaying);
|
|
|
|
// Swipe-up on player bar to open Now Playing (with drag visual feedback)
|
|
const playerBar = document.querySelector('.player');
|
|
const npOverlay = document.getElementById('np-overlay');
|
|
if(playerBar) {
|
|
let _sy=0, _dragging=false;
|
|
|
|
function _resetDrag(open) {
|
|
playerBar.classList.remove('dragging');
|
|
playerBar.style.transform = '';
|
|
if(npOverlay) { npOverlay.style.transition = ''; npOverlay.style.transform = ''; }
|
|
if(open) openNowPlaying();
|
|
_sy=0; _dragging=false;
|
|
}
|
|
|
|
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; _dragging=false;
|
|
}, {passive:true});
|
|
|
|
playerBar.addEventListener('touchmove', e => {
|
|
if(!_sy) return;
|
|
const dy = _sy - e.touches[0].clientY;
|
|
if(dy < 5) return;
|
|
_dragging = true;
|
|
playerBar.classList.add('dragging');
|
|
const lift = Math.min(Math.max(dy, 0), 160);
|
|
playerBar.style.transform = `translateY(${-lift}px)`;
|
|
if(npOverlay) {
|
|
npOverlay.style.transition = 'none';
|
|
npOverlay.style.transform = `translateY(${Math.max(0, window.innerHeight - lift * 1.6)}px)`;
|
|
}
|
|
}, {passive:true});
|
|
|
|
playerBar.addEventListener('touchend', e => {
|
|
if(!_sy) return;
|
|
const dy = _sy - e.changedTouches[0].clientY;
|
|
_resetDrag(_dragging ? dy > 60 : dy > 15);
|
|
}, {passive:true});
|
|
|
|
playerBar.addEventListener('touchcancel', () => _resetDrag(false), {passive:true});
|
|
}
|
|
});
|
|
|
|
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();
|
|
});
|
|
try { history.replaceState({ nsIdx: 0 }, '', ''); } catch {}
|
|
|
|
// ─── EXPOSE GLOBALS (for HTML onclick attributes) ───
|
|
Object.assign(window, {
|
|
// Navigation
|
|
navigate, onLocalSearch, onMobileSearch, onGlobalSearch, s_goArtist, s_goAlbum,
|
|
playerBarGoArtist, playerBarGoAlbum, searchSimilarArtist, searchAndPreview,
|
|
loadItunesAlbum, expandArtistBio,
|
|
// Player
|
|
playQueue, togglePlay, prevSong, nextSong, toggleShuffle, toggleRepeat,
|
|
toggleQueue, setVolume, seekTo, starCurrent, downloadCurrent, playPreview,
|
|
toggleStar, dlSongLocal, toggleRadioMode, stopPreview, npToggleStar,
|
|
// Now Playing
|
|
openNowPlaying, closeNowPlaying, switchNpTab, npGoArtist, npGoAlbum,
|
|
openNpMenu, closeNpMenu,
|
|
npSeek: () => {},
|
|
// UI / Modals
|
|
openSettings, closeSettings, saveSettings, checkBackend,
|
|
openSidebar, closeSidebar, switchNavMode, showCtxMenu,
|
|
// Views
|
|
loadRandom, loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
|
|
executeSpotifyDownload,
|
|
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt,
|
|
showSongInfo, closeInfoModal,
|
|
openDlModal, closeDlModal, selectDlType, executeDownload,
|
|
openEditPlModal, closeEditPlModal, saveEditPlaylist,
|
|
editPlUploadCover, editPlCoverSelected, editPlRemoveCover,
|
|
toggleMobileSearch, closeMobileSearch, switchSetupTab,
|
|
// App
|
|
doConnect, disconnect,
|
|
});
|