diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 416a79e..d2338e7 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -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…'); diff --git a/src/ui.js b/src/ui.js index 1bccab5..8958e59 100644 --- a/src/ui.js +++ b/src/ui.js @@ -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); }