fix: match-filter trailing-s optional + playlist search fallback for special chars

- buildTitleKeywords: append '?' to keywords ending in 's' so regex
  "Whispers?" matches both "Whisper" (YouTube title) and "Whispers"
  (iTunes title) — fixes NoAki Whispers downloading wrong track
- _createNdPlaylist: if artist+title search returns no title match,
  retry with title-only query so tracks with special chars in artist
  name (e.g. "Kløn") are still found in Navidrome

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-02 14:57:51 +02:00
1 parent 0a7591a690
commit 971cadd8fa
2 files changed
+12 -2

No files matched your search

+3 -1
View File
@@ -83,7 +83,9 @@ function buildTitleKeywords(title) {
.filter(w => w.length >= 3 && !VARIANT_WORDS.has(w.toLowerCase()) && !STOPWORDS.has(w.toLowerCase())); .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 main = words.sort((a, b) => b.length - a.length)[0] || '';
const qualifier = lower.includes('mix') ? MIX_QUALIFIERS.find(q => lower.includes(q)) : null; 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); if (qualifier && main.toLowerCase() !== qualifier) result.push(qualifier);
return result; return result;
} }
+9 -1
View File
@@ -380,7 +380,15 @@ async function _createNdPlaylist(tracks, name) {
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`);
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" // Strip trailing punctuation from first word so "Kløn," matches "Kløn"
const firstWord = s => norm(s).split(/[\s,/•]+/)[0].replace(/\W+$/, ''); const firstWord = s => norm(s).split(/[\s,/•]+/)[0].replace(/\W+$/, '');
const match = songs.find(s => const match = songs.find(s =>