bot: Spotify→YouTube 폴백 dedup ID를 YouTube seed로 사용

폴백 시 lastPlayed.identifier(Spotify ID)와 RD 믹스 첫 곡(YouTube ID)
비교가 안 맞아서, 방금 끝난 곡의 YouTube 버전이 자동재생 큐에 그대로
들어가던 버그 수정.

- youtubeMixFromSpotifyTrack: { result, seedYtId } 반환
- autoPlay: 폴백일 때 dedup 기준 ID를 seedYtId로 교체
This commit is contained in:
tkrmagid
2026-05-27 20:37:22 +09:00
parent 5756f4e10a
commit 09684b2070

View File

@@ -279,6 +279,9 @@ export class GuildPlayer {
const author = this.lastPlayedTrack.info.author; const author = this.lastPlayedTrack.info.author;
let result; let result;
let usedYtFallback = false; let usedYtFallback = false;
// 중복 제거 기준 ID. 기본은 lastPlayed의 identifier, 다만 Spotify→YouTube 폴백 시에는
// RD 믹스의 시드(YouTube videoId)와 비교해야 첫 곡(=방금 끝난 곡의 YouTube 버전)을 제거할 수 있음.
let dedupId = trackId;
if (source === "spotify") { if (source === "spotify") {
const sprecUri = `sprec:seed_tracks=${trackId}&limit=11`; const sprecUri = `sprec:seed_tracks=${trackId}&limit=11`;
@@ -302,7 +305,9 @@ export class GuildPlayer {
} else { } else {
Logger.warn(`[autoPlay] sprec 결과 없음. YouTube으로 폴백합니다.`); Logger.warn(`[autoPlay] sprec 결과 없음. YouTube으로 폴백합니다.`);
} }
result = await this.youtubeMixFromSpotifyTrack(author, title); const fallback = await this.youtubeMixFromSpotifyTrack(author, title);
result = fallback?.result;
if (fallback?.seedYtId) dedupId = fallback.seedYtId;
usedYtFallback = true; usedYtFallback = true;
} }
} else { } else {
@@ -327,7 +332,7 @@ export class GuildPlayer {
this.end(); this.end();
return; return;
} }
if (tracks.length > 0 && tracks[0].info.identifier === trackId) tracks = tracks.slice(1); if (tracks.length > 0 && tracks[0].info.identifier === dedupId) tracks = tracks.slice(1);
if (tracks.length === 0) { if (tracks.length === 0) {
this.end(); this.end();
return; return;
@@ -339,6 +344,7 @@ export class GuildPlayer {
* Spotify 트랙 메타데이터로 YouTube 검색 후 매칭된 곡의 RD 믹스를 가져옴. * Spotify 트랙 메타데이터로 YouTube 검색 후 매칭된 곡의 RD 믹스를 가져옴.
* (현재 Lavalink에서 `ytmsearch:`는 EMPTY를 자주 반환하므로 `ytsearch:` 사용) * (현재 Lavalink에서 `ytmsearch:`는 EMPTY를 자주 반환하므로 `ytsearch:` 사용)
* 각 단계가 실패하면 다음 단계로 진행하지 않고 undefined 반환. * 각 단계가 실패하면 다음 단계로 진행하지 않고 undefined 반환.
* 호출 측에서 RD 믹스 첫 곡(=시드 자체)을 중복 제거할 수 있도록 seed videoId도 같이 반환.
*/ */
private async youtubeMixFromSpotifyTrack(author: string, title: string) { private async youtubeMixFromSpotifyTrack(author: string, title: string) {
const cleanAuthor = author.replace(" - Topic", ""); const cleanAuthor = author.replace(" - Topic", "");
@@ -358,10 +364,11 @@ export class GuildPlayer {
Logger.warn(`[autoPlay] YouTube 폴백 검색 결과 없음 (query="${cleanAuthor} ${title}", loadType=${searchResult?.loadType ?? "(null)"})`); Logger.warn(`[autoPlay] YouTube 폴백 검색 결과 없음 (query="${cleanAuthor} ${title}", loadType=${searchResult?.loadType ?? "(null)"})`);
return undefined; return undefined;
} }
const ytId = searchResult.data[0].info.identifier; const seedYtId = searchResult.data[0].info.identifier;
Logger.info(`[autoPlay] YouTube 폴백 매칭: videoId=${ytId}`); Logger.info(`[autoPlay] YouTube 폴백 매칭: videoId=${seedYtId}`);
try { try {
return await this.player.node.rest.resolve(`https://music.youtube.com/watch?v=${ytId}&list=RD${ytId}`); const result = await this.player.node.rest.resolve(`https://music.youtube.com/watch?v=${seedYtId}&list=RD${seedYtId}`);
return { result, seedYtId };
} catch (err) { } catch (err) {
Logger.warn(`[autoPlay] YouTube 폴백 RD 믹스 resolve throw: ${String(err)}`); Logger.warn(`[autoPlay] YouTube 폴백 RD 믹스 resolve throw: ${String(err)}`);
return undefined; return undefined;