- 봇: partner API(fetchPlaylist/getAlbum, 토크너 익명 토큰)로 첫 100곡을 받아 바로 재생,
나머지는 1000곡씩(동시 3페이지) 백그라운드로 이어 붙임. 곡마다 앨범 커버 포함.
실패 시 임베드 페이지(100곡) → Lavalink 순으로 폴백
- 정지·끝·모두 지우기 시 queueEpoch 증가 → 이어 받기 중단. '-s'는 페이지마다 재생목록 곡끼리 재섞기
- 토크너 토큰 캐시를 정식 API 토큰 캐시와 분리
- queue_list: 앞 200곡 + total/totalMs만 전송, queue_set: windowSize 부분만 교체·clearAll 지원
- 대시보드: 곡 수·총 길이는 전체 기준, '외 N곡' 표시, 모두 지우기는 clearAll
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 재생목록/앨범: 임베드 페이지 곡 목록(~0.4초)으로 변환 전 항목(spotify-lazy:<id>)을 바로 추가
(기존: 곡마다 유튜브 매칭 후 5곡씩 이어 붙여 100곡에 ~12초)
- GuildPlayer.resolvePlayable 에서 스포티파이 곡을 재생 직전 유튜브 원곡으로 매칭(다음 곡은 프리페치)
- 매칭 결과 캐시(matchSpotifyTrack)로 예열·재생이 검색을 한 번만 하도록
- 원곡을 못 찾은 곡은 건너뜀, 재생목록 커버는 변환 시 곡 이미지로 교체
- '-s' 섞기를 resolveQuery로 옮겨 섞은 뒤 첫 곡을 예열
- queue_set: JSON으로 깨진 _resolved({}) 제거
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
page
- /login(디스코드 로그인 전용), /servers(서버 선택 전용), /servers/[id](유튜브 뮤직 플레이어)로 분리
· 서버 컴포넌트에서 로그인 검사, callbackUrl 은 같은 사이트 경로만 허용
· NextAuth 로그인/오류 화면도 /login 으로
- 서버 선택: 최근 사용 서버 맨 앞 "최근" 표시, 새로고침, 8개 초과 시 이름 검색, 오류 표시
- 플레이어: 검색어를 ?q= 로 남겨 뒤로가기 지원, 탭 제목에 지금 곡, 내 서버가 아니면 안내
- 검색창: 최근 검색어 목록, 유튜브·스포티파이 주소를 붙여넣으면 곡/재생목록 바로 불러오기
- 다음 트랙: 섞기, 모두 지우기(두 번 눌러 확인), 자동재생 스위치, 곡 수·총 길이
- 재생바: 처음부터 다시(⏮), 섞기 버튼 / 서버 홈: 음성채널 표시, 사용 안내, 최근 검색 칩
- 곡 추가 중 스피너 + 중복 추가 방지, 검색 실패 시 다시 시도, play/playlist 응답 대기 15초
- 계정 전환형 프로필 메뉴와 사이드바 서버 목록 제거(프로필은 서버 변경·로그아웃만)
bot
- Redis 액션 queue_shuffle, player_recommend 추가
- player_now 응답에 recommend(자동재생), voiceChannelName 추가
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 재생목록/앨범: Spotify Web API 정책 변경으로 LavaSrc가 403으로 실패 → 봇이 공개 임베드에서
곡 목록(최대 100곡)을 받아 곡마다 유튜브 원곡을 매칭. 첫 곡 즉시 재생, 나머지는 백그라운드로 추가
- 단일 곡: 유튜브뮤직 검색이 팟캐스트·분석 영상을 줘 엉뚱한 곡이 걸리던 문제 → 제목·아티스트·
재생시간(±수 초)·2차영상 감점으로 유튜브 검색 결과 중 원곡 오디오를 선택(TrackMatch)
- open.spotify.com/intl-ko/... 링크도 인식
- 유튜브뮤직 검색 결과에서 팟캐스트 에피소드·재생목록 등 비곡 항목 제외
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
측정으로 확인한 낭비: YoutubeMusic.getSearchUrl 적중률이 6/10(평균 548ms)이라,
빗나가는 40% 에서는 YTM 을 끝까지 기다린 뒤 다시 ytsearch(~0.5초)를 도는 순차 대기가
통째로 버려지고 있었다.
변경: resolveQuery 기본 경로에서 ytsearch 폴백 검색을 먼저 띄워두고 YTM 결과를 기다린다.
- YTM 적중 -> 폴백 결과는 버리고 기존과 동일하게 동작(Lavalink 검색 1회가 헛돌 뿐)
- YTM 빗나감 -> 이미 진행 중인 폴백 결과를 그대로 사용(추가 대기 0)
- 버려지는 폴백 promise 에 catch 를 달아 unhandledRejection 방지
- " Topic" 중복 부가 방지 조건 추가
URL 입력/`-p`(스포티파이 우선) 경로는 기존과 동일하게 동작한다.
실측 A/B(YTM 이 빗나가는 3곡, 검색+해석+리졸브 구간):
기존 1500 / 1688 / 1391ms -> 신규 1224 / 1180 / 1399ms
재생은 3곡 모두 성공, 관찰 중 멈춤 0. tsc 통과.
- resolver/ytresolver.py 를 현재 운영본(.5 /opt/ytresolver.py)과 동일하게 갱신.
(직전 커밋에서 scripts/ 로 잘못 중복 저장했던 사본은 제거)
- 루트 README / bot README:
- 아키텍처 다이어그램에 3단계 폴백(내부API 0.3초 / yt-dlp 상주 1.7초 / 쿠키 6~8초) 표기
- "왜 리졸버가 필요한가"를 SABR 기준으로 갱신, upstream feat/sabr-support 가 55초에서
403으로 끊긴다는 검증 결과와 재검증 조건을 주석으로 남김
- 성능 메모를 현재 구조(병렬화·예열·내부API)와 실측치로 갱신
첫 소리 약 7초 -> 4.9초 -> 2.6초 -> 1.6~2.3초, 남은 병목은 음성접속·유튜브뮤직 검색
기존: player_play 가 channelJoin() 완료 후에야 search() 를 시작(직렬)하고,
첫 곡은 playNext 시점에 yt-dlp 리졸브(콜드 3.6~4.2s)를 그제서야 수행했다.
- LavalinkManager.search 를 resolveQuery(해석) + addResolved(큐 반영)로 분리.
resolveQuery 는 플레이어가 필요 없어 음성접속과 동시에 실행 가능.
- resolveQuery 가 해석 직후 prewarmTrack() 으로 첫 곡 오디오 리졸브를 즉시 예열.
GuildPlayer.resolvePlayable 과 동일한 target 계산식을 써 캐시가 확실히 적중하게 함.
- RedisClient player_play/player_playlist: 해석을 먼저 출발시키고 음성접속과 병렬 진행.
- 구간 계측 로그 추가(음성접속/해석대기/오디오 리졸브 대기).
검증: tsc 통과. 실측 A/B(동일 조건, 미캐시 곡):
기존 직렬 접속2.00s+리졸브3.03s = 5.03s → 신규 병렬 2.00s+0.97s = 2.98s (약 2s 단축).
jsdom 30은 require('undici') 시 webidl.util.markAsUncloneable is not a function
(node:20-alpine/undici 비호환)으로 import 단계에서 크래시 → 자식 프로세스 code 1.
jsdom 25.0.1은 undici 6를 사용해 해당 경로를 타지 않으며 node:20-alpine(musl)에서
npm ci 재현 빌드 + 실제 실행으로 poToken 생성·주입(204) 검증 완료.
또한 LavalinkManager 실패 로그가 마지막 줄(=Node.js 버전 푸터)만 남겨 원인을 가리던 문제를
전체 출력 꼬리를 남기도록 수정.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
googlevideo 재생 403(스트림 거절)은 Lavalink youtube-plugin에 유효한
poToken/visitorData가 없어서 발생. 봇이 노드 ready 시점과 2시간 주기로
poToken을 생성해 Lavalink의 POST /youtube 엔드포인트에 주입한다.
- scripts/potoken-refresh.mjs: youtubei.js+bgutils-js+jsdom로 생성 후 주입 (독립 ESM 프로세스)
- LavalinkManager: ready 이벤트 + 2h 인터벌로 자식 프로세스 실행
- deps: bgutils-js 3.2.0, jsdom, youtubei.js 추가 / Docker base node:22-alpine
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lavalink는 exception 직후 TrackEndEvent(loadFailed)를 보내 end 핸들러가 다음 곡을
진행시킨다. 기존 exception 핸들러의 errMsg는 5초 뒤 queue[0]을 다시 재생해, 이미
진행된 다음 곡을 재시작시키는 부작용이 있었음.
- errMsg에 retry 파라미터 추가(기본 true). exception 경로는 retry=false로 호출.
- exception이 end를 유발하므로 stopTrack은 부르지 않음(중복 end로 곡 하나 더 스킵 방지).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
노래가 2/3 지점 등에서 버퍼링/멈춘 뒤 다음 곡으로 넘어가지 않고 프리징되던 문제 해결.
- stuck 이벤트: 기존 errMsg는 5초 뒤 같은 곡(queue[0])을 다시 재생해 무한 반복될 수
있었음. stopTrack으로 end 이벤트를 유도해 실제 다음 곡으로 넘김.
- 재생 위치 멈춤 감시(watchdog) 추가: TrackStuckEvent가 안 뜨는 느린 버퍼링
상황에서도 position이 20초간 진행 없으면 자동 스킵. start에서 시작, clearAllTimers에서 정리.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
기존 player가 있어 channelJoin을 건너뛰는 경로에서 search()가 노드 미연결로
reject될 때 handleMessage는 이미 종료돼 .catch가 잡지 못하던 문제 수정.
- handleMessage 내부 search() await 처리
- prefix 명령어 경로의 messageRun() await 처리 (try/catch가 비동기 오류도 포착)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
노드가 CONNECTED 상태가 아닐 때 joinVoiceChannel이 throw 되어
fire-and-forget handleMessage에서 처리되지 않은 Promise 거부로 번지던 문제 수정.
- LavalinkManager.hasReadyNode() 추가
- channelJoin에서 노드 미연결 시 안내 임베드 반환 + join try/catch
- messageCreate의 handleMessage 호출에 .catch 추가
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
폴백 시 lastPlayed.identifier(Spotify ID)와 RD 믹스 첫 곡(YouTube ID)
비교가 안 맞아서, 방금 끝난 곡의 YouTube 버전이 자동재생 큐에 그대로
들어가던 버그 수정.
- youtubeMixFromSpotifyTrack: { result, seedYtId } 반환
- autoPlay: 폴백일 때 dedup 기준 ID를 seedYtId로 교체
- sprec이 EMPTY/ERROR/빈 PLAYLIST면 곡 메타데이터(artist, title)로
ytsearch 후 첫 결과의 videoId로 RD<id> 믹스를 가져와 자동재생 큐 채움
- 리뷰어 확인: ytmsearch는 현재 Lavalink에서 EMPTY를 반환해서 ytsearch 사용
- sprec / 검색 / 믹스 resolve 각 단계에 try/catch 추가 (한 단계 실패 시
다음 단계로 진행하지 않고 폴백 또는 end로 안전하게 빠짐)
- 진단 로깅: sprec loadType, 폴백 사유, 검색 매칭 videoId, 최종 후보 곡수
- closed 이벤트에서 code/reason/byRemote 로깅 (4006/4014/4015 등 원인 식별)
- 5초 후 봇이 보이스 채널에 남아있으면 player를 새로 만들어 현재 곡을
position 그대로 이어서 재생 (일시정지/볼륨 상태도 복원)
- 재접속 실패 시에만 기존처럼 player 정리
- 라우터/네트워크 일시 끊김(4006 세션 만료 등) 시 사용자 체감 끊김 최소화
- 이전 커밋에서 제거한 axios가 lock 파일에 남아있던 것 정리
- npm audit fix로 transitive 의존성 보안 패치 적용 완료
- 보안 취약점 0건 (이전: high 2, moderate 1)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- GuildPlayer: 타이머 레이스 컨디션 수정, 모든 타이머 정리 로직 통합 (clearAllTimers)
- GuildPlayer: 이벤트 핸들러에 try-catch 추가 (end, exception, stuck)
- GuildPlayer: start 이벤트에서 endTimer 정리, autoPlay tracks 길이 검증 추가
- RedisClient: player_seek, player_volume에 누락된 return ���가
- RedisClient: queue_remove 인덱스 검증 주석 명확화
- Handler: runCommand에 try-catch 추가하여 에러 시 사용자에게 응답
- Channel: getGuildById에 누락된 await 추가, getMemberById/getVoiceChannelById 안전한 에러 처리
- Command.d.ts: 잘못된 타입 ChatInputChatInputCommandInteraction → ChatInputCommandInteraction 수정
- join.ts: 채널 멘션 닫는 괄호 누락 수정
- shuffle.ts: 제네릭 타입 적용, 불필요한 5회 반복 제거
- import 경로 대소문자 수정 (Shuffle → shuffle) - Linux 호환
- YoutubeMusic/Spotify: 하드코딩된 IP를 환경변수로 분리
- console.log/error → Logger 통일 (YoutubeMusic, Button, channel)
- interactionCreate: 전체 try-catch 추가, silent catch에 로깅 추가
- Database: schema 경로 __dirname 기반으로 수정, 컬럼 화이트리스트 추가
- 사용하지 않는 코드 정리 (axios 의존성, 주석처리된 user 관련 코드)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>