Add Spotify search type filter for album/track/artist with album-based Lidarr flow

This commit is contained in:
J0Z1L 2026-02-28 00:07:18 +01:00
parent b3a343c3a0
commit abf9d61d72
4 changed files with 167 additions and 40 deletions

View file

@ -1,5 +1,6 @@
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');
@ -10,7 +11,6 @@ const sendBtn = document.getElementById('sendBtn');
const cleanupToggle = document.getElementById('cleanupToggle');
let selectedAlbum = null;
let selectedTracks = [];
const CLEANUP_KEY = 'cleanupExtras';
cleanupToggle.checked = localStorage.getItem(CLEANUP_KEY) === 'true';
@ -40,15 +40,7 @@ async function fetchJson(url, options = {}) {
return data;
}
function renderAlbums(albums) {
results.innerHTML = '';
if (!albums.length) {
results.innerHTML = '<p>Keine Alben gefunden.</p>';
return;
}
for (const album of albums) {
function createAlbumCard(album, preselectedTrackNames = []) {
const card = document.createElement('article');
card.className = 'album-card';
@ -58,22 +50,75 @@ function renderAlbums(albums) {
<h3>${album.name}</h3>
<p>${album.artist}</p>
<p>${album.totalTracks} Tracks - ${album.releaseDate || 'unbekannt'}</p>
<button type="button">Auswaehlen</button>
<button type="button">Album auswaehlen</button>
</div>
`;
card.querySelector('button').addEventListener('click', () => openAlbumDialog(album.id));
results.appendChild(card);
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) {
async function openAlbumDialog(albumId, preselectedTrackNames = []) {
setStatus('Lade Albumdetails...');
try {
const album = await fetchJson(`/api/spotify/album/${albumId}`);
const preselected = new Set(preselectedTrackNames || []);
selectedAlbum = album;
selectedTracks = album.tracks.map((t) => t.name);
dialogTitle.textContent = album.name;
dialogArtist.textContent = `Artist: ${album.artist}`;
@ -82,8 +127,9 @@ async function openAlbumDialog(albumId) {
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" checked data-track-name="${track.name.replace(/"/g, '&quot;')}" />
<input type="checkbox" ${isChecked ? 'checked' : ''} data-track-name="${track.name.replace(/"/g, '&quot;')}" />
<span>${track.trackNumber}. ${track.name}</span>
<small>(${formatDuration(track.durationMs)})</small>
`;
@ -97,8 +143,22 @@ async function openAlbumDialog(albumId) {
}
}
async function searchAlbums() {
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;
@ -108,9 +168,10 @@ async function searchAlbums() {
results.innerHTML = '';
try {
const data = await fetchJson(`/api/spotify/search?q=${encodeURIComponent(q)}`);
renderAlbums(data.albums || []);
setStatus(`${data.albums.length} Alben gefunden.`);
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);
}
@ -160,10 +221,10 @@ async function sendToLidarr(event) {
}
}
searchBtn.addEventListener('click', searchAlbums);
searchBtn.addEventListener('click', searchSpotify);
queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
searchAlbums();
searchSpotify();
}
});
sendBtn.addEventListener('click', sendToLidarr);

View file

@ -28,8 +28,13 @@
</section>
<section class="panel search">
<h2>Album suchen</h2>
<h2>Spotify suchen</h2>
<div class="search-row">
<select id="searchType">
<option value="album">Alben</option>
<option value="track">Tracks</option>
<option value="artist">Interpreten</option>
</select>
<input id="query" placeholder="Album oder Artist eingeben" />
<button id="searchBtn">Suchen</button>
</div>

View file

@ -66,6 +66,7 @@ body {
}
input,
select,
button {
border: 1px solid var(--border);
border-radius: 10px;
@ -80,6 +81,12 @@ input {
flex: 1;
}
select {
background: #0b1220;
color: var(--text);
min-width: 140px;
}
button {
background: linear-gradient(120deg, var(--accent), var(--accent-2));
color: #001018;

View file

@ -247,19 +247,27 @@ app.get('/api/health', (_req, res) => {
app.get('/api/spotify/search', async (req, res) => {
const q = String(req.query.q || '').trim();
const type = String(req.query.type || 'album').trim();
const allowedTypes = new Set(['album', 'track', 'artist']);
if (!q) {
return res.status(400).json({ error: 'Suchbegriff fehlt.' });
}
if (!allowedTypes.has(type)) {
return res.status(400).json({ error: 'Ungueltiger Suchtyp. Erlaubt: album, track, artist.' });
}
try {
const data = await spotifyRequest('/search', {
q,
type: 'album',
type,
limit: 20,
market: 'DE'
});
const albums = (data.albums?.items || []).map((album) => ({
let items = [];
if (type === 'album') {
items = (data.albums?.items || []).map((album) => ({
id: album.id,
name: album.name,
artist: album.artists?.map((a) => a.name).join(', ') || 'Unbekannt',
@ -267,8 +275,54 @@ app.get('/api/spotify/search', async (req, res) => {
releaseDate: album.release_date,
totalTracks: album.total_tracks
}));
}
if (type === 'track') {
items = (data.tracks?.items || []).map((track) => ({
id: track.id,
trackName: track.name,
artist: track.artists?.map((a) => a.name).join(', ') || 'Unbekannt',
albumId: track.album?.id,
albumName: track.album?.name || 'Unbekannt',
image: track.album?.images?.[1]?.url || track.album?.images?.[0]?.url || null
}));
}
if (type === 'artist') {
items = (data.artists?.items || []).map((artist) => ({
id: artist.id,
name: artist.name,
followers: artist.followers?.total || 0,
image: artist.images?.[1]?.url || artist.images?.[0]?.url || null
}));
}
res.json({ albums });
res.json({ items });
} catch (err) {
res.status(500).json({ error: err.response?.data?.error?.message || err.message });
}
});
app.get('/api/spotify/artist/:id/albums', async (req, res) => {
try {
const data = await spotifyRequest(`/artists/${req.params.id}/albums`, {
include_groups: 'album,single',
limit: 50,
market: 'DE'
});
const unique = new Map();
for (const album of data.items || []) {
if (unique.has(album.id)) continue;
unique.set(album.id, {
id: album.id,
name: album.name,
artist: album.artists?.map((a) => a.name).join(', ') || 'Unbekannt',
image: album.images?.[1]?.url || album.images?.[0]?.url || null,
releaseDate: album.release_date,
totalTracks: album.total_tracks
});
}
res.json({ albums: Array.from(unique.values()) });
} catch (err) {
res.status(500).json({ error: err.response?.data?.error?.message || err.message });
}