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 uploaderWord = artistFilterWord(mainArtist);
const variantFilter = buildVariantFilter(title);
const titleKws = buildTitleKeywords(title);
const uploaderPart = uploaderWord ? ` & artist *= ${uploaderWord}` : '';
const variantPart = variantFilter ? ` & ${variantFilter}` : '';
// ~= 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.
const mf = (dur, artist, variant) => {
// Build a --match-filter arg. Artist filter removed — `artist` field is unreliable in yt-dlp
// for YTM (often empty for label-distributed tracks). Search query already targets the right
// artist; title keyword + duration are sufficient guards against wrong results.
const mf = (dur, variant) => {
const durPart = dur ? `duration > ${dur.min} & duration < ${dur.max} & ` : '';
return ['--match-filter',
`${durPart}!is_live${titleKwPart}${artist ? uploaderPart : ''}${variant ? variantPart : ''}`];
`${durPart}!is_live${titleKwPart}${variant ? variantPart : ''}`];
};
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 };
log(jobId, `🔍 Suche YT Music (~${durationSec}s ±${margin}s): "${query}"`);
// 1. YTM — Dauer + Artist + Varianten-Filter (Top 5, erster Treffer)
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(dur, true, true), ...baseArgs]);
// 1. YTM — Dauer + Titel + Varianten-Filter
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(dur, true), ...baseArgs]);
if (!hasAudio()) {
// 2. YT — Dauer + Artist + Varianten-Filter
// 2. YT — Dauer + Titel + Varianten-Filter
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()) {
// 3. YTM — Dauer + Varianten-Filter (kein Artist)
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)
// 3. YTM — Titel + Varianten (kein Dauer-Check)
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()) {
// 6. YT — nur Varianten-Filter
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]);
// 4. YT — Titel + Varianten
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()) {
// 7+8. Letzter Ausweg: komplett ungefiltert
@@ -340,18 +338,20 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
}
} else {
log(jobId, `🔍 Suche YT Music: "${query}"`);
// Ohne Dauer: Artist + Varianten → ohne Artist → ohne alles
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, true, true), ...baseArgs]);
// 1. YTM — Titel + Varianten
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
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()) {
log(jobId, '⚠️ Kein Treffer mit Artist — ohne Artist-Filter…');
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]);
// 3. YTM — nur Titel
log(jobId, '⚠️ Letzter gefilterter Versuch…');
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
}
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()) {
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;
// 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 {
await api('startScan', '');
toast('🔍 Navidrome scannt Bibliothek…');
await new Promise(r => setTimeout(r, 3000));
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 new Promise(r => setTimeout(r, 5000));
} catch {
await new Promise(r => setTimeout(r, 30000));
}
await _createNdPlaylist(tracks, playlistName);
}