bot: Spotify 자동재생 폴백 (sprec EMPTY → YouTube RD 믹스)

- sprec이 EMPTY/ERROR/빈 PLAYLIST면 곡 메타데이터(artist, title)로
  ytsearch 후 첫 결과의 videoId로 RD<id> 믹스를 가져와 자동재생 큐 채움
- 리뷰어 확인: ytmsearch는 현재 Lavalink에서 EMPTY를 반환해서 ytsearch 사용
- sprec / 검색 / 믹스 resolve 각 단계에 try/catch 추가 (한 단계 실패 시
  다음 단계로 진행하지 않고 폴백 또는 end로 안전하게 빠짐)
- 진단 로깅: sprec loadType, 폴백 사유, 검색 매칭 videoId, 최종 후보 곡수
This commit is contained in:
tkrmagid
2026-05-27 20:33:28 +09:00
parent e476e23d37
commit 5756f4e10a

View File

@@ -275,12 +275,45 @@ export class GuildPlayer {
if (!this.lastPlayedTrack) return; if (!this.lastPlayedTrack) return;
const source = this.lastPlayedTrack.info.sourceName; const source = this.lastPlayedTrack.info.sourceName;
const trackId = this.lastPlayedTrack.info.identifier; const trackId = this.lastPlayedTrack.info.identifier;
const title = this.lastPlayedTrack.info.title;
const author = this.lastPlayedTrack.info.author;
let result; let result;
let usedYtFallback = false;
if (source === "spotify") { if (source === "spotify") {
result = await this.player.node.rest.resolve(`sprec:seed_tracks=${trackId}&limit=11`); const sprecUri = `sprec:seed_tracks=${trackId}&limit=11`;
try {
result = await this.player.node.rest.resolve(sprecUri);
Logger.info(`[autoPlay] sprec loadType=${result?.loadType ?? "(null)"} for "${author} - ${title}"`);
} catch (err) {
Logger.warn(`[autoPlay] sprec resolve throw: ${String(err)}`);
result = undefined;
}
// 2024-11-27 이후 신규/development 모드 앱은 Spotify Recommendations API 접근이 막혀서
// sprec이 EMPTY로 떨어지는 경우가 있음. extended 모드 토큰이면 동작함.
// 결과 없으면 YouTube로 폴백.
if (!result
|| result.loadType === LoadType.EMPTY
|| result.loadType === LoadType.ERROR
|| (result.loadType === LoadType.PLAYLIST && result.data.tracks.length === 0)
) {
if (result?.loadType === LoadType.ERROR) {
Logger.warn(`[autoPlay] sprec ERROR: ${result.data?.message ?? "unknown"}. YouTube으로 폴백합니다.`);
} else {
Logger.warn(`[autoPlay] sprec 결과 없음. YouTube으로 폴백합니다.`);
}
result = await this.youtubeMixFromSpotifyTrack(author, title);
usedYtFallback = true;
}
} else { } else {
result = await this.player.node.rest.resolve(`https://music.youtube.com/watch?v=${trackId}&list=RD${trackId}`); try {
result = await this.player.node.rest.resolve(`https://music.youtube.com/watch?v=${trackId}&list=RD${trackId}`);
} catch (err) {
Logger.warn(`[autoPlay] YouTube mix resolve throw: ${String(err)}`);
result = undefined;
}
} }
let tracks: Track[] = []; let tracks: Track[] = [];
if (result?.loadType === LoadType.PLAYLIST) { if (result?.loadType === LoadType.PLAYLIST) {
tracks = result.data.tracks; tracks = result.data.tracks;
@@ -289,6 +322,7 @@ export class GuildPlayer {
} else if (result?.loadType === LoadType.TRACK) { } else if (result?.loadType === LoadType.TRACK) {
tracks = [ result.data ]; tracks = [ result.data ];
} }
Logger.info(`[autoPlay] 자동재생 후보 ${tracks.length}${usedYtFallback ? " (YouTube 폴백)" : ""}`);
if (tracks.length === 0) { if (tracks.length === 0) {
this.end(); this.end();
return; return;
@@ -301,6 +335,39 @@ export class GuildPlayer {
this.addTracks(tracks, "자동재생"); this.addTracks(tracks, "자동재생");
} }
/**
* Spotify 트랙 메타데이터로 YouTube 검색 후 매칭된 곡의 RD 믹스를 가져옴.
* (현재 Lavalink에서 `ytmsearch:`는 EMPTY를 자주 반환하므로 `ytsearch:` 사용)
* 각 단계가 실패하면 다음 단계로 진행하지 않고 undefined 반환.
*/
private async youtubeMixFromSpotifyTrack(author: string, title: string) {
const cleanAuthor = author.replace(" - Topic", "");
const query = `ytsearch:${cleanAuthor} ${title}`;
let searchResult;
try {
searchResult = await this.player.node.rest.resolve(query);
} catch (err) {
Logger.warn(`[autoPlay] YouTube 폴백 검색 throw: ${String(err)}`);
return undefined;
}
if (
searchResult?.loadType !== LoadType.SEARCH
|| !Array.isArray(searchResult.data)
|| searchResult.data.length === 0
) {
Logger.warn(`[autoPlay] YouTube 폴백 검색 결과 없음 (query="${cleanAuthor} ${title}", loadType=${searchResult?.loadType ?? "(null)"})`);
return undefined;
}
const ytId = searchResult.data[0].info.identifier;
Logger.info(`[autoPlay] YouTube 폴백 매칭: videoId=${ytId}`);
try {
return await this.player.node.rest.resolve(`https://music.youtube.com/watch?v=${ytId}&list=RD${ytId}`);
} catch (err) {
Logger.warn(`[autoPlay] YouTube 폴백 RD 믹스 resolve throw: ${String(err)}`);
return undefined;
}
}
private clearAllTimers() { private clearAllTimers() {
if (this.errorTimer !== undefined) { if (this.errorTimer !== undefined) {
clearTimeout(this.errorTimer); clearTimeout(this.errorTimer);