diff --git a/README.md b/README.md index 00a607a..542f789 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,45 @@ # music_bot_v2 -디스코드 음악봇 v2. 검색·재생·대기열·자동재생·성인인증(연령제한) 영상 재생을 지원한다. -`discord.js` + `shoukaku`(Lavalink 클라이언트) 기반이며, 유튜브 재생은 Lavalink `youtube-plugin`을 -쓰되 **연령제한/SABR로 막히는 영상은 yt-dlp 리졸버로 직접 오디오 URL을 뽑아 Lavalink `http` 소스로 우회 재생**한다. +디스코드 음악봇 + 웹 대시보드 모노레포. 디스코드에서 명령으로 음악을 틀 수도 있고, 웹에서 로그인해 +검색·재생·대기열을 제어할 수도 있다. 성인인증(연령제한) 영상 재생까지 지원한다. ---- +## 구성 -## 1. 아키텍처 +| 디렉터리 | 내용 | 문서 | +|----------|------|------| +| [`bot/`](bot) | 디스코드 봇 본체(discord.js + shoukaku/Lavalink). 검색·재생·대기열·자동재생·연령제한 우회. | [bot/README.md](bot/README.md) | +| [`page/`](page) | 웹 대시보드(Next.js 16 + NextAuth + Redis). 브라우저에서 봇 제어·상태 표시. | [page/README.md](page/README.md) | +| [`resolver/`](resolver) | `ytresolver.py` — yt-dlp 기반 오디오 URL 리졸버(+systemd 유닛). 봇이 연령제한/SABR 우회에 사용. | 아래 + bot/README §1·2 | +| [`scripts/`](scripts) | `refresh-cookies.mjs` — 유튜브 쿠키(`ytcookie.txt`) 재발급 스크립트(Chromium). | bot/README §4 | + +## 전체 아키텍처 ``` -[Discord] ──▶ 봇 컨테이너(music1/2/3) ── shoukaku ──▶ Lavalink (.6:2333) - (.5 호스트, docker) │ └ youtube-plugin (일반 영상) - │ - 재생 직전 리졸브 요청 - ▼ - ytresolver (.5:8779, systemd) - └ yt-dlp (+쿠키) → 직접 오디오 URL - │ - 그 URL을 Lavalink http 소스로 재생 + ┌─────────────── 디스코드 제어 평면 ───────────────┐ +[Discord 유저] ── 명령 ──▶ 봇(music1/2/3, .5 docker) ── shoukaku ──▶ Lavalink(.6:2333) + │ ▲ └ youtube-plugin(일반) + │ │ 재생 직전 리졸브 + ▼ │ + ytresolver(.5:8779) ── yt-dlp(+쿠키) ──▶ 직접 오디오 URL + │ (Lavalink http 소스로 재생) + │ + ┌───────── Redis(.7) Pub/Sub + Key ─────────┐ ← 제어/상태 버스 + │ │ +[브라우저] ──▶ 웹 대시보드 page/(Next.js, NextAuth) ──┘ + · site→bot: "site-bot" 명령 + 결과키 폴링(botRpc) + · bot→site: "bot-site" 이벤트 → SSE 실시간 표시 ``` -- **봇**: `.5` 호스트에서 docker compose 로 `music1/2/3` 3개 인스턴스 구동. 소스는 `bot/`. -- **Lavalink**: `.6` 호스트(`192.168.10.6:2333`). 일반 유튜브/스포티파이 미러 재생 담당. -- **ytresolver**: `.5` 호스트에서 systemd 로 구동(`:8779`). 연령제한/SABR 우회용 오디오 URL 리졸버. -- **Redis**(선택): `.7`. 대시보드(`page/`) 상태 연동. -- `.5`와 `.6`은 **같은 공인 IP**를 쓰므로, `.5`에서 뽑은 googlevideo URL을 `.6` Lavalink가 그대로 재생할 수 있다. +- **봇**과 **대시보드**는 직접 연결되지 않고 **Redis**로 느슨하게 연동된다(대시보드는 Redis만 있으면 봇과 통신). +- 봇은 유튜브 연령제한 영상을 Lavalink만으로는 못 틀어서, `resolver/`가 쿠키로 직접 오디오 URL을 뽑아 우회한다. +- 봇 서버(.5)와 Lavalink(.6)가 같은 공인 IP라, .5에서 뽑은 스트림 URL을 .6이 그대로 재생한다. -### 왜 리졸버가 필요한가 -Lavalink `youtube-plugin`은 OAuth 로그인만 지원하는데, 유튜브는 **한국 19금(본인인증 연령제한)** 스트림을 -OAuth 세션엔 안 열어준다(“This video requires login”). 반면 **웹 쿠키 세션**엔 열어준다. 그래서 쿠키를 쓸 수 있는 -`yt-dlp`로 직접 오디오 URL을 뽑아 Lavalink `http` 소스로 재생하는 우회 경로를 둔다. 일반 영상은 계정을 태우지 -않도록 **익명 먼저 시도 → 실패 시에만 쿠키 재시도** 한다. +## 빠르게 보기 ---- +- 봇 배포/쿠키/검색·재생 동작·성능·트러블슈팅 → [bot/README.md](bot/README.md) +- 대시보드 개발/환경변수/Redis 통신 구조/API·UI → [page/README.md](page/README.md) -## 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`로 뜬다. `/root/bot/db:/db` 볼륨을 공유하므로 -sqlite DB(`music1.db` 등)와 쿠키(`ytcookie.txt`)가 컨테이너에서 `/db/...`로 보인다. - ---- - -## 3. 환경변수 (`bot/.env`) - -전체 목록/설명은 [`bot/.env.example`](bot/.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 자동/수동 로그인): -```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. 성능(재생 시작 지연) 메모 - -- **다음 곡 프리페치(prefetch)**: 현재 곡 재생 중 큐의 다음 곡을 미리 리졸브해 곡 전환 대기를 없앤다(`GuildPlayer`). -- **첫 곡 프리워밍(prewarm) + 리졸버 요청 합치기(coalescing)**: 봇이 유튜브 URL을 받으면 음성채널 접속(~2초) *전에* 리졸브를 미리 걸어두고(`LavalinkManager.prewarmYoutubeUrl`), 리졸버는 같은 영상의 진행 중 요청을 하나로 합친다(`ytresolver` INFLIGHT). 실제 재생 요청이 새 `yt-dlp`를 띄우지 않고 prewarm 진행분에 붙으므로, 음성 접속 시간과 리졸브 시간이 겹쳐 **첫 재생이 ~2초 단축**된다(실측 콜드 3.8초 → prewarm 후 1.7초). - - 캐시 키를 맞추려고 prewarm은 표준 watch URL(`https://www.youtube.com/watch?v=ID`)로 요청한다(= `resolvePlayable`이 쓰는 `info.uri`). -- 리졸버는 리졸브된 URL을 30분 캐시한다(같은 곡 재생/재시도 시 즉시). -- 남는 지연: `yt-dlp` 추출(~2.5초, 연령제한이면 쿠키 재시도로 더)과 음성 첫 접속 핸드셰이크는 제거 불가. 그래서 "완전 즉시"는 아니고 대략 7초 → 4~4.5초 수준. - ---- - -## 7. 트러블슈팅 - -- **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 설치 필수). +- `bot/.gitignore`가 `*.env`, `*.db`를 제외 → 실제 토큰/DB는 커밋되지 않는다. 예시는 [`bot/.env.example`](bot/.env.example). +- 원격: Gitea `git.tkrmagid.kr/tkrmagid/music_bot_v2` (`master`). diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 0000000..4e2e99d --- /dev/null +++ b/bot/README.md @@ -0,0 +1,179 @@ +# 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) + └ yt-dlp (+쿠키) → 직접 오디오 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가 그대로 재생할 수 있다. + +### 왜 리졸버가 필요한가 +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`** 와 **`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. + 봇은 처리 후 결과를 `":"` 키에 `setex`(예: `player:play:`, `queue:list:`). + 사이트는 그 키를 짧게 폴링해 결과를 받는다. + - action 예: `search`, `player_play`, `player_playlist`, `player_now`, `player_pause`, `player_skip`, + `player_seek`, `player_volume`, `queue_list`, `queue_set`, `queue_remove`. +- **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`), 리졸버는 같은 영상의 진행 중 요청을 하나로 합친다(`ytresolver` INFLIGHT). 실제 재생 요청이 새 `yt-dlp`를 띄우지 않고 prewarm 진행분에 붙으므로, 음성 접속 시간과 리졸브 시간이 겹쳐 **첫 재생이 ~2초 단축**된다(실측 콜드 3.8초 → prewarm 후 1.7초). + - 캐시 키를 맞추려고 prewarm은 표준 watch URL(`https://www.youtube.com/watch?v=ID`)로 요청한다(= `resolvePlayable`이 쓰는 `info.uri`). +- 리졸버는 리졸브된 URL을 30분 캐시한다(같은 곡 재생/재시도 시 즉시). +- 남는 지연: `yt-dlp` 추출(~2.5초, 연령제한이면 쿠키 재시도로 더)과 음성 첫 접속 핸드셰이크는 제거 불가. 그래서 "완전 즉시"는 아니고 대략 7초 → 4~4.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). diff --git a/page/README.md b/page/README.md index e215bc4..abd6767 100644 --- a/page/README.md +++ b/page/README.md @@ -1,36 +1,93 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# music_bot_v2 — 웹 대시보드(`page/`) -## Getting Started +디스코드 음악봇(`../bot`)을 브라우저에서 제어하는 웹 대시보드. 디스코드 로그인 후 내가 속한 서버를 골라 +검색·재생·대기열 관리·볼륨/탐색 등을 조작하고, 현재 재생 상태를 실시간으로 본다. -First, run the development server: +- **스택**: Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS 4 · NextAuth 4(Discord OAuth) · ioredis +- **봇과의 통신**: 직접 호출이 아니라 **Redis Pub/Sub + Key**로 봇과 주고받는다(아래 3장). +- 리포 전체 개요는 [`../README.md`](../README.md), 봇 본체는 [`../bot/README.md`](../bot/README.md) 참고. + +--- + +## 1. 빠른 시작 (개발) ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +cd page +npm install +# .env.local 작성 (2장 참고) +npm run dev # http://localhost:3000 ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +빌드/구동: `npm run build` → `npm run start`. 린트: `npm run lint`. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +> Redis(봇과 공유)와 디스코드 OAuth 앱이 있어야 실제 동작한다. Redis에 연결되지 않으면 부팅 시 에러를 던진다(`src/lib/Redis.ts`). -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +--- -## Learn More +## 2. 환경변수 (`.env.local`) -To learn more about Next.js, take a look at the following resources: +| 변수 | 설명 | +|------|------| +| `REDIS_HOST` / `REDIS_PORT` | 봇과 공유하는 Redis. 봇 `.env`의 값과 동일해야 한다(미설정 시 부팅 에러). | +| `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` | 디스코드 OAuth 앱 자격증명(로그인·서버목록 scope). | +| `NEXTAUTH_SECRET` | NextAuth 세션 서명 키. | +| `NEXTAUTH_URL` | 배포 URL(예: `https://music.example.com`). 개발은 `http://localhost:3000`. | -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +- 로그인 scope는 `identify email guilds`(유저 정보 + 서버 목록). `src/app/api/auth/[...nextauth]/route.ts`. +- 디스코드 개발자 포털의 OAuth2 Redirect에 `/api/auth/callback/discord`를 등록해야 한다. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +--- -## Deploy on Vercel +## 3. 봇과의 통신 구조 (Redis) -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +대시보드는 봇을 직접 호출하지 않고 **Redis를 메시지 버스**로 쓴다. 봇 쪽 구현은 `../bot/src/classes/RedisClient.ts`. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +### 3-1. 명령 (site → bot, 요청/응답 RPC) — `src/lib/api.ts`의 `botRpc` +``` +site: PUBLISH "site-bot" {action, serverId, userId, requestId, …} +bot : 처리 후 SETEX ":" (예: player:play:) 에 결과 저장 +site: 그 키를 짧게 폴링(100ms→최대400ms 백오프, 기본 3초 타임아웃)해 결과 수신 +``` +- `requestId`는 CSPRNG(`randomUUID`). 결과 키는 읽은 즉시 삭제 + TTL 보험. +- action: `search`, `player_play`, `player_playlist`, `player_now`, `player_pause`, `player_skip`, + `player_seek`, `player_volume`, `queue_list`, `queue_set`, `queue_remove`. + +### 3-2. 실시간 상태 (bot → site, SSE) — `src/lib/sse.ts`의 `botEventStream` +``` +bot : PUBLISH "bot-site" {event:"player_update"|"queue_update", guildId} +site: "bot-site" 구독 → serverId 일치하는 것만 EventSource(SSE)로 브라우저에 전달(30초 keep-alive) +``` +- 브라우저는 `/api/player/events?serverId=…`, `/api/queue/events?serverId=…`(SSE)를 구독해 + 상태 변경 알림을 받고 `now`/`queue/list`를 다시 당겨온다. + +--- + +## 4. API 라우트 (`src/app/api`) + +모든 라우트는 세션 가드(`requireSession`)를 거치고 `{ success, … }` 형태로 응답한다. + +- **player**: `play`, `pause`, `skip`, `seek`, `volume`, `playlist`, `now`, `events`(SSE) +- **queue**: `list`, `set`, `remove`, `events`(SSE) +- **servers**: 로그인 유저의 디스코드 서버 목록(봇이 있는 서버로 필터링) +- **search**: 검색(봇의 `search` action → Spotify/YT Music/YT Video 결과) +- **auth**: `[...nextauth]`(Discord OAuth) + +공용 헬퍼(`src/lib/api.ts`): `requireSession`, `botRpc`, `readJsonBody`, `requireString/Number/Boolean`, `errorResponse`. + +--- + +## 5. UI 구성 (`src/`) + +- `app/layout.tsx`, `app/page.tsx` — 루트 레이아웃/페이지. `components/Providers.tsx`(세션 등), `ToastProvider.tsx`. +- `components/layout/` — `TopNav`(상단/서버선택·로그인), `LeftSidebar`(탐색). +- `components/player/` — `PlayerBar`(재생바/볼륨/탐색), `MainContent`(검색·결과), `QueueSidebar`(대기열). +- `types/music.ts` — `DiscordServer`, `Track`/`TrackInfo`, `SearchTrack`, `SearchResults` 등 공용 도메인 타입. +- `next.config.ts` — 보안 헤더(CSP: self + discord/spotify/ytimg/google 이미지 CDN만 허용, `frame-ancestors 'none'` 등). + +--- + +## 6. 배포 메모 + +- Next.js 16 standalone/`next start`로 구동. 봇과 같은 Redis(.7)에 접근 가능해야 한다. +- `NEXTAUTH_URL`/디스코드 Redirect URI를 실제 도메인으로 맞춘다. +- 이 디렉터리는 Next가 제공하는 에이전트 규칙 파일(`AGENTS.md`, `CLAUDE.md`)을 둔다 — 코드 수정 전 참고.