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 || '';
|
return best || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the most distinctive word from a track title to use as a title relevance filter.
|
// Extract distinctive words from a track title for case-insensitive yt-dlp title ~= (?i) filters.
|
||||||
// Prevents fallback steps from downloading completely unrelated tracks.
|
// Returns an array: [mainWord, optionalMixQualifier].
|
||||||
// Also preserves version qualifiers ("radio mix", "extended mix", "club mix", "vip mix") so
|
// "Memories" → ["Memories"] (matches "MEMORIES", "memories", etc.)
|
||||||
// "Eclipse - Radio Mix" filters for "eclipse" AND "radio" to avoid downloading the plain version.
|
// "Eclipse - Radio Mix" → ["Eclipse", "radio"]
|
||||||
function buildTitleKeyword(title) {
|
function buildTitleKeywords(title) {
|
||||||
const VARIANT_WORDS = new Set(['sped','speedup','slowed','reverb','nightcore','karaoke','parody','remix','cover','acoustic','instrumental']);
|
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 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 MIX_QUALIFIERS = ['radio', 'extended', 'club', 'vip', 'original', 'edit'];
|
||||||
const lower = (title || '').toLowerCase();
|
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, ' ')
|
.replace(/[^\w\s]/g, ' ')
|
||||||
.split(/\s+/)
|
.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] || '';
|
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;
|
const qualifier = lower.includes('mix') ? MIX_QUALIFIERS.find(q => lower.includes(q)) : null;
|
||||||
if (qualifier && qualifier !== main) return `${main} & title *= ${qualifier}`;
|
const result = main ? [main] : [];
|
||||||
return 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…)
|
// 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 uploaderWord = artistFilterWord(mainArtist);
|
||||||
const variantFilter = buildVariantFilter(title);
|
const variantFilter = buildVariantFilter(title);
|
||||||
const titleKeyword = buildTitleKeyword(title);
|
const titleKws = buildTitleKeywords(title);
|
||||||
const uploaderPart = uploaderWord ? ` & artist *= ${uploaderWord}` : '';
|
const uploaderPart = uploaderWord ? ` & artist *= ${uploaderWord}` : '';
|
||||||
const variantPart = variantFilter ? ` & ${variantFilter}` : '';
|
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).
|
// 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.
|
// !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 (!job || job.status === 'done' || job.status === 'error') break;
|
||||||
}
|
}
|
||||||
if (S.jobs[jobId]?.status !== 'done') return;
|
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);
|
await _createNdPlaylist(tracks, playlistName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user