fix: strip parens from keywords, top-10 search, clean search query
- buildTitleKeywords: strip parenthetical tags before picking words so "Eleven Night (Jersey Club)" → keywords ["Eleven","Night"] instead of ["Eleven","Jersey"] — "Jersey" was never in the YT video title - Cascade: add top-10 steps (3+4) with duration still active before dropping duration filter, so "Whispers" (position 6-10) can be found without falling through to the unfiltered last resort - search query: strip non-ASCII chars from artist name so "☆NoAki☆" becomes "NoAki" in the query → cleaner YTM search ranking Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
51d2ea257f
commit
afcbf81547
1 file changed
+27
-12
+27
-12
@@ -80,7 +80,10 @@ function buildTitleKeywords(title) {
|
|||||||
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 = (title || '')
|
// Strip parenthetical genre/style tags like "(Jersey Club)", "(feat. X)" before picking keywords
|
||||||
|
// so "Eleven Night (Jersey Club)" → keywords from "Eleven Night", not "Jersey"
|
||||||
|
const strippedTitle = (title || '').replace(/\([^)]*\)/g, '').trim();
|
||||||
|
const words = strippedTitle
|
||||||
.replace(/[^\w\s]/g, ' ')
|
.replace(/[^\w\s]/g, ' ')
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(w => w.length >= 3 && !VARIANT_WORDS.has(w.toLowerCase()) && !STOPWORDS.has(w.toLowerCase()));
|
.filter(w => w.length >= 3 && !VARIANT_WORDS.has(w.toLowerCase()) && !STOPWORDS.has(w.toLowerCase()));
|
||||||
@@ -239,7 +242,9 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
const outputTemplate = path.join(dir, `${trackPrefix}${safeTitle}.%(ext)s`);
|
const outputTemplate = path.join(dir, `${trackPrefix}${safeTitle}.%(ext)s`);
|
||||||
const query = `${mainArtist} - ${title}`;
|
// Strip non-ASCII chars (e.g. ☆) from artist name for cleaner search ranking
|
||||||
|
const searchArtist = mainArtist.replace(/[^\x00-\x7F]/g, '').trim();
|
||||||
|
const query = `${searchArtist} - ${title}`;
|
||||||
|
|
||||||
// Step 1: yt-dlp — audio only, no thumbnail (we embed iTunes artwork ourselves)
|
// Step 1: yt-dlp — audio only, no thumbnail (we embed iTunes artwork ourselves)
|
||||||
const baseArgs = [
|
const baseArgs = [
|
||||||
@@ -251,10 +256,11 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
|||||||
'--no-warnings',
|
'--no-warnings',
|
||||||
'--newline',
|
'--newline',
|
||||||
];
|
];
|
||||||
// Filtered searches scan top 5 results and stop at first match.
|
// Filtered searches: top 5 (fast) and top 10 (wider net when top 5 misses).
|
||||||
// Unfiltered fallback only needs item 1 (no filter to skip past).
|
// Unfiltered fallback only needs item 1 (no filter to skip past).
|
||||||
const PI_FILTERED = ['--playlist-items', '1-5', '--max-downloads', '1'];
|
const PI_FILTERED = ['--playlist-items', '1-5', '--max-downloads', '1'];
|
||||||
const PI_UNFILTERED = ['--playlist-items', '1'];
|
const PI_FILTERED_10 = ['--playlist-items', '1-10', '--max-downloads', '1'];
|
||||||
|
const PI_UNFILTERED = ['--playlist-items', '1'];
|
||||||
|
|
||||||
const spawnYtdlp = (args) => new Promise((resolve, reject) => {
|
const spawnYtdlp = (args) => new Promise((resolve, reject) => {
|
||||||
const proc = spawn(CONFIG.ytdlpBin, args, { env: { ...process.env, PYTHONUNBUFFERED: '1' } });
|
const proc = spawn(CONFIG.ytdlpBin, args, { env: { ...process.env, PYTHONUNBUFFERED: '1' } });
|
||||||
@@ -313,33 +319,42 @@ 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 + Titel + Varianten-Filter
|
// 1. YTM top-5 — Dauer + Titel + Varianten-Filter
|
||||||
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(dur, true), ...baseArgs]);
|
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(dur, true), ...baseArgs]);
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 2. YT — Dauer + Titel + Varianten-Filter
|
// 2. YT top-5 — 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), ...baseArgs]);
|
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(dur, true), ...baseArgs]);
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 3. YTM — Titel + Varianten (kein Dauer-Check)
|
// 3. YTM top-10 — Dauer + Titel (correcte Version evtl. außerhalb top-5)
|
||||||
|
log(jobId, '⚠️ Erweitere Suche auf top 10 mit Dauerbeschränkung…');
|
||||||
|
await tryYtdlp([ytmUrl, ...PI_FILTERED_10, ...mf(dur, true), ...baseArgs]);
|
||||||
|
}
|
||||||
|
if (!hasAudio()) {
|
||||||
|
// 4. YT top-10 — Dauer + Titel
|
||||||
|
await tryYtdlp([`ytsearch10:${query}`, ...PI_FILTERED_10, ...mf(dur, true), ...baseArgs]);
|
||||||
|
}
|
||||||
|
if (!hasAudio()) {
|
||||||
|
// 5. YTM top-5 — Titel + Varianten (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, true), ...baseArgs]);
|
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 4. YT — Titel + Varianten
|
// 6. YT top-5 — Titel + Varianten
|
||||||
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
|
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 5. YTM — nur Titel (kein Varianten-Filter)
|
// 7. YTM top-5 — nur Titel (kein Varianten-Filter)
|
||||||
log(jobId, '⚠️ Letzter gefilterter Versuch…');
|
log(jobId, '⚠️ Letzter gefilterter Versuch…');
|
||||||
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
|
await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 6. YT — nur Titel
|
// 8. YT top-5 — nur Titel
|
||||||
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
|
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 7+8. Letzter Ausweg: komplett ungefiltert
|
// 9+10. Letzter Ausweg: komplett ungefiltert
|
||||||
log(jobId, '⚠️ Letzter Versuch: keine Filter…');
|
log(jobId, '⚠️ Letzter Versuch: keine Filter…');
|
||||||
await tryYtdlp([ytmUrl, ...PI_UNFILTERED, ...baseArgs]);
|
await tryYtdlp([ytmUrl, ...PI_UNFILTERED, ...baseArgs]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user