From 5e7e700d6a34f73a845121fcbff2ec4b4e12d550 Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 2 May 2026 16:07:26 +0200 Subject: [PATCH] fix: rewrite _createNdPlaylist to remove problematic Unicode regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The normD regex used actual Unicode combining chars (U+0300-U+036F) in the source which some build tools mangle, causing a SyntaxError at runtime and silently killing playlist creation. Replaced with normA that strips non-word/non-space chars via plain ASCII regex, which is safe and achieves the same loose artist-first-word comparison. Also fixed race condition in _pollAndCreatePlaylist — save lastStatus before job is auto-deleted so the done check still works. Co-Authored-By: Claude Sonnet 4.6 --- src/ui.js | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/ui.js b/src/ui.js index 8a6ea64..931e3b8 100644 --- a/src/ui.js +++ b/src/ui.js @@ -338,12 +338,16 @@ export async function executeSpotifyDownload({ type, title, artist, artworkUrl, } async function _pollAndCreatePlaylist(jobId, tracks, playlistName) { + // Track last known status separately — job is deleted from S.jobs 5s after completion, + // so S.jobs[jobId] may already be gone by the time we check after the loop breaks. + let lastStatus = 'pending'; while (true) { await new Promise(r => setTimeout(r, 2500)); const job = S.jobs[jobId]; + if (job) lastStatus = job.status; if (!job || job.status === 'done' || job.status === 'error') break; } - if (S.jobs[jobId]?.status !== 'done') return; + if (lastStatus !== 'done') return; // Trigger Navidrome library scan so newly downloaded files are indexed before searching. // Initial 3s delay lets the scan actually start before we poll. @@ -371,30 +375,28 @@ async function _createNdPlaylist(tracks, name) { toast('🎵 Erstelle Navidrome-Playlist…'); const songIds = []; const missing = []; - // Normalize: lowercase, trim, strip diacritics (ø→o, ä→a, etc.) for fuzzy matching - const norm = s => s?.toLowerCase().trim() || ''; - const normD = s => norm(s).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^\w\s]/g, ''); + const norm = s => s?.toLowerCase().trim() || ''; + // Strip non-word, non-space chars for loose artist first-word comparison + const normA = s => norm(s).replace(/[^\w\s]/g, ''); for (const t of tracks) { if (!t.trackName) continue; - const missingLabel = t.artistName ? `${t.artistName} – ${t.trackName}` : t.trackName; + const missingLabel = t.artistName ? t.artistName + ' – ' + t.trackName : t.trackName; try { - // Use first artist only for search to avoid comma/multi-artist confusion const firstArtist = t.artistName ? t.artistName.split(/[,/•]/)[0].trim() : ''; - const artistQuery = firstArtist ? `${firstArtist} ${t.trackName}` : t.trackName; - const r = await api('search3', `&query=${encodeURIComponent(artistQuery)}&songCount=20&albumCount=0&artistCount=0`); + const artistQuery = firstArtist ? firstArtist + ' ' + t.trackName : t.trackName; + const r = await api('search3', '&query=' + encodeURIComponent(artistQuery) + '&songCount=20&albumCount=0&artistCount=0'); let songs = r.searchResult3?.song || []; - // If artist+title query returned no title match (e.g. special chars in artist like "Kløn"), + // If artist+title query returned no title match (e.g. special chars in artist like 'Kløn'), // retry with title-only so the track can still be found if (!songs.some(s => norm(s.title) === norm(t.trackName))) { try { - const r2 = await api('search3', `&query=${encodeURIComponent(t.trackName)}&songCount=20&albumCount=0&artistCount=0`); + const r2 = await api('search3', '&query=' + encodeURIComponent(t.trackName) + '&songCount=20&albumCount=0&artistCount=0'); songs = r2.searchResult3?.song || []; } catch { /* keep original songs */ } } - // firstWordD: first word of artist, diacritics stripped — "Kløn" and "Klon" both → "klon" - const firstWordD = s => normD(s).split(/\s+/)[0]; + const firstWord = s => normA(s).split(/\s+/)[0]; const titleMatch = s => norm(s.title) === norm(t.trackName); - const artistMatch = s => firstWordD(s.artist) === firstWordD(firstArtist || t.artistName); + const artistMatch = s => firstWord(s.artist) === firstWord(firstArtist || t.artistName); const match = songs.find(s => titleMatch(s) && artistMatch(s)) || songs.find(s => titleMatch(s)); if (match) songIds.push(match.id); @@ -403,15 +405,15 @@ async function _createNdPlaylist(tracks, name) { } if (!songIds.length) { toast('⚠ Keine Tracks in Navidrome gefunden — zuerst Library scannen, dann Playlist manuell anlegen'); - _addNotif({ id: 'pl_' + Date.now(), title: `Playlist: ${name}`, artist: '', status: 'error', message: 'Keine Tracks in Navidrome gefunden — Library scan nötig', missing, timestamp: Date.now(), read: false }); + _addNotif({ id: 'pl_' + Date.now(), title: 'Playlist: ' + name, artist: '', status: 'error', message: 'Keine Tracks in Navidrome gefunden — Library scan nötig', missing, timestamp: Date.now(), read: false }); return; } - const songParams = songIds.map(id => `&songId=${encodeURIComponent(id)}`).join(''); - const plRes = await api('createPlaylist', `&name=${encodeURIComponent(name)}${songParams}`); + const songParams = songIds.map(id => '&songId=' + encodeURIComponent(id)).join(''); + const plRes = await api('createPlaylist', '&name=' + encodeURIComponent(name) + songParams); const plId = plRes.playlist?.id || null; - const msg = `${songIds.length}/${tracks.length} Tracks hinzugefügt${missing.length ? `, ${missing.length} nicht gefunden` : ''}`; - toast(`✅ Playlist "${name}" mit ${songIds.length}/${tracks.length} Tracks erstellt`); - _addNotif({ id: 'pl_' + Date.now(), title: `Playlist: ${name}`, artist: '', status: 'done', message: msg, missing, timestamp: Date.now(), read: false, navType: 'playlist', navId: plId, navName: name }); + const msg = songIds.length + '/' + tracks.length + ' Tracks hinzugefügt' + (missing.length ? ', ' + missing.length + ' nicht gefunden' : ''); + toast('✅ Playlist "' + name + '" mit ' + songIds.length + '/' + tracks.length + ' Tracks erstellt'); + _addNotif({ id: 'pl_' + Date.now(), title: 'Playlist: ' + name, artist: '', status: 'done', message: msg, missing, timestamp: Date.now(), read: false, navType: 'playlist', navId: plId, navName: name }); } catch(e) { toast('Playlist-Erstellung fehlgeschlagen: ' + e.message); }