fix: remove artist filter + fix playlist scan timing

backend: drop unreliable artist *= filter entirely — yt-dlp artist field
is empty for many label-distributed YTM tracks (NoAki, etc.), causing
correct results to be rejected in Steps 1-2 and falling through to Step 3
anyway. Search query already targets the right artist; title keyword +
duration are sufficient. Simplifies cascade from 8→8 steps but with fewer
wasted filter attempts.

frontend: add 3s delay before polling getScanStatus (gives Navidrome time
to actually start the scan), and 5s buffer after scan completes (lets DB
writes settle). Fallback to 30s fixed wait if scan API is unavailable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-02 14:15:09 +02:00
1 parent 7a0a7ca455
commit 0a7591a690
2 files changed
+40 -33

No files matched your search

+31 -31
View File
@@ -282,20 +282,19 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
); );
const tryYtdlp = async (args) => { resetSnap(); await spawnYtdlp(args); }; const tryYtdlp = async (args) => { resetSnap(); await spawnYtdlp(args); };
const uploaderWord = artistFilterWord(mainArtist);
const variantFilter = buildVariantFilter(title); const variantFilter = buildVariantFilter(title);
const titleKws = buildTitleKeywords(title); const titleKws = buildTitleKeywords(title);
const uploaderPart = uploaderWord ? ` & artist *= ${uploaderWord}` : '';
const variantPart = variantFilter ? ` & ${variantFilter}` : ''; const variantPart = variantFilter ? ` & ${variantFilter}` : '';
// ~= uses Python re.search; (?i) inline flag = case-insensitive substring match // ~= uses Python re.search; (?i) inline flag = case-insensitive substring match
const titleKwPart = titleKws.length ? titleKws.map(k => ` & title ~= (?i)${k}`).join('') : ''; 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. Artist filter removed — `artist` field is unreliable in yt-dlp
// !is_live is always included. titleKw always included to avoid totally unrelated downloads. // for YTM (often empty for label-distributed tracks). Search query already targets the right
const mf = (dur, artist, variant) => { // artist; title keyword + duration are sufficient guards against wrong results.
const mf = (dur, variant) => {
const durPart = dur ? `duration > ${dur.min} & duration < ${dur.max} & ` : ''; const durPart = dur ? `duration > ${dur.min} & duration < ${dur.max} & ` : '';
return ['--match-filter', return ['--match-filter',
`${durPart}!is_live${titleKwPart}${artist ? uploaderPart : ''}${variant ? variantPart : ''}`]; `${durPart}!is_live${titleKwPart}${variant ? variantPart : ''}`];
}; };
if (durationMs) { if (durationMs) {
@@ -304,31 +303,30 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
const dur = { min: Math.max(30, durationSec - margin), max: durationSec + margin }; const dur = { min: Math.max(30, durationSec - margin), max: durationSec + margin };
log(jobId, `🔍 Suche YT Music (~${durationSec}s ±${margin}s): "${query}"`); log(jobId, `🔍 Suche YT Music (~${durationSec}s ±${margin}s): "${query}"`);
// 1. YTM — Dauer + Artist + Varianten-Filter (Top 5, erster Treffer) // 1. YTM — Dauer + Titel + Varianten-Filter
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(dur, true, true), ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(dur, true), ...baseArgs]);
if (!hasAudio()) { if (!hasAudio()) {
// 2. YT — Dauer + Artist + Varianten-Filter // 2. YT — Dauer + Titel + Varianten-Filter
log(jobId, '⚠️ Kein YTM-Treffer — Fallback zu YouTube…'); log(jobId, '⚠️ Kein YTM-Treffer — Fallback zu YouTube…');
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(dur, true, true), ...baseArgs]); await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(dur, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 3. YTM — Dauer + Varianten-Filter (kein Artist) // 3. YTM — Titel + Varianten (kein Dauer-Check)
log(jobId, '⚠️ Kein Treffer mit Artist-Filter — ohne Artist…');
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(dur, false, true), ...baseArgs]);
}
if (!hasAudio()) {
// 4. YT — Dauer + Varianten-Filter
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(dur, false, true), ...baseArgs]);
}
if (!hasAudio()) {
// 5. YTM — nur Varianten-Filter (kein Dauer-Check)
log(jobId, '⚠️ Kein Treffer im Dauerbereich — suche ohne Dauerbeschränkung…'); log(jobId, '⚠️ Kein Treffer im Dauerbereich — suche ohne Dauerbeschränkung…');
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 6. YT — nur Varianten-Filter // 4. YT — Titel + Varianten
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]); await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
}
if (!hasAudio()) {
// 5. YTM — nur Titel (kein Varianten-Filter)
log(jobId, '⚠️ Letzter gefilterter Versuch…');
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
}
if (!hasAudio()) {
// 6. YT — nur Titel
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 7+8. Letzter Ausweg: komplett ungefiltert // 7+8. Letzter Ausweg: komplett ungefiltert
@@ -340,18 +338,20 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
} }
} else { } else {
log(jobId, `🔍 Suche YT Music: "${query}"`); log(jobId, `🔍 Suche YT Music: "${query}"`);
// Ohne Dauer: Artist + Varianten → ohne Artist → ohne alles // 1. YTM — Titel + Varianten
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, true, true), ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
if (!hasAudio()) { if (!hasAudio()) {
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true, true), ...baseArgs]); // 2. YT — Titel + Varianten
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
log(jobId, '⚠️ Kein Treffer mit Artist — ohne Artist-Filter…'); // 3. YTM — nur Titel
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]); log(jobId, '⚠️ Letzter gefilterter Versuch…');
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]); // 4. YT — nur Titel
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
log(jobId, '⚠️ Letzter Versuch: keine Filter…'); log(jobId, '⚠️ Letzter Versuch: keine Filter…');
+9 -2
View File
@@ -345,16 +345,23 @@ async function _pollAndCreatePlaylist(jobId, tracks, playlistName) {
} }
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 // Trigger Navidrome library scan so newly downloaded files are indexed before searching.
// Initial 3s delay lets the scan actually start before we poll.
// 5s buffer after completion lets DB writes settle before we search.
// If scan API fails entirely, fall back to a 30s fixed wait.
try { try {
await api('startScan', ''); await api('startScan', '');
toast('🔍 Navidrome scannt Bibliothek…'); toast('🔍 Navidrome scannt Bibliothek…');
await new Promise(r => setTimeout(r, 3000));
for (let i = 0; i < 90; i++) { for (let i = 0; i < 90; i++) {
await new Promise(r => setTimeout(r, 2000)); await new Promise(r => setTimeout(r, 2000));
const st = await api('getScanStatus', ''); const st = await api('getScanStatus', '');
if (!st?.scanStatus?.scanning) break; if (!st?.scanStatus?.scanning) break;
} }
} catch { /* scan API unavailable — proceed anyway */ } await new Promise(r => setTimeout(r, 5000));
} catch {
await new Promise(r => setTimeout(r, 30000));
}
await _createNdPlaylist(tracks, playlistName); await _createNdPlaylist(tracks, playlistName);
} }