From 1bb1d10b25ce15d6b956d6e76c2cee10e1e55e50 Mon Sep 17 00:00:00 2001 From: tkrmagid Date: Sun, 13 Sep 2026 02:21:20 +0900 Subject: [PATCH] =?UTF-8?q?fix(play):=20=EC=8A=A4=ED=8F=AC=ED=8B=B0?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=20=ED=8A=B8=EB=9E=99=EC=9D=80=20=EC=9C=A0?= =?UTF-8?q?=ED=8A=9C=EB=B8=8C=EB=AE=A4=EC=A7=81=20=EC=98=A4=EB=94=94?= =?UTF-8?q?=EC=98=A4=EB=A1=9C=20=EC=9E=AC=EC=83=9D(=EB=AE=A4=EB=B9=84=20?= =?UTF-8?q?=ED=8F=B4=EB=B0=B1=20=EB=B0=A9=EC=A7=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문제: 대시보드에서 스포티파이 곡을 고르면 track.url(open.spotify.com/track/..)을 Lavalink가 그대로 resolve → 유튜브 '영상(뮤비)'으로 폴백 재생됨. 수정: LavalinkManager.search에서 스포티파이 트랙 URL을 감지하면 메타데이터 (제목+아티스트)를 조회해 YoutubeMusic.getSearchUrl ?? `ytsearch:.. Topic` 오디오 검색으로 대체. 직접 입력/대시보드/`-p` 모든 경로에 적용. youtube 등 타 URL은 무영향. - Spotify.parseTrackId / Spotify.getTrackById 추가. - 검증: tsc 통과, Spotify /tracks/{id} 메타데이터 조회 정상, 파싱 정상. --- bot/src/classes/LavalinkManager.ts | 11 +++++++++++ bot/src/utils/api/Spotify.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/bot/src/classes/LavalinkManager.ts b/bot/src/classes/LavalinkManager.ts index 1751315..35df7ae 100644 --- a/bot/src/classes/LavalinkManager.ts +++ b/bot/src/classes/LavalinkManager.ts @@ -118,6 +118,17 @@ export class LavalinkManager { if (isUrl) searchText = text; else if (flags.has('p')) searchText = (await Spotify.getSearchUrl(text)) ?? (await YoutubeMusic.getSearchUrl(text)) ?? `ytsearch:${text}`; else searchText = (await YoutubeMusic.getSearchUrl(text)) ?? `ytsearch:${text}`; + // 스포티파이 트랙 URL은 Lavalink가 유튜브 '영상(뮤비)'으로 폴백한다(유튜브뮤직 연결돼 있어도). + // 대시보드에서 고른 스포티파이 곡이나 `-p` 결과가 트랙 URL이면, 메타데이터(제목+아티스트)로 + // 유튜브뮤직 '오디오'를 직접 찾아 대체한다(텍스트 검색 경로와 동일하게 원곡 오디오 우선). + const spotifyTrackId = Spotify.parseTrackId(searchText); + if (spotifyTrackId) { + const meta = await Spotify.getTrackById(spotifyTrackId); + if (meta) { + const q = `${meta.title} ${meta.artist}`.trim(); + searchText = (await YoutubeMusic.getSearchUrl(q)) ?? `ytsearch:${q}`; + } + } if (searchText.startsWith("ytsearch") && !flags.has('o')) searchText += " Topic"; const result = await node.rest.resolve(searchText); if (!result || result.loadType === LoadType.EMPTY || result.loadType === LoadType.ERROR) { diff --git a/bot/src/utils/api/Spotify.ts b/bot/src/utils/api/Spotify.ts index ceca9c6..87408a7 100644 --- a/bot/src/utils/api/Spotify.ts +++ b/bot/src/utils/api/Spotify.ts @@ -126,5 +126,35 @@ export const Spotify = { const track = (await this.getSearchFull(query) ?? [])?.[0]; if (track?.url) searchCache.set(lowerQuery, track.url); return track?.url ?? null; + }, + + /** open.spotify.com/track/ 또는 spotify:track: 에서 트랙 id를 추출. 아니면 null. */ + parseTrackId(text: string): string | null { + const m = text.trim().match(/(?:open\.spotify\.com\/track\/|spotify:track:)([A-Za-z0-9]+)/); + return m ? m[1] : null; + }, + + /** 트랙 id로 제목/아티스트 메타데이터 조회 (유튜브뮤직 오디오 검색어 생성용). */ + async getTrackById(id: string): Promise<{ title: string; artist: string } | null> { + try { + const token = await this.getToken(); + if (!token) return null; + const response = await fetch(`${SPOTIFY_API_URL}/tracks/${encodeURIComponent(id)}?market=KR`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!response.ok) { + Logger.error(`스포티파이 트랙 조회 에러 (${response.status})`); + return null; + } + const track = await response.json() as SpotifyApi.TrackObjectFull; + if (!track?.name) return null; + return { + title: track.name, + artist: (track.artists ?? []).map((a) => a.name).join(", "), + }; + } catch (err) { + Logger.error(`스포티파이 트랙 조회 실패: ${err}`); + return null; + } } } \ No newline at end of file