diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index d2338e7..9c1ff12 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -83,7 +83,9 @@ function buildTitleKeywords(title) { .filter(w => w.length >= 3 && !VARIANT_WORDS.has(w.toLowerCase()) && !STOPWORDS.has(w.toLowerCase())); const main = words.sort((a, b) => b.length - a.length)[0] || ''; const qualifier = lower.includes('mix') ? MIX_QUALIFIERS.find(q => lower.includes(q)) : null; - const result = main ? [main] : []; + // Make trailing 's' optional so "Whispers?" matches both "Whisper" and "Whispers" in yt-dlp regex + const toPattern = w => (w.length >= 4 && w.endsWith('s')) ? w + '?' : w; + const result = main ? [toPattern(main)] : []; if (qualifier && main.toLowerCase() !== qualifier) result.push(qualifier); return result; } diff --git a/src/ui.js b/src/ui.js index 8958e59..4befad5 100644 --- a/src/ui.js +++ b/src/ui.js @@ -380,7 +380,15 @@ async function _createNdPlaylist(tracks, name) { 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 songs = r.searchResult3?.song || []; + let songs = r.searchResult3?.song || []; + // If the 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`); + songs = r2.searchResult3?.song || []; + } catch { /* keep original songs */ } + } // Strip trailing punctuation from first word so "Kløn," matches "Kløn" const firstWord = s => norm(s).split(/[\s,/•]+/)[0].replace(/\W+$/, ''); const match = songs.find(s =>