From 7a0a7ca455d7734ab206ff83a25614578fa31551 Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 2 May 2026 13:40:20 +0200 Subject: [PATCH] fix: case-insensitive title filter + scan before playlist creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit backend: replace title *= (case-sensitive) with title ~= (?i) regex so "MEMORIES" and "Echoes of Eternity" match regardless of YT title casing. Refactor buildTitleKeyword → buildTitleKeywords returning array. frontend: trigger Navidrome startScan after download completes and wait for scan to finish before running _createNdPlaylist, so newly downloaded tracks are indexed and searchable when the playlist is built. Co-Authored-By: Claude Sonnet 4.6 --- backend/novasonic-server.js | 26 ++++++++++++++------------ src/ui.js | 12 ++++++++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index e3c73d8..416a79e 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -67,24 +67,25 @@ function artistFilterWord(name) { return best || ''; } -// Extract the most distinctive word from a track title to use as a title relevance filter. -// Prevents fallback steps from downloading completely unrelated tracks. -// Also preserves version qualifiers ("radio mix", "extended mix", "club mix", "vip mix") so -// "Eclipse - Radio Mix" filters for "eclipse" AND "radio" to avoid downloading the plain version. -function buildTitleKeyword(title) { +// Extract distinctive words from a track title for case-insensitive yt-dlp title ~= (?i) filters. +// Returns an array: [mainWord, optionalMixQualifier]. +// "Memories" → ["Memories"] (matches "MEMORIES", "memories", etc.) +// "Eclipse - Radio Mix" → ["Eclipse", "radio"] +function buildTitleKeywords(title) { const VARIANT_WORDS = new Set(['sped','speedup','slowed','reverb','nightcore','karaoke','parody','remix','cover','acoustic','instrumental']); const STOPWORDS = new Set(['the','a','an','and','or','of','in','on','at','to','for','feat','ft','with']); const MIX_QUALIFIERS = ['radio', 'extended', 'club', 'vip', 'original', 'edit']; const lower = (title || '').toLowerCase(); - const words = lower + // Keep original case so the word looks right in logs, case-insensitivity is handled by (?i) flag + const words = (title || '') .replace(/[^\w\s]/g, ' ') .split(/\s+/) - .filter(w => w.length >= 3 && !VARIANT_WORDS.has(w) && !STOPWORDS.has(w)); + .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] || ''; - // If the title has a mix qualifier AND "mix", include the qualifier word too const qualifier = lower.includes('mix') ? MIX_QUALIFIERS.find(q => lower.includes(q)) : null; - if (qualifier && qualifier !== main) return `${main} & title *= ${qualifier}`; - return main; + const result = main ? [main] : []; + if (qualifier && main.toLowerCase() !== qualifier) result.push(qualifier); + return result; } // Build yt-dlp --match-filter conditions to reject variant versions (speed up, slowed, remix…) @@ -283,10 +284,11 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t const uploaderWord = artistFilterWord(mainArtist); const variantFilter = buildVariantFilter(title); - const titleKeyword = buildTitleKeyword(title); + const titleKws = buildTitleKeywords(title); const uploaderPart = uploaderWord ? ` & artist *= ${uploaderWord}` : ''; const variantPart = variantFilter ? ` & ${variantFilter}` : ''; - const titleKwPart = titleKeyword ? ` & title *= ${titleKeyword}` : ''; + // ~= uses Python re.search; (?i) inline flag = case-insensitive substring match + const titleKwPart = titleKws.length ? titleKws.map(k => ` & title ~= (?i)${k}`).join('') : ''; // Build a --match-filter arg from active parts (duration, artist, variant, title keyword). // !is_live is always included. titleKw always included to avoid totally unrelated downloads. diff --git a/src/ui.js b/src/ui.js index 3cad145..1bccab5 100644 --- a/src/ui.js +++ b/src/ui.js @@ -344,6 +344,18 @@ async function _pollAndCreatePlaylist(jobId, tracks, playlistName) { if (!job || job.status === 'done' || job.status === 'error') break; } if (S.jobs[jobId]?.status !== 'done') return; + + // Trigger Navidrome library scan so newly downloaded files are indexed before searching + try { + await api('startScan', ''); + toast('🔍 Navidrome scannt Bibliothek…'); + for (let i = 0; i < 90; i++) { + await new Promise(r => setTimeout(r, 2000)); + const st = await api('getScanStatus', ''); + if (!st?.scanStatus?.scanning) break; + } + } catch { /* scan API unavailable — proceed anyway */ } + await _createNdPlaylist(tracks, playlistName); }