diff --git a/bot/src/utils/api/YoutubeMusic.ts b/bot/src/utils/api/YoutubeMusic.ts index 0b84401..19d6bdf 100644 --- a/bot/src/utils/api/YoutubeMusic.ts +++ b/bot/src/utils/api/YoutubeMusic.ts @@ -219,9 +219,18 @@ export const YoutubeMusic = { const lowerQuery = query.toLocaleLowerCase().trim(); if (searchCache.has(lowerQuery)) return searchCache.get(lowerQuery) ?? null; const video = (await this.getSearchFull(query) ?? [])?.[0]; - // 최상위 결과가 커버/MR/노래방류면 null을 반환해, 호출측(LavalinkManager)이 - // 더 깨끗한 `ytsearch:... Topic`(공식 오디오) 폴백을 쓰도록 한다. - if (!video?.url || JUNK_RE.test(video.title)) return null; + if (!video?.url) return null; + // YTM API가 이 계정/엔드포인트에서 음악 카탈로그 '노래'를 주지 못하고 일반 유튜브 영상 + // (토크쇼·재생목록·커버·MR 등)을 반환하는 경우가 많다. 그래서 (1)커버/MR류거나 (2)제목이 + // 검색어와 충분히 일치하지 않으면 null을 반환해, 호출측이 더 깨끗한 `ytsearch:... Topic` + // (공식 오디오) 폴백을 쓰도록 한다. + if (JUNK_RE.test(video.title)) return null; + const titleLower = video.title.toLocaleLowerCase(); + const tokens = lowerQuery.split(/\s+/).filter((t) => t.length >= 2); + if (tokens.length > 0) { + const hit = tokens.filter((t) => titleLower.includes(t)).length; + if (hit / tokens.length < 0.5) return null; // 관련성 낮음 → 폴백 + } searchCache.set(lowerQuery, video.url); return video.url; }