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>
207 lines
14 KiB
Markdown
207 lines
14 KiB
Markdown
# music_bot_v2 — 봇(`bot/`)
|
|
|
|
디스코드 음악봇 본체. 검색·재생·대기열·자동재생·성인인증(연령제한) 영상 재생을 지원한다.
|
|
`discord.js` + `shoukaku`(Lavalink 클라이언트) 기반이며, 유튜브 재생은 Lavalink `youtube-plugin`을
|
|
쓰되 **연령제한/SABR로 막히는 영상은 yt-dlp 리졸버로 직접 오디오 URL을 뽑아 Lavalink `http` 소스로 우회 재생**한다.
|
|
|
|
> 이 문서는 봇 컴포넌트(`bot/`)와 이를 돕는 리포 루트의 `resolver/`·`scripts/`를 다룬다.
|
|
> 웹 대시보드는 [`../page/README.md`](../page/README.md), 리포 전체 개요는 [`../README.md`](../README.md) 참고.
|
|
|
|
---
|
|
|
|
## 1. 아키텍처
|
|
|
|
```
|
|
[Discord] ──▶ 봇 컨테이너(music1/2/3) ── shoukaku ──▶ Lavalink (.6:2333)
|
|
(.5 호스트, docker) │ └ youtube-plugin (일반 영상)
|
|
│
|
|
재생 직전 리졸브 요청
|
|
▼
|
|
ytresolver (.5:8779, systemd)
|
|
├ ① 유튜브 내부 API(VISIONOS) ~0.3초
|
|
├ ② yt-dlp 상주 호출 ~1.7초
|
|
└ ③ yt-dlp + 쿠키(연령제한) ~6~8초
|
|
│ → 직접 오디오 URL
|
|
그 URL을 Lavalink http 소스로 재생
|
|
```
|
|
|
|
- **봇**: `.5` 호스트에서 docker compose 로 `music1/2/3` 3개 인스턴스 구동. 소스는 `bot/`.
|
|
- **Lavalink**: `.6` 호스트(`192.168.10.6:2333`). 일반 유튜브/스포티파이 미러 재생 담당.
|
|
- **ytresolver**: `.5` 호스트에서 systemd 로 구동(`:8779`). 연령제한/SABR 우회용 오디오 URL 리졸버. 소스는 리포 루트 [`resolver/`](../resolver).
|
|
- **Redis**(선택): `.7`. 대시보드(`page/`) 제어/상태 연동(아래 6장, `RedisClient`).
|
|
- `.5`와 `.6`은 **같은 공인 IP**를 쓰므로, `.5`에서 뽑은 googlevideo URL을 `.6` Lavalink가 그대로 재생할 수 있다.
|
|
|
|
### 왜 리졸버가 필요한가
|
|
유튜브가 스트리밍을 **SABR**(서버 주도 적응 스트리밍)로 바꾼 뒤, Lavalink `youtube-plugin`(1.18.2)은
|
|
직접 스트림 URL을 받지 못해 실재생이 막힌다. 여기에 **한국 19금(본인인증 연령제한)** 영상은 OAuth 세션엔
|
|
열리지 않고(“This video requires login”) **웹 쿠키 세션**엔 열린다. 그래서 재생 직전에 직접 오디오 URL을
|
|
뽑아 Lavalink `http` 소스로 재생하는 우회 경로를 둔다.
|
|
|
|
> 참고: upstream `youtube-source`의 SABR 지원은 미병합 실험 브랜치(`feat/sabr-support`) 상태다.
|
|
> 실제로 올려 시험해 보면 첫 재생은 되지만 **약 55초 뒤 403으로 끊긴다**. 정식 릴리스에 SABR이
|
|
> 들어오면 그때 재검증할 것.
|
|
|
|
### 리졸버 3단계 폴백
|
|
1. **유튜브 내부 API(VISIONOS 클라이언트)** — player API 한 번으로 오디오 직링크(itag 251)를 받는다(**~0.3초**).
|
|
`visitorData`가 있어야 `LOGIN_REQUIRED`가 안 뜨며, 리졸버가 스스로 발급·캐시(6시간)하고 만료 시 재발급한다.
|
|
2. **yt-dlp 상주 호출** — zipapp(`/usr/local/bin/yt-dlp`)을 `sys.path`에 넣어 import하고 `YoutubeDL`
|
|
인스턴스를 재사용한다. 요청마다 프로세스를 띄우는 비용(실측 0.40초)을 없앤다(**~1.7초**).
|
|
3. **yt-dlp + 쿠키** — 1·2가 실패하는 연령제한(로그인 필요) 영상 전용(**~6~8초**). 계정을 불필요하게
|
|
태우지 않도록 **익명 먼저 → 실패 시에만 쿠키 재시도** 순서를 유지한다.
|
|
|
|
실측(2026-09-18, 운영 적용 후): 일반 유튜브 URL 리졸브 **274~559ms**(적용 전 1.7~2.3초),
|
|
연령제한 4건은 ③으로 폴백해 전부 재생 성공, 32분 연속 재생에서 멈춤 0/383회.
|
|
|
|
---
|
|
|
|
## 2. 설치 / 배포 (.5 호스트)
|
|
|
|
전제:
|
|
- `.5`에 Docker, `/root/bot/music_bot_v2-compose.yml`(3개 인스턴스 정의), `/root/bot/db`(sqlite + 쿠키) 존재.
|
|
- 리졸버 실행 호스트(.5)에 **`yt-dlp`** 와 **`node` v22 이상**, `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-dlp`
|
|
- `node 22`: 연령제한 영상의 yt-dlp EJS(시그니처/n-sig) 해독에 필요. **node 20 이하에서는 web 클라이언트가
|
|
“Requested format is not available”로 실패**한다. 예: `/opt/node22/bin/node` 를 `/usr/local/bin/node`로 심볼릭.
|
|
|
|
```bash
|
|
# 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`](.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`](../scripts/refresh-cookies.mjs)):
|
|
```bash
|
|
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`).
|
|
- `-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`,
|
|
`queue_shuffle`(다음 트랙 섞기), `player_recommend`(자동재생 토글 — 재생 중이 아니어도 바꿀 수 있음).
|
|
- `player_now` 응답에는 대시보드 표시용 `recommend`(자동재생 상태)와 `voiceChannelName`(봇이 있는 음성채널)도 들어 있다.
|
|
- **bot → site (실시간 상태)**: 상태가 바뀌면 봇이 `bot-site` 채널에 `{event, guildId}`를 publish
|
|
(`player_update`, `queue_update`). 사이트는 SSE로 브라우저에 흘려보낸다.
|
|
- 활성화: `bot/.env`의 `REDIS=true`, `REDIS_HOST`, `REDIS_PORT`.
|
|
|
|
---
|
|
|
|
## 7. 성능(재생 시작 지연) 메모
|
|
|
|
- **다음 곡 프리페치(prefetch)**: 현재 곡 재생 중 큐의 다음 곡을 미리 리졸브해 곡 전환 대기를 없앤다(`GuildPlayer`).
|
|
- **음성접속 ↔ 곡 해석 병렬화**: `player_play`가 `channelJoin()`을 기다렸다 검색하던 직렬 구조를 분리했다.
|
|
`LavalinkManager.resolveQuery()`(해석, 플레이어 불필요) + `addResolved()`(큐 반영)로 나눠, 해석을 먼저
|
|
출발시키고 음성접속과 동시에 진행한다.
|
|
- **첫 곡 프리워밍(prewarm) + 리졸버 요청 합치기(coalescing)**: 곡이 정해지는 즉시 `prewarmTrack()`이
|
|
리졸브를 걸어두고, 리졸버는 같은 영상의 진행 중 요청을 하나로 합친다(`ytresolver` INFLIGHT). 실제 재생
|
|
요청이 prewarm 진행분에 붙으므로 음성접속 시간 뒤로 리졸브가 숨는다.
|
|
- 캐시 키를 맞추려고 prewarm은 `resolvePlayable`과 **동일한 대상 문자열**을 쓴다(youtube면 `info.uri`).
|
|
- 리졸버는 리졸브된 URL을 30분 캐시한다(같은 곡 재생/재시도 시 즉시).
|
|
- **리졸브 자체 단축**: 내부 API 경로 도입으로 274~559ms(기존 yt-dlp subprocess 4.2~4.7초, 상주 호출 1.7~2.3초).
|
|
|
|
실측 추이(같은 곡 기준 첫 소리까지): **약 7초 → 4.9초(병렬화·예열) → 2.6초(상주 호출) → 1.6~2.3초(내부 API)**.
|
|
남은 지연은 음성 첫 접속 핸드셰이크(0.5~1.3초)와 유튜브뮤직 검색(0.5~0.9초)이다.
|
|
|
|
---
|
|
|
|
## 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).
|