fix: YTM search for Japanese tracks - channel filter + album query context

- Skip channel filter when artistFirstWord is non-ASCII (Japanese kanji/kana):
  YouTube channels are named in romaji/English, not kanji — the filter was
  rejecting all valid results and causing 6+ extra cascade steps
- Add albumQuerySuffix: when title is Japanese (no keyword match) OR has only
  one romaji keyword, include the album's first two English words in the search
  query so YTM ranks the official OST/album version above fan covers

Fixes: 照井順政 channel filter rejecting all YTM results
Fixes: "Souiunatsu" matching covers with romaji subtitle instead of official OST

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-25 16:12:27 +02:00
1 parent 1aff19e319
commit 1812500523
1 file changed
+19 -5
+19 -5
View File
@@ -81,7 +81,7 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
// rawArtist is undefined for non-Japanese tracks, so fall back to mainArtist. // rawArtist is undefined for non-Japanese tracks, so fall back to mainArtist.
const ytmArtist = (rawArtist || mainArtist).replace(/\p{S}/gu, '').trim(); const ytmArtist = (rawArtist || mainArtist).replace(/\p{S}/gu, '').trim();
const searchArtist = ytmArtist; const searchArtist = ytmArtist;
const query = `${ytmArtist} - ${title}`; const query = `${ytmArtist} - ${title}${albumQuerySuffix}`;
const baseArgs = [ const baseArgs = [
'--extract-audio', '--extract-audio',
@@ -117,7 +117,7 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
const YTM_SONGS_PARAM = 'EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D'; const YTM_SONGS_PARAM = 'EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D';
// Use original (raw) artist for YTM URL — better ranking since channel is in original language // Use original (raw) artist for YTM URL — better ranking since channel is in original language
const ytmQuery = `${rawArtist || mainArtist} - ${title}`; const ytmQuery = `${rawArtist || mainArtist} - ${title}${albumQuerySuffix}`;
const ytmUrl = `https://music.youtube.com/search?q=${encodeURIComponent(ytmQuery)}&params=${YTM_SONGS_PARAM}`; const ytmUrl = `https://music.youtube.com/search?q=${encodeURIComponent(ytmQuery)}&params=${YTM_SONGS_PARAM}`;
const expectedAudio = path.join(dir, `${fileBase}.${CONFIG.audioFmt}`); const expectedAudio = path.join(dir, `${fileBase}.${CONFIG.audioFmt}`);
const hasAudio = () => fs.existsSync(expectedAudio); const hasAudio = () => fs.existsSync(expectedAudio);
@@ -167,6 +167,9 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
proc.on('close', () => resolve(channelExpanded ? [] : urls)); proc.on('close', () => resolve(channelExpanded ? [] : urls));
}); });
const escRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const JP_CHARS = /[ぁ-んァ-ン一-龯]/;
const variantFilter = buildVariantFilter(title); const variantFilter = buildVariantFilter(title);
const titleKws = buildTitleKeywords(title); const titleKws = buildTitleKeywords(title);
const variantPart = variantFilter ? ` & ${variantFilter}` : ''; const variantPart = variantFilter ? ` & ${variantFilter}` : '';
@@ -174,12 +177,23 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
// Soft channel check: first word of original artist name used in early cascade steps to avoid // 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). // 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. // Skip when artistFirstWord is non-ASCII (Japanese kanji/kana): YouTube channel names are
const escRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // almost never in kanji, so the filter would reject every valid result.
const artistFirstWord = (rawArtist || mainArtist).split(/[\s,/]+/)[0]; const artistFirstWord = (rawArtist || mainArtist).split(/[\s,/]+/)[0];
const artistChannelPart = artistFirstWord.length >= 3 const artistChannelPart = (artistFirstWord.length >= 3 && !JP_CHARS.test(artistFirstWord))
? ` & channel ~= (?i)${escRe(artistFirstWord)}` : ''; ? ` & channel ~= (?i)${escRe(artistFirstWord)}` : '';
// When the title is Japanese (keywords stripped to nothing) OR when we have an album that
// provides better search context, include the album's first two words in the search query.
// This helps YTM rank the official OST/album track above fan covers and romaji-subtitle uploads.
// Only use album words that are ASCII (romaji/English) — Japanese album words don't help YTM ranking.
const hasJpTitle = JP_CHARS.test(title);
const albumWords = (album && album !== 'Singles' && album !== 'Unknown')
? album.split(/\s+/).filter(w => /^[A-Za-z0-9]/.test(w)).slice(0, 2).join(' ')
: '';
const albumQuerySuffix = (hasJpTitle || (albumWords && titleKws.length <= 1)) && albumWords
? ' ' + albumWords : '';
const mf = (dur, variant, strict = false) => { const mf = (dur, variant, strict = false) => {
const durPart = dur ? `duration > ${dur.min} & duration < ${dur.max} & ` : ''; const durPart = dur ? `duration > ${dur.min} & duration < ${dur.max} & ` : '';
return ['--match-filter', return ['--match-filter',