const queryInput = document.getElementById('query');
const searchBtn = document.getElementById('searchBtn');
const searchTypeSelect = document.getElementById('searchType');
const existsFilterSelect = document.getElementById('existsFilter');
const results = document.getElementById('results');
const statusEl = document.getElementById('status');
const dialog = document.getElementById('albumDialog');
const dialogTitle = document.getElementById('dialogTitle');
const dialogArtist = document.getElementById('dialogArtist');
const trackList = document.getElementById('trackList');
const sendBtn = document.getElementById('sendBtn');
const cleanupToggle = document.getElementById('cleanupToggle');
const updateLibraryBtn = document.getElementById('updateLibraryBtn');
const updateFrontendBtn = document.getElementById('updateFrontendBtn');
const scanLibraryBtn = document.getElementById('scanLibraryBtn');
const playlistSource = document.getElementById('playlistSource');
const playlistValue = document.getElementById('playlistValue');
const importPlaylistBtn = document.getElementById('importPlaylistBtn');
const sendPlaylistBtn = document.getElementById('sendPlaylistBtn');
const playlistActions = document.getElementById('playlistActions');
const jobList = document.getElementById('jobList');
let selectedAlbum = null;
let sendContext = null;
let playlistTracks = [];
let jobs = [];
let lastSearch = { type: null, items: [] };
const CLEANUP_KEY = 'cleanupExtras';
cleanupToggle.checked = localStorage.getItem(CLEANUP_KEY) === 'true';
cleanupToggle.addEventListener('change', () => {
localStorage.setItem(CLEANUP_KEY, String(cleanupToggle.checked));
});
function setStatus(text, isError = false) {
statusEl.textContent = text;
statusEl.style.color = isError ? 'var(--danger)' : 'var(--text)';
}
function formatDuration(ms) {
const totalSec = Math.floor(ms / 1000);
const min = Math.floor(totalSec / 60);
const sec = totalSec % 60;
return `${min}:${String(sec).padStart(2, '0')}`;
}
async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`);
}
return data;
}
function pill(exists) {
return `${exists ? 'Vorhanden' : 'Fehlt'}`;
}
function createAlbumCard(album, preselectedTrackNames = []) {
const card = document.createElement('article');
card.className = 'album-card';
card.innerHTML = `
${album.image ? `` : '
${album.artist}
${album.totalTracks} Tracks - ${album.releaseDate || 'unbekannt'}
${pill(Boolean(album.exists))}
${track.artist}
Album: ${track.albumName}
${pill(Boolean(track.exists))}
${artist.followers || 0} Follower
${track.artist}
Album: ${track.albumName}
${pill(Boolean(track.exists))}
Keine Treffer gefunden.
'; return; } for (const item of filtered) { let card; if (type === 'track') card = createTrackCard(item); if (type === 'artist') card = createArtistCard(item); if (type === 'album') card = createAlbumCard(item); if (card) results.appendChild(card); } } function applyExistsFilter(items, type) { const mode = existsFilterSelect.value || 'all'; if (mode === 'all') return items; if (type === 'artist') return items; return (items || []).filter((item) => { const exists = Boolean(item.exists); if (mode === 'exists') return exists; if (mode === 'missing') return !exists; return true; }); } function updateDialogActionButton() { const rows = Array.from(trackList.querySelectorAll('input[type="checkbox"]')); const selected = rows.filter((x) => x.checked).map((x) => ({ exists: x.dataset.exists === 'true' })); const add = selected.filter((x) => !x.exists).length; const keep = selected.filter((x) => x.exists).length; const remove = rows.filter((x) => !x.checked && x.dataset.exists === 'true').length; if (add > 0 && remove > 0) { sendBtn.textContent = `Anwenden (+${add} / -${remove})`; } else if (add > 0) { sendBtn.textContent = `Hinzufuegen (${add})`; } else if (remove > 0) { sendBtn.textContent = `Entfernen (${remove})`; } else if (keep > 0) { sendBtn.textContent = `Behalten (${keep})`; } else { sendBtn.textContent = 'An Lidarr senden'; } } async function openAlbumDialog(albumId, preselectedTrackNames = [], context = null) { setStatus('Lade Albumdetails...'); try { const album = await fetchJson(`/api/spotify/album/${albumId}`); selectedAlbum = album; sendContext = context; dialogTitle.textContent = album.name; dialogArtist.textContent = `Artist: ${album.artist}`; const preselected = new Set(preselectedTrackNames || []); trackList.innerHTML = ''; for (const track of album.tracks) { const row = document.createElement('label'); row.className = 'track-row'; const isChecked = preselected.size > 0 ? preselected.has(track.name) : true; row.innerHTML = ` ${track.trackNumber}. ${track.name} (${formatDuration(track.durationMs)}) ${pill(Boolean(track.exists))} `; const input = row.querySelector('input'); input.addEventListener('change', updateDialogActionButton); trackList.appendChild(row); } updateDialogActionButton(); dialog.showModal(); setStatus('Album bereit. Auswahl pruefen und senden.'); } catch (err) { setStatus(err.message, true); } } async function loadArtistAlbums(artistId, artistName) { setStatus(`Lade Alben von ${artistName}...`); try { const data = await fetchJson(`/api/spotify/artist/${artistId}/albums`); renderItems('album', data.albums || []); setStatus(`${data.albums.length} Alben von ${artistName} gefunden.`); } catch (err) { setStatus(err.message, true); } } async function searchSpotify() { const q = queryInput.value.trim(); const type = searchTypeSelect.value; if (!q) return setStatus('Bitte Suchbegriff eingeben.', true); setStatus('Suche in Spotify...'); results.innerHTML = ''; try { const data = await fetchJson(`/api/spotify/search?q=${encodeURIComponent(q)}&type=${encodeURIComponent(type)}`); renderItems(type, data.items || []); setStatus(`${(data.items || []).length} Treffer gefunden (${type}).`); } catch (err) { setStatus(err.message, true); } } function addJob(job) { jobs.unshift(job); if (jobs.length > 30) jobs = jobs.slice(0, 30); renderJobs(); } function renderJobs() { jobList.innerHTML = ''; for (const job of jobs) { const row = document.createElement('div'); row.className = 'job-row'; row.textContent = `${job.label}: ${job.status}`; jobList.appendChild(row); } } async function pollCommand(commandId, label) { if (!commandId) return; const job = { label, status: 'queued', commandId }; addJob(job); for (let i = 0; i < 60; i += 1) { try { const data = await fetchJson(`/api/lidarr/command/${commandId}`); job.status = data.status || 'unknown'; renderJobs(); if (['completed', 'failed', 'aborted'].includes(String(data.status || '').toLowerCase())) return; } catch (_err) { job.status = 'status-unbekannt'; renderJobs(); return; } await new Promise((r) => setTimeout(r, 3000)); } } async function sendToLidarr(event) { event.preventDefault(); if (!selectedAlbum) return setStatus('Kein Album ausgewaehlt.', true); const rows = Array.from(trackList.querySelectorAll('input[type="checkbox"]')); const trackStates = rows.map((input) => ({ name: input.dataset.trackName, selected: input.checked, exists: input.dataset.exists === 'true' })); const selectedTrackNames = trackStates.filter((x) => x.selected).map((x) => x.name); setStatus('Sende Auswahl an Lidarr...'); sendBtn.disabled = true; try { const data = await fetchJson('/api/lidarr/send-album', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ albumName: sendContext?.albumName || selectedAlbum.name, artistName: sendContext?.artistName || selectedAlbum.artist, selectedTrackNames, trackStates, cleanupExtras: cleanupToggle.checked }) }); setStatus(`Aktion gespeichert (+${data.trackActions.add} / -${data.trackActions.remove} / =${data.trackActions.keep}).`); dialog.close(); pollCommand(data.commandId, `${selectedAlbum.name}`); } catch (err) { setStatus(`Fehler: ${err.message}`, true); } finally { sendBtn.disabled = false; } } async function scanLibrary() { scanLibraryBtn.disabled = true; setStatus('Scanne Lidarr Bibliothek...'); try { const data = await fetchJson('/api/lidarr/library-scan', { method: 'POST' }); setStatus(`Bibliothek gescannt (${data.trackCount} Tracks).`); } catch (err) { setStatus(`Scan fehlgeschlagen: ${err.message}`, true); } finally { scanLibraryBtn.disabled = false; } } async function updateLibrary() { setStatus('Starte Lidarr Bibliothek-Update...'); updateLibraryBtn.disabled = true; try { const data = await fetchJson('/api/lidarr/update-library', { method: 'POST' }); setStatus(`Bibliothek-Update gestartet (${data.commandNames.join(', ')}).`); } catch (err) { setStatus(`Fehler beim Bibliothek-Update: ${err.message}`, true); } finally { updateLibraryBtn.disabled = false; } } async function updateFrontendFromGit() { setStatus('Hole aktuelle Git-Daten...'); updateFrontendBtn.disabled = true; try { const data = await fetchJson('/api/system/update-frontend', { method: 'POST' }); if (data.updated) { setStatus(`Frontend aktualisiert (${data.before} -> ${data.after}). Seite wird neu geladen...`); setTimeout(() => window.location.reload(), 1200); } else { setStatus(`Bereits aktuell (${data.after}).`); } } catch (err) { setStatus(`Frontend-Update fehlgeschlagen: ${err.message}`, true); } finally { updateFrontendBtn.disabled = false; } } function renderPlaylist() { lastSearch = { type: 'playlist-track', items: playlistTracks.slice() }; results.innerHTML = ''; const filtered = applyExistsFilter(playlistTracks, 'track'); if (!filtered.length) { results.innerHTML = 'Keine Playlist-Tracks gefunden.
'; playlistActions.style.display = 'none'; return; } filtered.forEach((track) => { const idx = playlistTracks.indexOf(track); const card = createPlaylistTrackCard(track, idx); const cb = card.querySelector('input[type="checkbox"]'); cb.addEventListener('change', () => { playlistTracks[idx].selected = cb.checked; }); results.appendChild(card); }); playlistActions.style.display = 'block'; } async function importPlaylist() { const source = playlistSource.value; const value = playlistValue.value.trim(); if (!value) return setStatus('Bitte Playlist URL oder ID eingeben.', true); importPlaylistBtn.disabled = true; setStatus(`Importiere ${source}-Playlist...`); try { const data = await fetchJson('/api/import/playlist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source, value }) }); playlistTracks = (data.tracks || []).map((t) => ({ ...t, selected: true })); renderPlaylist(); setStatus(`${playlistTracks.length} Tracks importiert.`); } catch (err) { setStatus(`Playlist-Import fehlgeschlagen: ${err.message}`, true); } finally { importPlaylistBtn.disabled = false; } } async function sendPlaylistSelection() { const selected = playlistTracks.filter((t) => t.selected); if (!selected.length) return setStatus('Keine Playlist-Tracks ausgewaehlt.', true); const groups = new Map(); for (const track of selected) { const key = `${track.albumName}___${track.albumArtist}`; if (!groups.has(key)) { groups.set(key, { albumName: track.albumName, artistName: track.albumArtist, selectedTrackNames: [], trackStates: [] }); } const g = groups.get(key); g.selectedTrackNames.push(track.trackName); g.trackStates.push({ name: track.trackName, selected: true, exists: Boolean(track.exists) }); } setStatus(`Sende ${groups.size} Alben aus Playlist an Lidarr...`); sendPlaylistBtn.disabled = true; try { for (const item of groups.values()) { const data = await fetchJson('/api/lidarr/send-album', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item) }); pollCommand(data.commandId, `${item.artistName} - ${item.albumName}`); } setStatus('Playlist-Auswahl wurde an Lidarr uebergeben.'); } catch (err) { setStatus(`Fehler bei Playlist-Download: ${err.message}`, true); } finally { sendPlaylistBtn.disabled = false; } } searchBtn.addEventListener('click', searchSpotify); queryInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') searchSpotify(); }); sendBtn.addEventListener('click', sendToLidarr); scanLibraryBtn.addEventListener('click', scanLibrary); updateLibraryBtn.addEventListener('click', updateLibrary); updateFrontendBtn.addEventListener('click', updateFrontendFromGit); importPlaylistBtn.addEventListener('click', importPlaylist); sendPlaylistBtn.addEventListener('click', sendPlaylistSelection); existsFilterSelect.addEventListener('change', () => { if (lastSearch.type === 'playlist-track') { renderPlaylist(); return; } if (lastSearch.type) { renderItems(lastSearch.type, lastSearch.items); } }); renderJobs();