Merge dev into master: Spotify 자동재생 폴백

This commit is contained in:
tkrmagid
2026-05-27 20:33:54 +09:00

View File

@@ -275,12 +275,45 @@ export class GuildPlayer {
if (!this.lastPlayedTrack) return;
const source = this.lastPlayedTrack.info.sourceName;
const trackId = this.lastPlayedTrack.info.identifier;
const title = this.lastPlayedTrack.info.title;
const author = this.lastPlayedTrack.info.author;
let result;
let usedYtFallback = false;
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 {
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[] = [];
if (result?.loadType === LoadType.PLAYLIST) {
tracks = result.data.tracks;
@@ -289,6 +322,7 @@ export class GuildPlayer {
} else if (result?.loadType === LoadType.TRACK) {
tracks = [ result.data ];
}
Logger.info(`[autoPlay] 자동재생 후보 ${tracks.length}${usedYtFallback ? " (YouTube 폴백)" : ""}`);
if (tracks.length === 0) {
this.end();
return;
@@ -301,6 +335,39 @@ export class GuildPlayer {
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() {
if (this.errorTimer !== undefined) {
clearTimeout(this.errorTimer);