- bot: reply() 헬퍼(LPUSH+EXPIRE) 추가, 67개 setex 응답 지점을 일괄 교체. 결과키를 리스트에 push해 사이트가 BRPOP으로 블로킹 수신하도록 함. - page(api.ts botRpc): short-polling 제거, 요청마다 전용 커넥션(duplicate)으로 BRPOP 대기 → 응답 즉시 수신(폴링 지연/헛 GET 제거), 전용 커넥션은 finally에서 정리. - search route: 사라진 pollIntervalMs 옵션 제거. 검증: 테스트봇(.9)+.7 Redis 실측 BRPOP 왕복 10ms, 고아 키 없음. bot tsc/page next build 통과.
music_bot_v2 — 봇(bot/)
디스코드 음악봇 본체. 검색·재생·대기열·자동재생·성인인증(연령제한) 영상 재생을 지원한다.
discord.js + shoukaku(Lavalink 클라이언트) 기반이며, 유튜브 재생은 Lavalink youtube-plugin을
쓰되 연령제한/SABR로 막히는 영상은 yt-dlp 리졸버로 직접 오디오 URL을 뽑아 Lavalink http 소스로 우회 재생한다.
이 문서는 봇 컴포넌트(
bot/)와 이를 돕는 리포 루트의resolver/·scripts/를 다룬다. 웹 대시보드는../page/README.md, 리포 전체 개요는../README.md참고.
1. 아키텍처
[Discord] ──▶ 봇 컨테이너(music1/2/3) ── shoukaku ──▶ Lavalink (.6:2333)
(.5 호스트, docker) │ └ youtube-plugin (일반 영상)
│
재생 직전 리졸브 요청
▼
ytresolver (.5:8779, systemd)
└ yt-dlp (+쿠키) → 직접 오디오 URL
│
그 URL을 Lavalink http 소스로 재생
- 봇:
.5호스트에서 docker compose 로music1/2/33개 인스턴스 구동. 소스는bot/. - Lavalink:
.6호스트(192.168.10.6:2333). 일반 유튜브/스포티파이 미러 재생 담당. - ytresolver:
.5호스트에서 systemd 로 구동(:8779). 연령제한/SABR 우회용 오디오 URL 리졸버. 소스는 리포 루트resolver/. - Redis(선택):
.7. 대시보드(page/) 제어/상태 연동(아래 6장,RedisClient). .5와.6은 같은 공인 IP를 쓰므로,.5에서 뽑은 googlevideo URL을.6Lavalink가 그대로 재생할 수 있다.
왜 리졸버가 필요한가
Lavalink youtube-plugin은 OAuth 로그인만 지원하는데, 유튜브는 한국 19금(본인인증 연령제한) 스트림을
OAuth 세션엔 안 열어준다(“This video requires login”). 반면 웹 쿠키 세션엔 열어준다. 그래서 쿠키를 쓸 수 있는
yt-dlp로 직접 오디오 URL을 뽑아 Lavalink http 소스로 재생하는 우회 경로를 둔다. 일반 영상은 계정을 태우지
않도록 익명 먼저 시도 → 실패 시에만 쿠키 재시도 한다.
2. 설치 / 배포 (.5 호스트)
전제:
.5에 Docker,/root/bot/music_bot_v2-compose.yml(3개 인스턴스 정의),/root/bot/db(sqlite + 쿠키) 존재.- 리졸버 실행 호스트(.5)에
yt-dlp와nodev22 이상,python3필요.yt-dlp:curl -fsSL https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp && chmod a+rx /usr/local/bin/yt-dlpnode 22: 연령제한 영상의 yt-dlp EJS(시그니처/n-sig) 해독에 필요. node 20 이하에서는 web 클라이언트가 “Requested format is not available”로 실패한다. 예:/opt/node22/bin/node를/usr/local/bin/node로 심볼릭.
# 1) 소스 최신화
cd /root/bot/music_bot_v2 && git pull
# 2) 봇 3개 빌드 + 재기동
cd /root/bot && docker compose -f music_bot_v2-compose.yml up -d --build
# 3) ytresolver(리졸버) 설치/갱신 (최초 1회 또는 리졸버 코드 변경 시)
cp /root/bot/music_bot_v2/resolver/ytresolver.py /opt/ytresolver.py
cp /root/bot/music_bot_v2/resolver/ytresolver.service /etc/systemd/system/ytresolver.service
systemctl daemon-reload && systemctl enable --now ytresolver
systemctl restart ytresolver # 코드 갱신 시
빌드는 bot/Dockerfile(node:20-alpine, npm run build=tsc)로 이루어진다.
인스턴스 정의(/root/bot/music_bot_v2-compose.yml)
music1/2/3 세 서비스가 각자 다른 TOKEN/APPID/PREFIX/DBPATH로 뜬다. 셋 다 build: ./music_bot_v2/bot
(같은 소스)이라 코드는 항상 동일하게 빌드되고, env만 다르다. /root/bot/db:/db 볼륨을 공유하므로
sqlite DB(music1.db 등)와 쿠키(ytcookie.txt)가 컨테이너에서 /db/...로 보인다.
3. 환경변수 (bot/.env)
전체 목록/설명은 .env.example 참고. 핵심만:
| 변수 | 설명 |
|---|---|
TOKEN / APPID |
디스코드 봇 토큰 / 앱 ID |
PREFIX |
명령어 접두사(예: m1;) |
DBPATH |
sqlite 경로(컨테이너 기준, 예 /db/music1.db) |
LAVALINK_HOST/PORT/PW |
Lavalink(.6:2333) 접속 정보 |
SPOTIFY_CLIENTID/SECRET |
스포티파이 검색/미러 |
YOUTUBE_COOKIE_PATH |
Netscape 쿠키 파일 경로(기본 /db/ytcookie.txt). 리졸버·검색 공용 쿠키 소스 |
YOUTUBE_COOKIE |
(선택·하위호환) 쿠키 헤더 문자열 인라인. 설정 시 이 값이 우선 |
YTRESOLVER_URL |
리졸버 주소(기본 http://192.168.10.5:8779) |
REDIS/REDIS_HOST/REDIS_PORT |
대시보드 제어/상태 연동(선택) |
bot/.gitignore가*.env,*.db를 제외하므로 실제.env와 DB는 커밋되지 않는다.
4. 유튜브 쿠키 (ytcookie.txt) — 성인인증/연령제한 재생의 핵심
- 무엇: 유튜브에 로그인된 계정의 세션 쿠키(Netscape
cookies.txt형식). yt-dlp가 연령제한 영상의 스트림을 받으려면 이 쿠키가 필요하다. 봇의 유튜브뮤직 검색도 이 쿠키를 헤더 문자열로 변환해 쓴다. - 위치:
.5호스트/root/bot/db/ytcookie.txt(= 컨테이너/db/ytcookie.txt, 리졸버도 같은 파일을 읽음). - 계정: 전용(버리는) 구글 계정을 쓰고 성인인증(본인확인)을 해둔 것. 본계정 사용 금지(밴 리스크).
- 형식(Netscape) 예:
최소한
# Netscape HTTP Cookie File .youtube.com TRUE / TRUE 2000000000 SID g.a000... .youtube.com TRUE / TRUE 2000000000 LOGIN_INFO AFm...SID,LOGIN_INFO,__Secure-1PSID/3PSID,SAPISID등 로그인 쿠키가 있어야 한다.
쿠키 만료 시 재발급 (증상: 19금만 안 되고 일반 영상은 정상)
방법 1 — 스크립트(Chromium 자동/수동 로그인, 소스는 리포 루트 scripts/refresh-cookies.mjs):
cd /root/bot/music_bot_v2
npm i -D playwright && npx playwright install chromium # 최초 1회
GOOGLE_EMAIL=<계정> GOOGLE_PASSWORD=<비번> \
OUT=/root/bot/db/ytcookie.txt node scripts/refresh-cookies.mjs
# 구글이 자동 로그인을 막으면 뜬 창에서 직접 로그인 → 자동 저장됨(디스플레이 있는 환경에서 실행)
방법 2 — 브라우저 확장(수동): PC 크롬에 Get cookies.txt LOCALLY 설치 → 전용 계정으로 youtube 로그인 →
Export → 받은 cookies.txt를 /root/bot/db/ytcookie.txt로 복사.
재발급 후: systemctl restart ytresolver 하면 즉시 반영(봇 재시작 불필요, 리졸버는 매 요청마다 파일을 읽음).
계정/비밀번호는 별도 안전한 곳(비밀번호 관리자/서버 secrets)에 보관한다.
5. 사용법 (디스코드)
- 지정 채널에
PREFIX + 검색어/URL→ 검색 후 재생, 대기열 추가. - 임베드의 버튼으로 일시정지/스킵/셔플/볼륨/자동재생 토글.
- 지원 소스: 유튜브(검색/URL), 유튜브뮤직, 스포티파이(미러), 플레이리스트.
- 자동재생: 마지막 곡 기준 추천(스포티파이 rec → 실패 시 유튜브 RD 믹스 폴백).
텍스트 검색 순서 & 플래그 (검색어 뒤에 -x 형태로 붙임)
- 기본(플래그 없음): 유튜브뮤직 우선(
YoutubeMusic.getSearchUrl) → 결과가 없거나 애매(커버/MR/노래방/방송/재생목록/검색어 무관)하면 자동으로ytsearch:… Topic(공식 오디오 우선) 폴백.- YTM API가 이 계정에선 음악 카탈로그 '노래'를 잘 못 줘서, 애매하면 버리고 폴백하도록 JUNK 필터 + 검색어-제목 관련성 검사를 둔다(
YoutubeMusic.JUNK_RE,getSearchUrl).
- YTM API가 이 계정에선 음악 카탈로그 '노래'를 잘 못 줘서, 애매하면 버리고 폴백하도록 JUNK 필터 + 검색어-제목 관련성 검사를 둔다(
-p: 스포티파이 우선 검색.-o:ytsearch에 "Topic" 미부가.-s: 플레이리스트 셔플 추가.-y: (구) 유튜브뮤직 강제 — 기본이 이미 YTM이라 사실상 동일.
6. 웹 대시보드 연동 (Redis)
대시보드(page/)는 Redis Pub/Sub + Key로 봇과 통신한다. 봇 쪽 구현은 src/classes/RedisClient.ts.
- site → bot (명령): 사이트가
site-bot채널에{action, serverId, userId, requestId, …}를 publish. 봇은 처리 후 결과를"<action-key>:<requestId>"키에setex(예:player:play:<reqId>,queue:list:<reqId>). 사이트는 그 키를 짧게 폴링해 결과를 받는다.- action 예:
search,player_play,player_playlist,player_now,player_pause,player_skip,player_seek,player_volume,queue_list,queue_set,queue_remove.
- action 예:
- bot → site (실시간 상태): 상태가 바뀌면 봇이
bot-site채널에{event, guildId}를 publish (player_update,queue_update). 사이트는 SSE로 브라우저에 흘려보낸다. - 활성화:
bot/.env의REDIS=true,REDIS_HOST,REDIS_PORT.
7. 성능(재생 시작 지연) 메모
- 다음 곡 프리페치(prefetch): 현재 곡 재생 중 큐의 다음 곡을 미리 리졸브해 곡 전환 대기를 없앤다(
GuildPlayer). - 첫 곡 프리워밍(prewarm) + 리졸버 요청 합치기(coalescing): 봇이 유튜브 URL을 받으면 음성채널 접속(~2초) 전에 리졸브를 미리 걸어두고(
LavalinkManager.prewarmYoutubeUrl), 리졸버는 같은 영상의 진행 중 요청을 하나로 합친다(ytresolverINFLIGHT). 실제 재생 요청이 새yt-dlp를 띄우지 않고 prewarm 진행분에 붙으므로, 음성 접속 시간과 리졸브 시간이 겹쳐 첫 재생이 ~2초 단축된다(실측 콜드 3.8초 → prewarm 후 1.7초).- 캐시 키를 맞추려고 prewarm은 표준 watch URL(
https://www.youtube.com/watch?v=ID)로 요청한다(=resolvePlayable이 쓰는info.uri).
- 캐시 키를 맞추려고 prewarm은 표준 watch URL(
- 리졸버는 리졸브된 URL을 30분 캐시한다(같은 곡 재생/재시도 시 즉시).
- 남는 지연:
yt-dlp추출(2.5초, 연령제한이면 쿠키 재시도로 더)과 음성 첫 접속 핸드셰이크는 제거 불가. 그래서 "완전 즉시"는 아니고 대략 7초 → 44.5초 수준.
8. 트러블슈팅
- 19금만 재생 안 됨: 쿠키 만료 → 4장 참고해
ytcookie.txt재발급 후systemctl restart ytresolver. - 전부 재생 안 됨: Lavalink(.6) 또는 ytresolver(.5) 다운 확인.
systemctl status ytresolver,curl http://192.168.10.5:8779/health, Lavalink:2333. - 유튜브뮤직 검색 품질: YTM API가 이 계정/엔드포인트에선 음악 카탈로그 '노래'를 거의 안 주고 일반 유튜브 영상(토크쇼·재생목록·MV 등)을 반환한다. 그래서 애매한 결과는 버리고
ytsearch:… Topic(공식 오디오)로 폴백하도록 해둠. 더 깨끗한 공식 오디오가 필요하면 검색에-p(스포티파이 우선)를 쓴다. - yt-dlp EJS(시그니처) 오류: 리졸버는 연령제한 영상에
--js-runtimes node가 필요하다(node 설치 필수).
9. 소스 구조 (bot/src)
index.ts— 부팅(클라이언트/LavalinkManager/Handler/Redis 초기화).classes/BotClient.ts— discord.js 클라이언트 래퍼.LavalinkManager.ts— shoukaku 노드/검색/프리워밍.GuildPlayer.ts— 길드별 큐·재생·프리페치·리졸브(resolvePlayable)·스톨 감시.RedisClient.ts— 대시보드 RPC/이벤트.Handler.ts— 명령 로딩.commands/—join,seek,channel,help,ping등.events/—messageCreate(접두사 재생),interactionCreate(버튼),voiceStateUpdate등.utils/—Config.ts(env),api/Spotify.ts·api/YoutubeMusic.ts(검색),music/*(URL 파싱·임베드·채널),Database.ts(sqlite).