fix(ui): playlist search with artist+title, artist in missing list, no bounce on job cards

- Playlist track search: use 'Artist Title' query + songCount=10 (was title-only + 5)
  fixes songs like 'Dead' by Geoxor being missed due to common-word collision
- Missing tracks now show 'Artist – Title' instead of just title
- renderJobs: update existing cards instead of full innerHTML wipe every 1.5s
  eliminates slideUp animation re-triggering on every poll tick

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-02 11:56:17 +02:00
1 parent 4b6095f213
commit cf8ba70af8
1 file changed
+20 -6
+20 -6
View File
@@ -355,16 +355,19 @@ async function _createNdPlaylist(tracks, name) {
const norm = s => s?.toLowerCase().trim() || ''; const norm = s => s?.toLowerCase().trim() || '';
for (const t of tracks) { for (const t of tracks) {
if (!t.trackName) continue; if (!t.trackName) continue;
const missingLabel = t.artistName ? `${t.artistName} – ${t.trackName}` : t.trackName;
try { try {
const r = await api('search3', `&query=${encodeURIComponent(t.trackName)}&songCount=5&albumCount=0&artistCount=0`); // Search with artist+title first for precision, fall back to title-only with more results
const artistQuery = t.artistName ? `${t.artistName} ${t.trackName}` : t.trackName;
const r = await api('search3', `&query=${encodeURIComponent(artistQuery)}&songCount=10&albumCount=0&artistCount=0`);
const songs = r.searchResult3?.song || []; const songs = r.searchResult3?.song || [];
const match = songs.find(s => const match = songs.find(s =>
norm(s.title) === norm(t.trackName) && norm(s.title) === norm(t.trackName) &&
norm(s.artist).split(' ')[0] === norm(t.artistName).split(' ')[0] norm(s.artist).split(' ')[0] === norm(t.artistName).split(' ')[0]
) || songs.find(s => norm(s.title) === norm(t.trackName)); ) || songs.find(s => norm(s.title) === norm(t.trackName));
if (match) songIds.push(match.id); if (match) songIds.push(match.id);
else missing.push(t.trackName); else missing.push(missingLabel);
} catch { missing.push(t.trackName); } } catch { missing.push(missingLabel); }
} }
if (!songIds.length) { if (!songIds.length) {
toast('⚠ Keine Tracks in Navidrome gefunden — zuerst Library scannen, dann Playlist manuell anlegen'); toast('⚠ Keine Tracks in Navidrome gefunden — zuerst Library scannen, dann Playlist manuell anlegen');
@@ -521,9 +524,21 @@ function updateJob(id, status, msg, progress) {
} }
function renderJobs() { function renderJobs() {
const panel = document.getElementById('jobs-panel'); const panel = document.getElementById('jobs-panel');
panel.innerHTML = ''; const activeIds = new Set(Object.keys(S.jobs));
// Remove cards whose jobs are gone
panel.querySelectorAll('[data-job-id]').forEach(el => {
if (!activeIds.has(el.dataset.jobId)) el.remove();
});
Object.entries(S.jobs).forEach(([id, j]) => { Object.entries(S.jobs).forEach(([id, j]) => {
const c = document.createElement('div'); c.className = 'job-card'; let c = panel.querySelector(`[data-job-id="${id}"]`);
if (!c) {
c = document.createElement('div');
c.className = 'job-card';
c.dataset.jobId = id;
panel.appendChild(c);
}
const isDone = j.status === 'done', isErr = j.status === 'error'; const isDone = j.status === 'done', isErr = j.status === 'error';
const statusText = isErr ? 'Fehler' : isDone ? '✓ Fertig! Bibliothek wird gescannt…' : j.msg || 'Wird heruntergeladen…'; const statusText = isErr ? 'Fehler' : isDone ? '✓ Fertig! Bibliothek wird gescannt…' : j.msg || 'Wird heruntergeladen…';
c.innerHTML = `<div class="job-header"> c.innerHTML = `<div class="job-header">
@@ -532,7 +547,6 @@ function renderJobs() {
</div> </div>
<div class="job-bar"><div class="job-fill ${isDone?'done':isErr?'error':''}" style="width:${isDone?100:isErr?100:j.progress}%"></div></div> <div class="job-bar"><div class="job-fill ${isDone?'done':isErr?'error':''}" style="width:${isDone?100:isErr?100:j.progress}%"></div></div>
<div class="job-sub">${statusText}</div>`; <div class="job-sub">${statusText}</div>`;
panel.appendChild(c);
}); });
} }
async function pollJob(jobId) { async function pollJob(jobId) {