fix: rewrite _createNdPlaylist to remove problematic Unicode regex

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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-02 16:07:26 +02:00
1 parent 2d1c9029c5
commit 5e7e700d6a
1 file changed
+21 -19
+21 -19
View File
@@ -338,12 +338,16 @@ export async function executeSpotifyDownload({ type, title, artist, artworkUrl,
} }
async function _pollAndCreatePlaylist(jobId, tracks, playlistName) { 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) { while (true) {
await new Promise(r => setTimeout(r, 2500)); await new Promise(r => setTimeout(r, 2500));
const job = S.jobs[jobId]; const job = S.jobs[jobId];
if (job) lastStatus = job.status;
if (!job || job.status === 'done' || job.status === 'error') break; 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. // Trigger Navidrome library scan so newly downloaded files are indexed before searching.
// Initial 3s delay lets the scan actually start before we poll. // Initial 3s delay lets the scan actually start before we poll.
@@ -371,30 +375,28 @@ async function _createNdPlaylist(tracks, name) {
toast('🎵 Erstelle Navidrome-Playlist…'); toast('🎵 Erstelle Navidrome-Playlist…');
const songIds = []; const songIds = [];
const missing = []; const missing = [];
// Normalize: lowercase, trim, strip diacritics (ø→o, ä→a, etc.) for fuzzy matching const norm = s => s?.toLowerCase().trim() || '';
const norm = s => s?.toLowerCase().trim() || ''; // Strip non-word, non-space chars for loose artist first-word comparison
const normD = s => norm(s).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^\w\s]/g, ''); const normA = s => norm(s).replace(/[^\w\s]/g, '');
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; const missingLabel = t.artistName ? t.artistName + ' – ' + t.trackName : t.trackName;
try { try {
// Use first artist only for search to avoid comma/multi-artist confusion
const firstArtist = t.artistName ? t.artistName.split(/[,/•]/)[0].trim() : ''; const firstArtist = t.artistName ? t.artistName.split(/[,/•]/)[0].trim() : '';
const artistQuery = firstArtist ? `${firstArtist} ${t.trackName}` : t.trackName; const artistQuery = firstArtist ? firstArtist + ' ' + t.trackName : t.trackName;
const r = await api('search3', `&query=${encodeURIComponent(artistQuery)}&songCount=20&albumCount=0&artistCount=0`); const r = await api('search3', '&query=' + encodeURIComponent(artistQuery) + '&songCount=20&albumCount=0&artistCount=0');
let songs = r.searchResult3?.song || []; 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 // retry with title-only so the track can still be found
if (!songs.some(s => norm(s.title) === norm(t.trackName))) { if (!songs.some(s => norm(s.title) === norm(t.trackName))) {
try { 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 || []; songs = r2.searchResult3?.song || [];
} catch { /* keep original songs */ } } catch { /* keep original songs */ }
} }
// firstWordD: first word of artist, diacritics stripped — "Kløn" and "Klon" both → "klon" const firstWord = s => normA(s).split(/\s+/)[0];
const firstWordD = s => normD(s).split(/\s+/)[0];
const titleMatch = s => norm(s.title) === norm(t.trackName); 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)) const match = songs.find(s => titleMatch(s) && artistMatch(s))
|| songs.find(s => titleMatch(s)); || songs.find(s => titleMatch(s));
if (match) songIds.push(match.id); if (match) songIds.push(match.id);
@@ -403,15 +405,15 @@ async function _createNdPlaylist(tracks, name) {
} }
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');
_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; return;
} }
const songParams = songIds.map(id => `&songId=${encodeURIComponent(id)}`).join(''); const songParams = songIds.map(id => '&songId=' + encodeURIComponent(id)).join('');
const plRes = await api('createPlaylist', `&name=${encodeURIComponent(name)}${songParams}`); const plRes = await api('createPlaylist', '&name=' + encodeURIComponent(name) + songParams);
const plId = plRes.playlist?.id || null; const plId = plRes.playlist?.id || null;
const msg = `${songIds.length}/${tracks.length} Tracks hinzugefügt${missing.length ? `, ${missing.length} nicht gefunden` : ''}`; 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`); 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 }); _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) { } catch(e) {
toast('Playlist-Erstellung fehlgeschlagen: ' + e.message); toast('Playlist-Erstellung fehlgeschlagen: ' + e.message);
} }