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:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user