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
+20 -18
+20 -18
View File
@@ -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, '');
// 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);
}