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:
1 parent
2d1c9029c5
commit
5e7e700d6a
1 file changed
+21
-19
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user