fix: case-insensitive title filter + scan before playlist creation
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
42658b3cbf
commit
7a0a7ca455
2 files changed
+26
-12
No files matched your search
+14
-12
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user