274 lines
8.4 KiB
JavaScript
274 lines
8.4 KiB
JavaScript
const queryInput = document.getElementById('query');
|
|
const searchBtn = document.getElementById('searchBtn');
|
|
const searchTypeSelect = document.getElementById('searchType');
|
|
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');
|
|
|
|
let selectedAlbum = null;
|
|
|
|
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 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>
|
|
<button type="button">Album auswaehlen</button>
|
|
</div>
|
|
`;
|
|
|
|
card.querySelector('button').addEventListener('click', () => openAlbumDialog(album.id, preselectedTrackNames));
|
|
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>
|
|
<button type="button">Album fuer Track auswaehlen</button>
|
|
</div>
|
|
`;
|
|
|
|
card.querySelector('button').addEventListener('click', () => openAlbumDialog(track.albumId, [track.trackName]));
|
|
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>
|
|
<p>Artist</p>
|
|
<button type="button">Alben anzeigen</button>
|
|
</div>
|
|
`;
|
|
|
|
card.querySelector('button').addEventListener('click', () => loadArtistAlbums(artist.id, artist.name));
|
|
return card;
|
|
}
|
|
|
|
function renderItems(type, items) {
|
|
results.innerHTML = '';
|
|
|
|
if (!items.length) {
|
|
results.innerHTML = '<p>Keine Treffer gefunden.</p>';
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
async function openAlbumDialog(albumId, preselectedTrackNames = []) {
|
|
setStatus('Lade Albumdetails...');
|
|
|
|
try {
|
|
const album = await fetchJson(`/api/spotify/album/${albumId}`);
|
|
const preselected = new Set(preselectedTrackNames || []);
|
|
|
|
selectedAlbum = album;
|
|
|
|
dialogTitle.textContent = album.name;
|
|
dialogArtist.textContent = `Artist: ${album.artist}`;
|
|
|
|
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 = `
|
|
<input type="checkbox" ${isChecked ? 'checked' : ''} data-track-name="${track.name.replace(/"/g, '"')}" />
|
|
<span>${track.trackNumber}. ${track.name}</span>
|
|
<small>(${formatDuration(track.durationMs)})</small>
|
|
`;
|
|
trackList.appendChild(row);
|
|
}
|
|
|
|
dialog.showModal();
|
|
setStatus('Album bereit. Tracks auswaehlen 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) {
|
|
setStatus('Bitte Suchbegriff eingeben.', true);
|
|
return;
|
|
}
|
|
|
|
setStatus('Suche in Spotify...');
|
|
results.innerHTML = '';
|
|
|
|
try {
|
|
const data = await fetchJson(`/api/spotify/search?q=${encodeURIComponent(q)}&type=${encodeURIComponent(type)}`);
|
|
const items = data.items || [];
|
|
renderItems(type, items);
|
|
setStatus(`${items.length} Treffer gefunden (${type}).`);
|
|
} catch (err) {
|
|
setStatus(err.message, true);
|
|
}
|
|
}
|
|
|
|
async function sendToLidarr(event) {
|
|
event.preventDefault();
|
|
|
|
if (!selectedAlbum) {
|
|
setStatus('Kein Album ausgewaehlt.', true);
|
|
return;
|
|
}
|
|
|
|
const checked = Array.from(trackList.querySelectorAll('input[type="checkbox"]:checked'));
|
|
const selectedTrackNames = checked.map((input) => input.dataset.trackName);
|
|
|
|
if (!selectedTrackNames.length) {
|
|
setStatus('Bitte mindestens einen Track auswaehlen.', true);
|
|
return;
|
|
}
|
|
|
|
setStatus('Sende Album 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: selectedAlbum.name,
|
|
artistName: selectedAlbum.artist,
|
|
selectedTrackNames,
|
|
cleanupExtras: cleanupToggle.checked
|
|
})
|
|
});
|
|
|
|
const cleanupMsg = data.cleanup?.attempted
|
|
? ` Cleanup: ${data.cleanup.deleted} Dateien geloescht.`
|
|
: '';
|
|
|
|
setStatus(`Album erfolgreich an Lidarr uebergeben (ID ${data.albumId}).${cleanupMsg}`);
|
|
dialog.close();
|
|
} catch (err) {
|
|
setStatus(`Fehler: ${err.message}`, true);
|
|
} finally {
|
|
sendBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function updateLibrary() {
|
|
setStatus('Starte Lidarr Bibliothek-Update...');
|
|
updateLibraryBtn.disabled = true;
|
|
|
|
try {
|
|
const data = await fetchJson('/api/lidarr/update-library', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
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',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
searchBtn.addEventListener('click', searchSpotify);
|
|
queryInput.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') {
|
|
searchSpotify();
|
|
}
|
|
});
|
|
sendBtn.addEventListener('click', sendToLidarr);
|
|
updateLibraryBtn.addEventListener('click', updateLibrary);
|
|
updateFrontendBtn.addEventListener('click', updateFrontendFromGit);
|