fix: use original artist name in YTM search and add strict channel filter
- YTM URL now uses unstripped artist name (e.g. "N!GHT" instead of "NGHT") for better search ranking on YouTube Music - Steps 1-2 of cascade now include a soft channel ~= filter using the artist's first word, preventing wrong-artist songs with matching titles from being downloaded (e.g. a different "Moonlight" for N!GHT) - Steps 3+ drop the channel filter so label-hosted tracks still get found Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b92cf7444
commit
03fb45b986
1 file changed
+34
-16
+34
-16
@@ -100,7 +100,9 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
});
|
||||
|
||||
const YTM_SONGS_PARAM = 'EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D';
|
||||
const ytmUrl = `https://music.youtube.com/search?q=${encodeURIComponent(query)}¶ms=${YTM_SONGS_PARAM}`;
|
||||
// Use original artist name (with special chars) for YTM URL — better search ranking than stripped version
|
||||
const ytmQuery = `${mainArtist} - ${title}`;
|
||||
const ytmUrl = `https://music.youtube.com/search?q=${encodeURIComponent(ytmQuery)}¶ms=${YTM_SONGS_PARAM}`;
|
||||
const expectedAudio = path.join(dir, `${trackPrefix}${safeTitle}.${CONFIG.audioFmt}`);
|
||||
const hasAudio = () => fs.existsSync(expectedAudio);
|
||||
const tryYtdlp = async (args) => { await spawnYtdlp(args); };
|
||||
@@ -155,10 +157,18 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
const variantPart = variantFilter ? ` & ${variantFilter}` : '';
|
||||
const titleKwPart = titleKws.length ? titleKws.map(k => ` & title ~= (?i)${k}`).join('') : '';
|
||||
|
||||
const mf = (dur, variant) => {
|
||||
// Soft channel check: first word of original artist name used in early cascade steps to avoid
|
||||
// downloading a same-title song by a different artist (e.g. wrong "Moonlight" for N!GHT).
|
||||
// Dropped in later fallback steps so genuine mismatches (label channels etc.) still get found.
|
||||
const escRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const artistFirstWord = mainArtist.split(/[\s,/]+/)[0];
|
||||
const artistChannelPart = artistFirstWord.length >= 3
|
||||
? ` & channel ~= (?i)${escRe(artistFirstWord)}` : '';
|
||||
|
||||
const mf = (dur, variant, strict = false) => {
|
||||
const durPart = dur ? `duration > ${dur.min} & duration < ${dur.max} & ` : '';
|
||||
return ['--match-filter',
|
||||
`${durPart}!is_live${titleKwPart}${variant ? variantPart : ''}`];
|
||||
`${durPart}!is_live${titleKwPart}${strict ? artistChannelPart : ''}${variant ? variantPart : ''}`];
|
||||
};
|
||||
|
||||
const PI_FILTERED = ['--playlist-items', '1-5', '--max-downloads', '1'];
|
||||
@@ -171,20 +181,20 @@ 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 top-5 — pre-filtered URLs (no channel expansion), duration + title + variant filter
|
||||
await tryYtmSearch(5, mf(dur, true));
|
||||
// 1. YTM top-5 — strict: duration + title + variant + channel check
|
||||
await tryYtmSearch(5, mf(dur, true, true));
|
||||
if (!hasAudio()) {
|
||||
// 2. YT top-5 — ytsearch always returns videos (no channels), safe to use directly
|
||||
// 2. YT top-5 — strict: same filters including channel check
|
||||
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, true), ...baseArgs]);
|
||||
}
|
||||
if (!hasAudio()) {
|
||||
// 3. YTM top-10 — wider net
|
||||
// 3. YTM top-10 — no channel check (label channels, etc.)
|
||||
log(jobId, '⚠️ Erweitere Suche auf top 10 mit Dauerbeschränkung…');
|
||||
await tryYtmSearch(10, mf(dur, true));
|
||||
}
|
||||
if (!hasAudio()) {
|
||||
// 4. YT top-10
|
||||
// 4. YT top-10 — no channel check
|
||||
await tryYtdlp([`ytsearch10:${query}`, ...PI_FILTERED_10, ...mf(dur, true), ...baseArgs]);
|
||||
}
|
||||
if (!hasAudio()) {
|
||||
@@ -216,28 +226,36 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
}
|
||||
} else {
|
||||
log(jobId, `🔍 Suche YT Music: "${query}"`);
|
||||
// 1. YTM top-5 — pre-filtered, title + variant
|
||||
await tryYtmSearch(5, mf(null, true));
|
||||
// 1. YTM top-5 — strict: title + variant + channel check
|
||||
await tryYtmSearch(5, mf(null, true, true));
|
||||
if (!hasAudio()) {
|
||||
// 2. YT top-5
|
||||
// 2. YT top-5 — strict
|
||||
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true, true), ...baseArgs]);
|
||||
}
|
||||
if (!hasAudio()) {
|
||||
// 3. YTM top-5 — no channel check
|
||||
await tryYtmSearch(5, mf(null, true));
|
||||
}
|
||||
if (!hasAudio()) {
|
||||
// 4. YT top-5 — no channel check
|
||||
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
|
||||
}
|
||||
if (!hasAudio()) {
|
||||
// 3. YTM top-5 — title only
|
||||
// 5. YTM top-5 — title only
|
||||
log(jobId, '⚠️ Letzter gefilterter Versuch…');
|
||||
await tryYtmSearch(5, mf(null, false));
|
||||
}
|
||||
if (!hasAudio()) {
|
||||
// 4. YT top-5 — title only
|
||||
// 6. YT top-5 — title only
|
||||
await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
|
||||
}
|
||||
if (!hasAudio()) {
|
||||
// 5. YTM — first video result, no filter
|
||||
// 7. YTM — first video result, no filter
|
||||
log(jobId, '⚠️ Letzter Versuch: keine Filter…');
|
||||
await tryYtmSearch(3, []);
|
||||
}
|
||||
if (!hasAudio()) {
|
||||
// 6. YT — no filter
|
||||
// 8. YT — no filter
|
||||
await tryYtdlp([`ytsearch1:${query}`, ...PI_UNFILTERED, ...baseArgs]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user