2026-02-27 23:07:13 +01:00
|
|
|
const queryInput = document.getElementById('query');
|
|
|
|
|
const searchBtn = document.getElementById('searchBtn');
|
2026-02-28 00:07:18 +01:00
|
|
|
const searchTypeSelect = document.getElementById('searchType');
|
2026-02-27 23:07:13 +01:00
|
|
|
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');
|
2026-02-28 00:09:47 +01:00
|
|
|
const updateLibraryBtn = document.getElementById('updateLibraryBtn');
|
2026-02-28 00:12:10 +01:00
|
|
|
const updateFrontendBtn = document.getElementById('updateFrontendBtn');
|
2026-02-28 00:33:16 +01:00
|
|
|
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');
|
2026-02-27 23:07:13 +01:00
|
|
|
|
|
|
|
|
let selectedAlbum = null;
|
2026-02-28 00:18:59 +01:00
|
|
|
let sendContext = null;
|
2026-02-28 00:33:16 +01:00
|
|
|
let playlistTracks = [];
|
|
|
|
|
let jobs = [];
|
2026-02-27 23:07:13 +01:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
function pill(exists) {
|
|
|
|
|
return `<span class="pill ${exists ? 'exists' : 'missing'}">${exists ? 'Vorhanden' : 'Fehlt'}</span>`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:07:18 +01:00
|
|
|
function createAlbumCard(album, preselectedTrackNames = []) {
|
|
|
|
|
const card = document.createElement('article');
|
|
|
|
|
card.className = 'album-card';
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
${album.image ? `<img class="album-cover" src="${album.image}" alt="${album.name}" />` : '<div class="album-cover"></div>'}
|
|
|
|
|
<div class="album-body">
|
|
|
|
|
<h3>${album.name}</h3>
|
|
|
|
|
<p>${album.artist}</p>
|
|
|
|
|
<p>${album.totalTracks} Tracks - ${album.releaseDate || 'unbekannt'}</p>
|
2026-02-28 00:33:16 +01:00
|
|
|
<p>${pill(Boolean(album.exists))}</p>
|
2026-02-28 00:07:18 +01:00
|
|
|
<button type="button">Album auswaehlen</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2026-02-28 00:18:59 +01:00
|
|
|
card.querySelector('button').addEventListener('click', () =>
|
|
|
|
|
openAlbumDialog(album.id, preselectedTrackNames, { albumName: album.name, artistName: album.artist })
|
|
|
|
|
);
|
2026-02-28 00:07:18 +01:00
|
|
|
return card;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createTrackCard(track) {
|
|
|
|
|
const card = document.createElement('article');
|
|
|
|
|
card.className = 'album-card';
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
${track.image ? `<img class="album-cover" src="${track.image}" alt="${track.albumName}" />` : '<div class="album-cover"></div>'}
|
|
|
|
|
<div class="album-body">
|
|
|
|
|
<h3>${track.trackName}</h3>
|
|
|
|
|
<p>${track.artist}</p>
|
|
|
|
|
<p>Album: ${track.albumName}</p>
|
2026-02-28 00:33:16 +01:00
|
|
|
<p>${pill(Boolean(track.exists))}</p>
|
2026-02-28 00:07:18 +01:00
|
|
|
<button type="button">Album fuer Track auswaehlen</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2026-02-28 00:18:59 +01:00
|
|
|
card.querySelector('button').addEventListener('click', () =>
|
|
|
|
|
openAlbumDialog(track.albumId, [track.trackName], {
|
|
|
|
|
albumName: track.albumName,
|
|
|
|
|
artistName: track.albumArtist || track.artist
|
|
|
|
|
})
|
|
|
|
|
);
|
2026-02-28 00:07:18 +01:00
|
|
|
return card;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createArtistCard(artist) {
|
|
|
|
|
const card = document.createElement('article');
|
|
|
|
|
card.className = 'album-card';
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
${artist.image ? `<img class="album-cover" src="${artist.image}" alt="${artist.name}" />` : '<div class="album-cover"></div>'}
|
|
|
|
|
<div class="album-body">
|
|
|
|
|
<h3>${artist.name}</h3>
|
|
|
|
|
<p>${artist.followers || 0} Follower</p>
|
|
|
|
|
<button type="button">Alben anzeigen</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
card.querySelector('button').addEventListener('click', () => loadArtistAlbums(artist.id, artist.name));
|
|
|
|
|
return card;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
function createPlaylistTrackCard(track, idx) {
|
|
|
|
|
const card = document.createElement('article');
|
|
|
|
|
card.className = 'album-card';
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
${track.image ? `<img class="album-cover" src="${track.image}" alt="${track.albumName}" />` : '<div class="album-cover"></div>'}
|
|
|
|
|
<div class="album-body">
|
|
|
|
|
<h3>${track.trackName}</h3>
|
|
|
|
|
<p>${track.artist}</p>
|
|
|
|
|
<p>Album: ${track.albumName}</p>
|
|
|
|
|
<p>${pill(Boolean(track.exists))}</p>
|
|
|
|
|
<label><input type="checkbox" data-playlist-idx="${idx}" ${track.selected ? 'checked' : ''}/> Auswaehlen</label>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
return card;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:07:18 +01:00
|
|
|
function renderItems(type, items) {
|
2026-02-27 23:07:13 +01:00
|
|
|
results.innerHTML = '';
|
2026-02-28 00:07:18 +01:00
|
|
|
if (!items.length) {
|
|
|
|
|
results.innerHTML = '<p>Keine Treffer gefunden.</p>';
|
2026-02-27 23:07:13 +01:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-28 00:07:18 +01:00
|
|
|
for (const item of items) {
|
|
|
|
|
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);
|
2026-02-27 23:07:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:18:59 +01:00
|
|
|
async function openAlbumDialog(albumId, preselectedTrackNames = [], context = null) {
|
2026-02-27 23:07:13 +01:00
|
|
|
setStatus('Lade Albumdetails...');
|
|
|
|
|
try {
|
|
|
|
|
const album = await fetchJson(`/api/spotify/album/${albumId}`);
|
|
|
|
|
selectedAlbum = album;
|
2026-02-28 00:18:59 +01:00
|
|
|
sendContext = context;
|
2026-02-27 23:07:13 +01:00
|
|
|
|
|
|
|
|
dialogTitle.textContent = album.name;
|
|
|
|
|
dialogArtist.textContent = `Artist: ${album.artist}`;
|
|
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
const preselected = new Set(preselectedTrackNames || []);
|
2026-02-27 23:07:13 +01:00
|
|
|
trackList.innerHTML = '';
|
|
|
|
|
for (const track of album.tracks) {
|
|
|
|
|
const row = document.createElement('label');
|
|
|
|
|
row.className = 'track-row';
|
2026-02-28 00:07:18 +01:00
|
|
|
const isChecked = preselected.size > 0 ? preselected.has(track.name) : true;
|
2026-02-27 23:07:13 +01:00
|
|
|
row.innerHTML = `
|
2026-02-28 00:33:16 +01:00
|
|
|
<input type="checkbox" ${isChecked ? 'checked' : ''} data-track-name="${track.name.replace(/"/g, '"')}" data-exists="${Boolean(track.exists)}" />
|
2026-02-27 23:07:13 +01:00
|
|
|
<span>${track.trackNumber}. ${track.name}</span>
|
|
|
|
|
<small>(${formatDuration(track.durationMs)})</small>
|
2026-02-28 00:33:16 +01:00
|
|
|
${pill(Boolean(track.exists))}
|
2026-02-27 23:07:13 +01:00
|
|
|
`;
|
2026-02-28 00:33:16 +01:00
|
|
|
const input = row.querySelector('input');
|
|
|
|
|
input.addEventListener('change', updateDialogActionButton);
|
2026-02-27 23:07:13 +01:00
|
|
|
trackList.appendChild(row);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
updateDialogActionButton();
|
2026-02-27 23:07:13 +01:00
|
|
|
dialog.showModal();
|
2026-02-28 00:33:16 +01:00
|
|
|
setStatus('Album bereit. Auswahl pruefen und senden.');
|
2026-02-27 23:07:13 +01:00
|
|
|
} catch (err) {
|
|
|
|
|
setStatus(err.message, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:07:18 +01:00
|
|
|
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() {
|
2026-02-27 23:07:13 +01:00
|
|
|
const q = queryInput.value.trim();
|
2026-02-28 00:07:18 +01:00
|
|
|
const type = searchTypeSelect.value;
|
2026-02-28 00:33:16 +01:00
|
|
|
if (!q) return setStatus('Bitte Suchbegriff eingeben.', true);
|
2026-02-27 23:07:13 +01:00
|
|
|
|
|
|
|
|
setStatus('Suche in Spotify...');
|
|
|
|
|
results.innerHTML = '';
|
|
|
|
|
try {
|
2026-02-28 00:07:18 +01:00
|
|
|
const data = await fetchJson(`/api/spotify/search?q=${encodeURIComponent(q)}&type=${encodeURIComponent(type)}`);
|
2026-02-28 00:33:16 +01:00
|
|
|
renderItems(type, data.items || []);
|
|
|
|
|
setStatus(`${(data.items || []).length} Treffer gefunden (${type}).`);
|
2026-02-27 23:07:13 +01:00
|
|
|
} catch (err) {
|
|
|
|
|
setStatus(err.message, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
function addJob(job) {
|
|
|
|
|
jobs.unshift(job);
|
|
|
|
|
if (jobs.length > 30) jobs = jobs.slice(0, 30);
|
|
|
|
|
renderJobs();
|
|
|
|
|
}
|
2026-02-27 23:07:13 +01:00
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
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);
|
2026-02-27 23:07:13 +01:00
|
|
|
}
|
2026-02-28 00:33:16 +01:00
|
|
|
}
|
2026-02-27 23:07:13 +01:00
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
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));
|
2026-02-27 23:07:13 +01:00
|
|
|
}
|
2026-02-28 00:33:16 +01:00
|
|
|
}
|
2026-02-27 23:07:13 +01:00
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
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);
|
2026-02-27 23:07:13 +01:00
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
setStatus('Sende Auswahl an Lidarr...');
|
|
|
|
|
sendBtn.disabled = true;
|
2026-02-27 23:07:13 +01:00
|
|
|
try {
|
|
|
|
|
const data = await fetchJson('/api/lidarr/send-album', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
2026-02-28 00:18:59 +01:00
|
|
|
albumName: sendContext?.albumName || selectedAlbum.name,
|
|
|
|
|
artistName: sendContext?.artistName || selectedAlbum.artist,
|
2026-02-27 23:07:13 +01:00
|
|
|
selectedTrackNames,
|
2026-02-28 00:33:16 +01:00
|
|
|
trackStates,
|
2026-02-27 23:07:13 +01:00
|
|
|
cleanupExtras: cleanupToggle.checked
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
setStatus(`Aktion gespeichert (+${data.trackActions.add} / -${data.trackActions.remove} / =${data.trackActions.keep}).`);
|
2026-02-27 23:07:13 +01:00
|
|
|
dialog.close();
|
2026-02-28 00:33:16 +01:00
|
|
|
pollCommand(data.commandId, `${selectedAlbum.name}`);
|
2026-02-27 23:07:13 +01:00
|
|
|
} catch (err) {
|
|
|
|
|
setStatus(`Fehler: ${err.message}`, true);
|
|
|
|
|
} finally {
|
|
|
|
|
sendBtn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:09:47 +01:00
|
|
|
async function updateLibrary() {
|
|
|
|
|
setStatus('Starte Lidarr Bibliothek-Update...');
|
|
|
|
|
updateLibraryBtn.disabled = true;
|
|
|
|
|
try {
|
2026-02-28 00:33:16 +01:00
|
|
|
const data = await fetchJson('/api/lidarr/update-library', { method: 'POST' });
|
2026-02-28 00:09:47 +01:00
|
|
|
setStatus(`Bibliothek-Update gestartet (${data.commandNames.join(', ')}).`);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setStatus(`Fehler beim Bibliothek-Update: ${err.message}`, true);
|
|
|
|
|
} finally {
|
|
|
|
|
updateLibraryBtn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:12:10 +01:00
|
|
|
async function updateFrontendFromGit() {
|
|
|
|
|
setStatus('Hole aktuelle Git-Daten...');
|
|
|
|
|
updateFrontendBtn.disabled = true;
|
|
|
|
|
try {
|
2026-02-28 00:33:16 +01:00
|
|
|
const data = await fetchJson('/api/system/update-frontend', { method: 'POST' });
|
2026-02-28 00:12:10 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:33:16 +01:00
|
|
|
function renderPlaylist() {
|
|
|
|
|
results.innerHTML = '';
|
|
|
|
|
if (!playlistTracks.length) {
|
|
|
|
|
results.innerHTML = '<p>Keine Playlist-Tracks gefunden.</p>';
|
|
|
|
|
playlistActions.style.display = 'none';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
playlistTracks.forEach((track, idx) => {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 00:07:18 +01:00
|
|
|
searchBtn.addEventListener('click', searchSpotify);
|
2026-02-27 23:07:13 +01:00
|
|
|
queryInput.addEventListener('keydown', (event) => {
|
2026-02-28 00:33:16 +01:00
|
|
|
if (event.key === 'Enter') searchSpotify();
|
2026-02-27 23:07:13 +01:00
|
|
|
});
|
|
|
|
|
sendBtn.addEventListener('click', sendToLidarr);
|
2026-02-28 00:33:16 +01:00
|
|
|
scanLibraryBtn.addEventListener('click', scanLibrary);
|
2026-02-28 00:09:47 +01:00
|
|
|
updateLibraryBtn.addEventListener('click', updateLibrary);
|
2026-02-28 00:12:10 +01:00
|
|
|
updateFrontendBtn.addEventListener('click', updateFrontendFromGit);
|
2026-02-28 00:33:16 +01:00
|
|
|
importPlaylistBtn.addEventListener('click', importPlaylist);
|
|
|
|
|
sendPlaylistBtn.addEventListener('click', sendPlaylistSelection);
|
|
|
|
|
|
|
|
|
|
renderJobs();
|