Implement YouTube music playback workflow #2

Merged
myong merged 1 commits from myong_dev into main 2026-03-30 17:14:17 +00:00
14 changed files with 2893 additions and 10 deletions
Showing only changes of commit 85f5057fd7 - Show all commits

View File

@ -0,0 +1,314 @@
# Kord - YouTube 음악 재생 기능 기획안 (YouTube Music Playback)
## 체인지로그 (Changelog)
- **2026-03-30**: 최초 기획안 작성
---
## 1. 개요
YouTube 음악 재생 기능은 사용자가 텍스트 기반 명령으로 음악을 검색하거나 YouTube 링크를 입력하면,
현재 참여 중인 음성 채팅방에 봇이 입장하여 재생 목록(플레이리스트)을 관리하고 오디오를 재생하는 기능입니다.
이 기능은 서버 유틸리티를 넘어서 실시간 상호작용 기능으로 확장되는 첫 사례이며,
음성 연결, 큐 관리, UI 컨트롤, 외부 미디어 소스 처리까지 포함하는 비교적 큰 범위의 기능입니다.
### 목표
- 문자열 검색으로 YouTube 영상을 찾아 재생 목록에 추가할 수 있어야 함
- YouTube 링크를 입력해 직접 재생 목록에 추가할 수 있어야 함
- 현재 재생 목록을 조회할 수 있어야 함
- 인덱스 기반으로 재생 목록 항목을 삭제할 수 있어야 함
- 재생 중지 / 스킵 / 일시정지 / 재개 등 기본 컨트롤을 버튼 또는 이모지 기반 UI로 제공해야 함
- 음악 추가 요청 시, 요청자가 있는 음성 채팅방에 봇이 자동 입장하고 재생을 시작해야 함
- 마지막에 봇을 음성 채팅방에서 내보내는 기능이 있어야 함
---
## 2. 지원 범위 (MVP)
### 포함
- `/music add query:<문자열>`
- `/music add url:<YouTube 링크>`
- `/music queue`
- `/music remove index:<번호>`
- `/music skip`
- `/music stop`
- `/music leave`
- 재생 컨트롤 메시지 (버튼 또는 이모지 라벨 기반)
- 음성 채널 자동 입장 및 큐 기반 연속 재생
### 제외 (초기 범위 외)
- Spotify, SoundCloud 등 타 플랫폼 연동
- 반복 재생 / 셔플 / 볼륨 조절
- 길드별 DJ 역할 분리
- 재생 이력 저장
- 노래 가사 표시
---
## 3. 사용자 시나리오
### 시나리오 A: 검색어로 곡 추가
1. 사용자가 음성 채널에 입장한 상태에서 `/music add query:아이유 좋은날` 실행
2. 봇이 검색 결과 1위를 선택하거나 선택 UI를 제공
3. 재생 목록에 곡을 추가
4. 봇이 음성 채널에 자동 입장
5. 현재 재생 중이 없다면 즉시 재생 시작
### 시나리오 B: 링크로 곡 추가
1. 사용자가 `/music add url:https://www.youtube.com/watch?v=...` 실행
2. 봇이 링크 메타데이터를 파싱
3. 재생 목록에 항목 추가
4. 이미 재생 중이면 큐 뒤에 대기
### 시나리오 C: 큐 조회 및 삭제
1. 사용자가 `/music queue` 실행
2. 봇이 현재 재생곡과 대기열을 인덱스와 함께 Embed로 표시
3. 사용자가 `/music remove index:3` 실행
4. 3번 항목이 큐에서 제거됨
### 시나리오 D: 컨트롤 UI 사용
1. 재생 시작 시 봇이 "지금 재생 중" 메시지를 게시
2. 메시지에는 ⏸️, ▶️, ⏭️, ⏹️ 같은 버튼이 포함됨
3. 사용자가 버튼을 눌러 재생 상태를 제어
---
## 4. 명령 구조 제안
### `/music add`
입력 방식:
- `query`: YouTube 검색 문자열
- `url`: YouTube 링크
규칙:
- 두 옵션 중 하나만 입력
- 사용자는 반드시 음성 채널에 있어야 함
### `/music queue`
출력:
- 현재 재생 중 항목
- 대기열 목록
- 각 곡의 인덱스, 제목, 길이, 요청자
### `/music remove`
입력:
- `index`: 큐에서 제거할 번호
### `/music skip`
- 현재 곡을 스킵하고 다음 곡 재생
### `/music stop`
- 재생 중지
- 현재 곡 정지 및 대기열 비움 여부는 정책 선택 필요
### `/music leave`
- 봇을 음성 채널에서 퇴장시킴
- 기본 정책은 큐도 함께 정리
---
## 5. 재생 컨트롤 UI
### 컨트롤 메시지 구성
재생 시작 시 텍스트 채널에 Embed + 버튼 메시지 생성
권장 버튼:
- `⏸️` 일시정지
- `▶️` 재개
- `⏭️` 스킵
- `⏹️` 정지
- `📜` 큐 보기
### 정책
- 한 길드당 활성 컨트롤 메시지 1개 유지
- 곡이 바뀔 때 동일 메시지를 업데이트하거나 새 메시지를 생성할지 결정 필요
- MVP에서는 새 메시지 생성보다 기존 메시지 업데이트가 로그 노이즈를 줄이는 데 유리
---
## 6. 큐 및 재생 상태 설계
### 핵심 개념
- 재생 상태는 길드 단위로 분리
- 각 길드는 하나의 음성 연결과 하나의 큐를 가짐
- 큐는 메모리 기반으로 시작하고, 필요 시 DB 영속화 확장 가능
### 추천 구조
```ts
interface MusicQueueItem {
id: string;
title: string;
url: string;
durationSec?: number;
requestedByUserId: string;
}
interface GuildMusicSession {
guildId: string;
voiceChannelId: string;
textChannelId: string;
nowPlaying: MusicQueueItem | null;
queue: MusicQueueItem[];
paused: boolean;
controlMessageId?: string;
}
```
### 초기 전략
- 메모리 기반 세션 관리
- 봇 재시작 시 큐는 초기화됨
- 안정화 이후 DB 저장 여부 검토
---
## 7. 음성 입장 및 재생 흐름
### 자동 입장
- `/music add` 실행 시 사용자의 음성 채널을 확인
- 봇이 해당 채널에 없으면 자동 입장
- 이미 다른 채널에 있으면 정책 필요
### 기본 정책 제안
- 같은 길드 내에서 봇이 이미 재생 중이면 다른 채널 요청은 거부
- "현재 다른 음성 채널에서 사용 중" 메시지 제공
### 자동 퇴장
- `/music leave`로 수동 퇴장 가능
- 추가 정책 후보:
- 큐가 끝나고 1분 뒤 자동 퇴장
- 음성 채널에 봇만 남으면 자동 퇴장
---
## 8. YouTube 검색 및 스트리밍 전략
> [!IMPORTANT]
> YouTube 관련 기능은 라이브러리 안정성, 차단 이슈, 서비스 정책을 반드시 검토해야 합니다.
### 검색
가능한 접근:
- YouTube 공식 API 사용
- 서드파티 검색 라이브러리 사용
### 오디오 스트리밍
가능한 접근:
- `@discordjs/voice` 기반 음성 재생
- YouTube 스트림 추출 라이브러리 사용
### 기술 리스크
- YouTube 구조 변경 시 스트림 추출 라이브러리 고장 가능
- 지역 제한 / 연령 제한 / 라이브 영상 처리 문제
- 긴 재생 목록에서 메모리 및 연결 안정성 문제
---
## 9. 권한 및 운영 정책
### 봇 권한
- `Connect`
- `Speak`
- `View Channel`
- 텍스트 채널의 `Send Messages`, `Embed Links`
### 사용자 권한
- 기본적으로 일반 사용자도 곡 추가 가능
- `skip`, `stop`, `leave`, `remove`는 다음 중 하나로 제한 가능
- 관리자 전용
- 요청자 또는 관리자
- 같은 음성 채널 참여자 전원 허용
### 추천 MVP 정책
- `add`, `queue`: 같은 음성 채널 참여자 누구나 가능
- `skip`, `stop`, `remove`, `leave`: 관리자 또는 같은 음성 채널 참여자 허용
---
## 10. 에러 처리
필수 안내 케이스:
- 사용자가 음성 채널에 없음
- 유효하지 않은 YouTube 링크
- 검색 결과 없음
- 재생 목록 인덱스 범위 오류
- 봇 음성 권한 부족
- 스트림 로드 실패
에러 메시지는 기존 Error Guidance 체계와 연결하는 것이 좋습니다.
---
## 11. 구현 단계 (Phased Implementation)
| 단계 | 내용 |
|------|------|
| **Phase 1** | `@discordjs/voice` 기반 음성 연결, 메모리 큐, `/music add`, `/music queue`, `/music skip`, `/music leave` |
| **Phase 2** | 링크 기반 추가 + 검색 기반 추가 분리, `/music remove`, `/music stop` |
| **Phase 3** | 컨트롤 메시지(⏸️ ▶️ ⏭️ ⏹️) 및 상호작용 처리 |
| **Phase 4** | 자동 퇴장, 권한 정책 세분화, 예외 처리 고도화 |
| **Phase 5** | 반복 재생, 셔플, DJ 역할, 재생 이력 등 확장 기능 |
---
## 12. 검증 계획
### 수동 테스트
1. 음성 채널 입장 후 검색어로 곡 추가
2. 링크로 곡 추가
3. 큐 조회 및 인덱스 삭제
4. 스킵 / 정지 / 퇴장 동작 확인
5. 곡 종료 후 다음 곡 자동 재생 확인
6. 권한 부족 환경에서 적절한 에러 표시 확인
### 자동 테스트
- 큐 삽입 / 삭제 / 스킵 로직 단위 테스트
- 길드별 세션 분리 테스트
- 인덱스 유효성 검사 테스트
- 컨트롤 인터랙션 핸들러 테스트
---
## 13. 관련 문서
| 문서 | 링크 |
|------|------|
| 기능 로드맵 | [`Feature_Roadmap.md`](./Feature_Roadmap.md) |
| 임시 음성 채널 기획 | [`Temp_Voice_Channel_Plan.md`](./Temp_Voice_Channel_Plan.md) |
| 에러 안내 기획 | [`Error_Guidance_Plan.md`](./Error_Guidance_Plan.md) |

View File

@ -0,0 +1,87 @@
# Kord - YouTube 음악 재생 Phase 1 구현
## 변경 개요
`@discordjs/voice + youtubei.js` 조합을 기준으로 YouTube 음악 재생 기능의 1차 구현을 추가했다.
이번 단계에서는 길드별 메모리 큐와 음성 연결 세션을 도입하고, 검색 또는 영상 URL로 곡을 추가해 바로 재생할 수 있는 최소 워크플로우를 구현했다.
---
## 구현 범위
- `/music add`
- `query`로 YouTube 검색 후 첫 번째 영상 추가
- `url`로 YouTube 영상 링크 직접 추가
- `/music queue`
- 현재 재생 곡과 대기열 조회
- `/music skip`
- 현재 곡 스킵
- `/music leave`
- 봇 음성 채널 퇴장 및 큐 정리
- 재생 컨트롤 버튼
- `⏭ Skip`
- `⏹ Stop`
- `👋 Leave`
---
## 기술 구현
### 1. MusicService 추가
`src/services/MusicService.ts`
- 길드별 음악 세션을 메모리에서 관리
- `youtubei.js`를 lazy import로 초기화
- `getBasicInfo()`로 메타데이터 조회
- `getInfo()``server_abr_streaming_url` 또는 `hls_manifest_url`을 재생 소스로 사용
- `ffmpeg-static`으로 원격 스트림을 PCM으로 디코딩
- `@discordjs/voice` 플레이어에 연결
### 2. Slash Command 추가
`src/commands/music.ts`
- `/music add`
- `/music queue`
- `/music skip`
- `/music leave`
### 3. 버튼 상호작용 연결
`src/events/interactionCreate.ts`
- 재생 메시지의 `music_*` 버튼 상호작용 처리 추가
### 4. i18n 반영
- `src/i18n/types.ts`
- `src/i18n/locales/en.ts`
- `src/i18n/locales/ko.ts`
음악 명령/재생 상태/오류 안내에 필요한 번역 키를 추가했다.
### 5. 테스트 추가
`tests/services/MusicService.test.ts`
- YouTube URL 파싱
- 재생 시간 포맷팅
---
## 검증
- `yarn build`
- `yarn test --runInBand`
두 명령 모두 정상 통과했다.
---
## 현재 한계
- 이번 단계는 단일 영상 단위 큐 처리 기준이다.
- YouTube 재생 목록 URL 전체 추가, 인덱스 삭제, 상세한 권한 제어는 아직 포함하지 않았다.
- 스트림 URL은 `youtubei.js`의 현재 응답 구조에 의존하므로 향후 YouTube 응답 변경 시 보완이 필요할 수 있다.

View File

@ -0,0 +1,81 @@
# Kord - YouTube 음악 재생 Phase 2 구현
## 변경 개요
YouTube 음악 재생 기능의 Phase 2를 구현했다.
이번 단계에서는 재생 목록 관리 기능을 확장하고, 재생 중 메시지에 진행 바 UI를 추가해 현재 재생 상태를 더 직관적으로 볼 수 있도록 개선했다.
---
## 구현 범위
- `/music remove index:<번호>`
- 대기열에서 원하는 곡 삭제
- `/music stop`
- 현재 재생 중지 + 대기열 비우기
- YouTube 재생목록 URL 추가
- `list=` 파라미터가 있는 링크를 재생목록으로 인식
- 최대 100곡까지 대기열에 추가
- 재생 메시지 UI 개선
- 현재 재생 시간
- 전체 길이
- 진행 바 텍스트 UI
- 10초 간격 자동 갱신
---
## 기술 구현
### 1. MusicService 확장
`src/services/MusicService.ts`
- 길드 세션에 현재 곡 시작 시각(`nowPlayingStartedAt`)과 진행 바 갱신 인터벌(`progressInterval`) 추가
- `yt-dlp --dump-single-json --flat-playlist`를 사용해 재생목록 URL 파싱
- `remove()` 메서드 추가
- 현재 곡 진행률 계산 및 텍스트 진행 바 생성
- 재생 메시지를 10초 간격으로 업데이트
### 2. Slash Command 확장
`src/commands/music.ts`
- `/music remove`
- `/music stop`
- 재생목록 URL 응답 메시지 분기 추가
### 3. i18n 반영
- `src/i18n/types.ts`
- `src/i18n/locales/en.ts`
- `src/i18n/locales/ko.ts`
추가된 번역 키:
- remove / stop 명령 설명
- 인덱스 삭제 성공 / 범위 오류 안내
- 재생목록 추가 성공 메시지
- 진행 바 / 알 수 없는 길이 표기
### 4. 테스트 보강
`tests/services/MusicService.test.ts`
- YouTube 재생목록 URL 감지 테스트 추가
---
## 검증
- `yarn build`
- `yarn test --runInBand`
두 명령 모두 정상 통과했다.
---
## 참고 사항
- 진행 바는 길이를 알 수 있는 곡에만 10초 간격으로 갱신된다.
- 재생목록 URL은 최대 100곡까지 가져오도록 제한했다.
- 현재 재생 중인 곡은 `/music remove` 대상이 아니며, 대기열 곡만 삭제된다.

View File

@ -0,0 +1,45 @@
# 2026-03-31 - YouTube Music Playback Phase 3 Implementation
## Summary
Phase 3 focused on playback controls and now-playing UX improvements.
Implemented:
- pause and resume controls
- next-track preview in the now-playing message
- immediate control message refresh after queue and button actions
## Changes
### 1. Pause / Resume
- Added `/music pause`
- Added `/music resume`
- Added pause/resume control button toggle in the playback message
- Paused playback now freezes progress updates and resume restarts them
### 2. Next Track Preview
- The now-playing embed now shows the next queued track when one exists
- Queue length and next-track display are refreshed when tracks are added or removed
### 3. Immediate UI Refresh
- Button actions now use deferred updates and refresh the control message immediately
- Stop and leave actions move the playback message to the idle state without waiting for the periodic updater
- Queue mutations such as `add` and `remove` also refresh the control message
## Files
- `src/commands/music.ts`
- `src/services/MusicService.ts`
- `src/i18n/types.ts`
- `src/i18n/locales/en.ts`
- `src/i18n/locales/ko.ts`
## Validation
- `yarn build`
- `yarn test --runInBand`
Both commands completed successfully.

View File

@ -23,6 +23,7 @@
- [에러 안내 기능 기획서 (Error Guidance Plan)](Plans/Error_Guidance_Plan.md) - [에러 안내 기능 기획서 (Error Guidance Plan)](Plans/Error_Guidance_Plan.md)
- [다국어 지원 기획서 (i18n Plan)](Plans/i18n_Plan.md) - [다국어 지원 기획서 (i18n Plan)](Plans/i18n_Plan.md)
- [서버 이벤트 일정 관리 기능 기획안 (Event Schedule Management Plan)](Plans/Event_Schedule_Management_Plan.md) - [서버 이벤트 일정 관리 기능 기획안 (Event Schedule Management Plan)](Plans/Event_Schedule_Management_Plan.md)
- [YouTube 鞚岇晠 鞛<>儩 旮半姤 旮绊殟鞎<E6AE9F> (YouTube Music Playback Plan)](Plans/YouTube_Music_Playback_Plan.md)
## 아키텍처 및 정책 결정 (Decisions) ## 아키텍처 및 정책 결정 (Decisions)
@ -53,3 +54,6 @@
- [2026-03-30: 서버 이벤트 시작 시점 공지 구현 (Event Schedule Start Announcement Implementation)](WorkDone/2026-03-30_Event_Schedule_Start_Announcement_Implementation.md) - [2026-03-30: 서버 이벤트 시작 시점 공지 구현 (Event Schedule Start Announcement Implementation)](WorkDone/2026-03-30_Event_Schedule_Start_Announcement_Implementation.md)
- [2026-03-30: 이벤트 리마인더 분 단위 옵션 구현 (Event Reminder Offsets Implementation)](WorkDone/2026-03-30_Event_Reminder_Offsets_Implementation.md) - [2026-03-30: 이벤트 리마인더 분 단위 옵션 구현 (Event Reminder Offsets Implementation)](WorkDone/2026-03-30_Event_Reminder_Offsets_Implementation.md)
- [2026-03-30: 명령어 계층 구조 리팩토링 (Hierarchical Command Refactoring)](WorkDone/2026-03-30_HierarchicalRefactor.md) - [2026-03-30: 명령어 계층 구조 리팩토링 (Hierarchical Command Refactoring)](WorkDone/2026-03-30_HierarchicalRefactor.md)
- [2026-03-30: YouTube 澜厩 犁积 Phase 1 备泅 (YouTube Music Playback Phase 1 Implementation)](WorkDone/2026-03-30_YouTube_Music_Playback_Phase1_Implementation.md)
- [2026-03-31: YouTube 澜厩 犁积 Phase 2 备泅 (YouTube Music Playback Phase 2 Implementation)](WorkDone/2026-03-31_YouTube_Music_Playback_Phase2_Implementation.md)
- [2026-03-31: YouTube 澜厩 犁积 Phase 3 备泅 (YouTube Music Playback Phase 3 Implementation)](WorkDone/2026-03-31_YouTube_Music_Playback_Phase3_Implementation.md)

View File

@ -2,10 +2,15 @@
"name": "kord", "name": "kord",
"packageManager": "yarn@4.9.1", "packageManager": "yarn@4.9.1",
"dependencies": { "dependencies": {
"@discordjs/opus": "^0.10.0",
"@discordjs/voice": "^0.19.2",
"@prisma/client": "6.4.1", "@prisma/client": "6.4.1",
"discord.js": "^14.25.1", "discord.js": "^14.25.1",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"ioredis": "^5.10.1" "ffmpeg-static": "^5.3.0",
"ioredis": "^5.10.1",
"prism-media": "^1.3.5",
"youtubei.js": "^17.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",

431
src/commands/music.ts Normal file
View File

@ -0,0 +1,431 @@
import { ChatInputCommandInteraction, GuildMember, SlashCommandBuilder } from 'discord.js';
import { SupportedLocale, t } from '../i18n';
import { MusicService } from '../services/MusicService';
import { logger } from '../utils/logger';
function buildErrorMessage(locale: SupportedLocale, key: string) {
const message = t(locale, `commands.music.${key}`);
const resolution = t(locale, `commands.music.${key}Resolution`);
return resolution && resolution !== `commands.music.${key}Resolution`
? `${message}\n${resolution}`
: message;
}
async function respond(
interaction: ChatInputCommandInteraction,
content: string,
ephemeral = false,
) {
if (interaction.deferred) {
await interaction.editReply({ content });
return;
}
if (interaction.replied) {
await interaction.followUp({ content, ephemeral });
return;
}
await interaction.reply({ content, ephemeral });
}
export default {
data: new SlashCommandBuilder()
.setName('music')
.setDescription('Play YouTube audio in voice channels.')
.setDescriptionLocalizations({
ko: '음성 채널에서 YouTube 오디오를 재생합니다.',
})
.addSubcommand((subcommand) =>
subcommand
.setName('add')
.setDescription('Search YouTube or add a video URL to the queue.')
.setDescriptionLocalizations({
ko: 'YouTube를 검색하거나 영상 URL을 재생 목록에 추가합니다.',
})
.addStringOption((option) =>
option
.setName('query')
.setDescription('Search query for YouTube')
.setDescriptionLocalizations({
ko: 'YouTube 검색어',
}),
)
.addStringOption((option) =>
option
.setName('url')
.setDescription('YouTube video URL')
.setDescriptionLocalizations({
ko: 'YouTube 영상 URL',
}),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName('queue')
.setDescription('Show the current music queue.')
.setDescriptionLocalizations({
ko: '현재 음악 재생 목록을 표시합니다.',
}),
)
.addSubcommand((subcommand) =>
subcommand
.setName('remove')
.setDescription('Remove a track from the upcoming queue.')
.setDescriptionLocalizations({
ko: '대기열에서 원하는 곡을 삭제합니다.',
})
.addIntegerOption((option) =>
option
.setName('index')
.setDescription('Queue index to remove')
.setDescriptionLocalizations({
ko: '삭제할 대기열 인덱스',
})
.setRequired(true)
.setMinValue(1),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName('pause')
.setDescription('Pause the currently playing track.')
.setDescriptionLocalizations({
ko: '현재 재생 중인 곡을 일시정지합니다.',
}),
)
.addSubcommand((subcommand) =>
subcommand
.setName('resume')
.setDescription('Resume the paused track.')
.setDescriptionLocalizations({
ko: '일시정지된 곡의 재생을 다시 시작합니다.',
}),
)
.addSubcommand((subcommand) =>
subcommand
.setName('skip')
.setDescription('Skip the currently playing track.')
.setDescriptionLocalizations({
ko: '현재 재생 중인 곡을 건너뜁니다.',
}),
)
.addSubcommand((subcommand) =>
subcommand
.setName('stop')
.setDescription('Stop playback and clear the queue.')
.setDescriptionLocalizations({
ko: '재생을 중지하고 대기열을 비웁니다.',
}),
)
.addSubcommand((subcommand) =>
subcommand
.setName('leave')
.setDescription('Disconnect the bot from the voice channel.')
.setDescriptionLocalizations({
ko: '봇을 음성 채널에서 내보냅니다.',
}),
),
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
const subcommand = interaction.options.getSubcommand();
try {
if (subcommand === 'add') {
const query = interaction.options.getString('query');
const url = interaction.options.getString('url');
if ((!query && !url) || (query && url)) {
await interaction.reply({
content: buildErrorMessage(locale, 'addMutuallyExclusive'),
ephemeral: true,
});
return;
}
const member = interaction.member as GuildMember;
if (!member.voice.channel) {
await interaction.reply({
content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
});
return;
}
const textChannel = interaction.channel as any;
if (!textChannel?.send) {
await interaction.reply({
content: t(locale, 'errors.E3003.userMessage'),
ephemeral: true,
});
return;
}
await interaction.deferReply();
const result = query
? await MusicService.addFromQuery(member, textChannel, query, locale)
: await MusicService.addFromUrl(member, textChannel, url!, locale);
await interaction.editReply({
content: result.tracksAdded > 1
? (result.startedNow
? t(locale, 'commands.music.playlistAddedNowPlaying', {
count: String(result.tracksAdded),
channel: `<#${result.voiceChannelId}>`,
})
: t(locale, 'commands.music.playlistAddedLater', {
count: String(result.tracksAdded),
}))
: (result.startedNow
? t(locale, 'commands.music.queueAddedNowPlaying', {
title: result.track.title,
channel: `<#${result.voiceChannelId}>`,
})
: t(locale, 'commands.music.queueAddedLater', {
title: result.track.title,
position: String(result.position),
})),
});
return;
}
if (subcommand === 'queue') {
await interaction.reply({
embeds: [MusicService.getQueueEmbed(interaction.guildId!, locale)],
});
return;
}
if (subcommand === 'remove') {
const member = interaction.member as GuildMember;
if (!member.voice.channel) {
await interaction.reply({
content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
});
return;
}
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({
content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
});
return;
}
const index = interaction.options.getInteger('index', true);
const removed = await MusicService.remove(interaction.guildId!, index);
if (!removed) {
await interaction.reply({
content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
});
return;
}
await interaction.reply({
content: t(locale, 'commands.music.queueRemoved', {
title: removed.title,
}),
});
return;
}
if (subcommand === 'pause') {
const member = interaction.member as GuildMember;
if (!member.voice.channel) {
await interaction.reply({
content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
});
return;
}
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({
content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
});
return;
}
const paused = await MusicService.pause(interaction.guildId!, locale);
if (!paused) {
await interaction.reply({
content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
});
return;
}
await interaction.reply({ content: t(locale, 'commands.music.pauseSuccess') });
return;
}
if (subcommand === 'resume') {
const member = interaction.member as GuildMember;
if (!member.voice.channel) {
await interaction.reply({
content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
});
return;
}
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({
content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
});
return;
}
const resumed = await MusicService.resume(interaction.guildId!, locale);
if (!resumed) {
await interaction.reply({
content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
});
return;
}
await interaction.reply({ content: t(locale, 'commands.music.resumeSuccess') });
return;
}
if (subcommand === 'skip') {
const member = interaction.member as GuildMember;
if (!member.voice.channel) {
await interaction.reply({
content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
});
return;
}
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({
content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
});
return;
}
const skipped = await MusicService.skip(interaction.guildId!);
if (!skipped) {
await interaction.reply({
content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
});
return;
}
await interaction.reply({ content: t(locale, 'commands.music.skipSuccess') });
return;
}
if (subcommand === 'stop') {
const member = interaction.member as GuildMember;
if (!member.voice.channel) {
await interaction.reply({
content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
});
return;
}
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({
content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
});
return;
}
const stopped = await MusicService.stop(interaction.guildId!, locale);
if (!stopped) {
await interaction.reply({
content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
});
return;
}
await interaction.reply({ content: t(locale, 'commands.music.stopSuccess') });
return;
}
if (subcommand === 'leave') {
const member = interaction.member as GuildMember;
if (!member.voice.channel) {
await interaction.reply({
content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
});
return;
}
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({
content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
});
return;
}
const left = await MusicService.leave(interaction.guildId!, locale);
if (!left) {
await interaction.reply({
content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
});
return;
}
await interaction.reply({ content: t(locale, 'commands.music.leaveSuccess') });
}
} catch (error) {
logger.error('Error in music command:', error);
const knownError = error instanceof Error ? error.message : '';
if (knownError === 'NOT_IN_VOICE') {
await respond(interaction, buildErrorMessage(locale, 'notInVoice'), true);
return;
}
if (knownError === 'DIFFERENT_VOICE') {
await respond(interaction, buildErrorMessage(locale, 'differentVoiceChannel'), true);
return;
}
if (knownError === 'NO_SEARCH_RESULTS') {
await respond(interaction, buildErrorMessage(locale, 'noSearchResults'), true);
return;
}
if (knownError === 'INVALID_URL') {
await respond(interaction, buildErrorMessage(locale, 'invalidUrl'), true);
return;
}
if (knownError === 'QUEUE_INDEX_OUT_OF_RANGE') {
await respond(interaction, buildErrorMessage(locale, 'queueRemoveOutOfRange'), true);
return;
}
if (interaction.replied || interaction.deferred) {
await respond(interaction, t(locale, 'errors.E3003.userMessage'), true);
return;
}
await interaction.reply({
content: t(locale, 'errors.E3003.userMessage'),
ephemeral: true,
});
}
},
};

View File

@ -8,6 +8,7 @@ import { ErrorReporter, withErrorHandler } from '../errors/ErrorReporter';
import { t } from '../i18n'; import { t } from '../i18n';
import { getInteractionLocale } from '../i18n/localeHelper'; import { getInteractionLocale } from '../i18n/localeHelper';
import { handleSetupWizardInteraction } from '../interactions/handlers/setupWizardHandler'; import { handleSetupWizardInteraction } from '../interactions/handlers/setupWizardHandler';
import { MusicService } from '../services/MusicService';
export default { export default {
name: Events.InteractionCreate, name: Events.InteractionCreate,
@ -22,6 +23,12 @@ export default {
await command.execute(interaction, locale); await command.execute(interaction, locale);
}, locale); }, locale);
} }
else if (interaction.isButton() && interaction.customId.startsWith('music_')) {
const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => {
await MusicService.handleControlInteraction(interaction, locale);
}, locale);
}
else if (interaction.isMessageComponent() && interaction.customId.startsWith('setup_')) { else if (interaction.isMessageComponent() && interaction.customId.startsWith('setup_')) {
const locale = await getInteractionLocale(interaction); const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => { await withErrorHandler(interaction, async () => {

View File

@ -191,6 +191,73 @@ export const en: TranslationSchema = {
status: 'Status', status: 'Status',
}, },
}, },
music: {
description: 'Play YouTube audio in voice channels.',
addDescription: 'Search YouTube or add a video URL to the queue.',
queueDescription: 'Show the current music queue.',
removeDescription: 'Remove a track from the upcoming queue.',
pauseDescription: 'Pause the currently playing track.',
resumeDescription: 'Resume the paused track.',
skipDescription: 'Skip the currently playing track.',
stopDescription: 'Stop playback and clear the queue.',
leaveDescription: 'Disconnect the bot from the voice channel.',
queryDescription: 'Search query for YouTube',
urlDescription: 'YouTube video URL',
indexDescription: 'Queue index to remove',
addMutuallyExclusive: 'Choose either a search query or a YouTube URL.',
addMutuallyExclusiveResolution: 'Provide exactly one of `query` or `url`.',
notInVoice: 'You must be in a voice channel to use music commands.',
notInVoiceResolution: 'Join a voice channel first, then try again.',
differentVoiceChannel: 'Music is already being used in another voice channel.',
differentVoiceChannelResolution: 'Join the same voice channel as the bot or wait until the current session ends.',
noSearchResults: 'No YouTube results were found for that query.',
noSearchResultsResolution: 'Try a more specific search phrase or use a direct YouTube URL.',
invalidUrl: 'The provided YouTube URL is invalid.',
invalidUrlResolution: 'Use a standard `youtube.com` or `youtu.be` video link.',
noActiveSession: 'There is no active music session in this server.',
noActiveSessionResolution: 'Add a track first to start playback.',
queueAddedNowPlaying: 'Added **{{title}}** and started playback in {{channel}}.',
queueAddedLater: 'Added **{{title}}** to the queue. Position: `#{{position}}`.',
playlistAddedNowPlaying: 'Added **{{count}}** tracks from the playlist and started playback in {{channel}}.',
playlistAddedLater: 'Added **{{count}}** tracks from the playlist to the queue.',
queueTitle: 'Music Queue',
queueEmpty: 'The music queue is currently empty.',
queueNowPlaying: 'Now Playing',
queueUpcoming: 'Up Next',
queueMoreItems: '...and **{{count}}** more track(s).',
queueRemoved: 'Removed **{{title}}** from the queue.',
queueRemoveOutOfRange: 'That queue index does not exist.',
queueRemoveOutOfRangeResolution: 'Use `/music queue` to check the current queue indexes first.',
pauseSuccess: 'Paused the current track.',
resumeSuccess: 'Resumed playback.',
skipSuccess: 'Skipped the current track.',
leaveSuccess: 'Disconnected from the voice channel and cleared the queue.',
stopSuccess: 'Stopped playback and cleared the queue.',
playbackStartedTitle: 'Now Playing',
playbackIdleTitle: 'Queue Finished',
playbackIdleBody: 'There are no more tracks in the queue.',
playbackFailed: 'Failed to play **{{title}}**. Skipping to the next track.',
playbackFailedResolution: 'The stream could not be loaded from YouTube.',
streamUnavailable: 'Could not load a playable audio stream for this video.',
streamUnavailableResolution: 'Try another video or add the track again later.',
requestedBy: 'Requested by',
duration: 'Duration',
progress: 'Progress',
source: 'Source',
status: 'Status',
queueLength: 'Queue Length',
nextTrack: 'Next Track',
statusPlaying: 'Playing',
statusPaused: 'Paused',
unknownDuration: 'Unknown',
buttons: {
pause: '⏸ Pause',
resume: '▶ Resume',
skip: '⏭ Skip',
stop: '⏹ Stop',
leave: '👋 Leave',
},
},
permissionAudit: { permissionAudit: {
title: 'Bot Permission Audit Report', title: 'Bot Permission Audit Report',
channel: 'Channel', channel: 'Channel',

View File

@ -191,6 +191,73 @@ export const ko: TranslationSchema = {
status: '상태', status: '상태',
}, },
}, },
music: {
description: 'Play YouTube audio in voice channels.',
addDescription: 'Search YouTube or add a video URL to the queue.',
queueDescription: 'Show the current music queue.',
removeDescription: 'Remove a track from the upcoming queue.',
pauseDescription: 'Pause the currently playing track.',
resumeDescription: 'Resume the paused track.',
skipDescription: 'Skip the currently playing track.',
stopDescription: 'Stop playback and clear the queue.',
leaveDescription: 'Disconnect the bot from the voice channel.',
queryDescription: 'Search query for YouTube',
urlDescription: 'YouTube video URL',
indexDescription: 'Queue index to remove',
addMutuallyExclusive: 'Choose either a search query or a YouTube URL.',
addMutuallyExclusiveResolution: 'Provide exactly one of `query` or `url`.',
notInVoice: 'You must be in a voice channel to use music commands.',
notInVoiceResolution: 'Join a voice channel first, then try again.',
differentVoiceChannel: 'Music is already being used in another voice channel.',
differentVoiceChannelResolution: 'Join the same voice channel as the bot or wait until the current session ends.',
noSearchResults: 'No YouTube results were found for that query.',
noSearchResultsResolution: 'Try a more specific search phrase or use a direct YouTube URL.',
invalidUrl: 'The provided YouTube URL is invalid.',
invalidUrlResolution: 'Use a standard `youtube.com` or `youtu.be` video link.',
noActiveSession: 'There is no active music session in this server.',
noActiveSessionResolution: 'Add a track first to start playback.',
queueAddedNowPlaying: 'Added **{{title}}** and started playback in {{channel}}.',
queueAddedLater: 'Added **{{title}}** to the queue. Position: `#{{position}}`.',
playlistAddedNowPlaying: 'Added **{{count}}** tracks from the playlist and started playback in {{channel}}.',
playlistAddedLater: 'Added **{{count}}** tracks from the playlist to the queue.',
queueTitle: 'Music Queue',
queueEmpty: 'The music queue is currently empty.',
queueNowPlaying: 'Now Playing',
queueUpcoming: 'Up Next',
queueMoreItems: '...and **{{count}}** more track(s).',
queueRemoved: 'Removed **{{title}}** from the queue.',
queueRemoveOutOfRange: 'That queue index does not exist.',
queueRemoveOutOfRangeResolution: 'Use `/music queue` to check the current queue indexes first.',
pauseSuccess: 'Paused the current track.',
resumeSuccess: 'Resumed playback.',
skipSuccess: 'Skipped the current track.',
leaveSuccess: 'Disconnected from the voice channel and cleared the queue.',
stopSuccess: 'Stopped playback and cleared the queue.',
playbackStartedTitle: 'Now Playing',
playbackIdleTitle: 'Queue Finished',
playbackIdleBody: 'There are no more tracks in the queue.',
playbackFailed: 'Failed to play **{{title}}**. Skipping to the next track.',
playbackFailedResolution: 'The stream could not be loaded from YouTube.',
streamUnavailable: 'Could not load a playable audio stream for this video.',
streamUnavailableResolution: 'Try another video or add the track again later.',
requestedBy: 'Requested by',
duration: 'Duration',
progress: 'Progress',
source: 'Source',
status: 'Status',
queueLength: 'Queue Length',
nextTrack: 'Next Track',
statusPlaying: 'Playing',
statusPaused: 'Paused',
unknownDuration: 'Unknown',
buttons: {
pause: 'Pause',
resume: 'Resume',
skip: 'Skip',
stop: 'Stop',
leave: 'Leave',
},
},
permissionAudit: { permissionAudit: {
title: '봇 권한 진단 보고서', title: '봇 권한 진단 보고서',
channel: '채널', channel: '채널',

View File

@ -145,6 +145,73 @@ export interface TranslationSchema {
status: string; status: string;
}; };
}; };
music: {
description: string;
addDescription: string;
queueDescription: string;
removeDescription: string;
pauseDescription: string;
resumeDescription: string;
skipDescription: string;
stopDescription: string;
leaveDescription: string;
queryDescription: string;
urlDescription: string;
indexDescription: string;
addMutuallyExclusive: string;
addMutuallyExclusiveResolution: string;
notInVoice: string;
notInVoiceResolution: string;
differentVoiceChannel: string;
differentVoiceChannelResolution: string;
noSearchResults: string;
noSearchResultsResolution: string;
invalidUrl: string;
invalidUrlResolution: string;
noActiveSession: string;
noActiveSessionResolution: string;
queueAddedNowPlaying: string;
queueAddedLater: string;
playlistAddedNowPlaying: string;
playlistAddedLater: string;
queueTitle: string;
queueEmpty: string;
queueNowPlaying: string;
queueUpcoming: string;
queueMoreItems: string;
queueRemoved: string;
queueRemoveOutOfRange: string;
queueRemoveOutOfRangeResolution: string;
pauseSuccess: string;
resumeSuccess: string;
skipSuccess: string;
leaveSuccess: string;
stopSuccess: string;
playbackStartedTitle: string;
playbackIdleTitle: string;
playbackIdleBody: string;
playbackFailed: string;
playbackFailedResolution: string;
streamUnavailable: string;
streamUnavailableResolution: string;
requestedBy: string;
duration: string;
progress: string;
source: string;
status: string;
queueLength: string;
nextTrack: string;
statusPlaying: string;
statusPaused: string;
unknownDuration: string;
buttons: {
pause: string;
resume: string;
skip: string;
stop: string;
leave: string;
};
};
permissionAudit: { permissionAudit: {
title: string; title: string;
channel: string; channel: string;

1039
src/services/MusicService.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
import { extractYouTubeVideoId, formatDuration, isYouTubePlaylistUrl } from '../../src/services/MusicService';
describe('MusicService helpers', () => {
it('extracts a video id from standard watch URLs', () => {
expect(extractYouTubeVideoId('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
});
it('extracts a video id from short URLs', () => {
expect(extractYouTubeVideoId('https://youtu.be/dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
});
it('returns null for invalid URLs', () => {
expect(extractYouTubeVideoId('https://example.com/watch?v=dQw4w9WgXcQ')).toBeNull();
});
it('detects playlist URLs', () => {
expect(isYouTubePlaylistUrl('https://www.youtube.com/playlist?list=PL1234567890')).toBe(true);
expect(isYouTubePlaylistUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PL1234567890')).toBe(true);
expect(isYouTubePlaylistUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe(false);
});
it('formats durations consistently', () => {
expect(formatDuration(65)).toBe('01:05');
expect(formatDuration(3665)).toBe('1:01:05');
});
});

661
yarn.lock
View File

@ -381,6 +381,25 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@bufbuild/protobuf@npm:^2.0.0":
version: 2.11.0
resolution: "@bufbuild/protobuf@npm:2.11.0"
checksum: 10c0/d54fffd414660b823999cc321d26bd6c5f18a6e75343fc7d2588bda5be540ec542b557ac1f03d6d4b6e9d3e5596b2016e58cda173cd1858c043f0e846ece453f
languageName: node
linkType: hard
"@derhuerst/http-basic@npm:^8.2.0":
version: 8.2.4
resolution: "@derhuerst/http-basic@npm:8.2.4"
dependencies:
caseless: "npm:^0.12.0"
concat-stream: "npm:^2.0.0"
http-response-object: "npm:^3.0.1"
parse-cache-control: "npm:^1.0.1"
checksum: 10c0/f500c53d8f587ce980f55638d9912ba03652b7483181b90ed41d74fb7eae85c6e05b2f739d97860413886ada9d58d169ec49208237f62c55bdd542f8003f8389
languageName: node
linkType: hard
"@discordjs/builders@npm:^1.13.0": "@discordjs/builders@npm:^1.13.0":
version: 1.14.0 version: 1.14.0
resolution: "@discordjs/builders@npm:1.14.0" resolution: "@discordjs/builders@npm:1.14.0"
@ -419,6 +438,35 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@discordjs/node-pre-gyp@npm:^0.4.5":
version: 0.4.5
resolution: "@discordjs/node-pre-gyp@npm:0.4.5"
dependencies:
detect-libc: "npm:^2.0.0"
https-proxy-agent: "npm:^5.0.0"
make-dir: "npm:^3.1.0"
node-fetch: "npm:^2.6.7"
nopt: "npm:^5.0.0"
npmlog: "npm:^5.0.1"
rimraf: "npm:^3.0.2"
semver: "npm:^7.3.5"
tar: "npm:^6.1.11"
bin:
node-pre-gyp: bin/node-pre-gyp
checksum: 10c0/617e2d6efd00d56d2a276670e8592ee2d6de7b8149a4d8e38fea7b8effae676944f46be2867c6964a0bd7b4c9923e3cd0027f6ef29954848500de3efe867a94e
languageName: node
linkType: hard
"@discordjs/opus@npm:^0.10.0":
version: 0.10.0
resolution: "@discordjs/opus@npm:0.10.0"
dependencies:
"@discordjs/node-pre-gyp": "npm:^0.4.5"
node-addon-api: "npm:^8.1.0"
checksum: 10c0/d7cb5f95c4b9556b3dc6b32e5a5f589396230fcdce33e4aab98d8623bcc8895b0776116255ffb93c5dcca0fb98db3e39b6b70f1687dc2104e0a9f189b5e28362
languageName: node
linkType: hard
"@discordjs/rest@npm:^2.5.1, @discordjs/rest@npm:^2.6.0": "@discordjs/rest@npm:^2.5.1, @discordjs/rest@npm:^2.6.0":
version: 2.6.1 version: 2.6.1
resolution: "@discordjs/rest@npm:2.6.1" resolution: "@discordjs/rest@npm:2.6.1"
@ -445,6 +493,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@discordjs/voice@npm:^0.19.2":
version: 0.19.2
resolution: "@discordjs/voice@npm:0.19.2"
dependencies:
"@snazzah/davey": "npm:^0.1.9"
"@types/ws": "npm:^8.18.1"
discord-api-types: "npm:^0.38.41"
prism-media: "npm:^1.3.5"
tslib: "npm:^2.8.1"
ws: "npm:^8.19.0"
checksum: 10c0/e45c8d2ce7297a7b665044e900da32dea0232c060c3a66235c6e164252ad17c1471b42ba3d074d5d9e3ca61bea4636a6c00854c51fee91ab2676cbf2a993a367
languageName: node
linkType: hard
"@discordjs/ws@npm:^1.2.3": "@discordjs/ws@npm:^1.2.3":
version: 1.2.3 version: 1.2.3
resolution: "@discordjs/ws@npm:1.2.3" resolution: "@discordjs/ws@npm:1.2.3"
@ -1143,6 +1205,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@napi-rs/wasm-runtime@npm:^1.1.2":
version: 1.1.2
resolution: "@napi-rs/wasm-runtime@npm:1.1.2"
dependencies:
"@tybys/wasm-util": "npm:^0.10.1"
peerDependencies:
"@emnapi/core": ^1.7.1
"@emnapi/runtime": ^1.7.1
checksum: 10c0/725c30ec9c480a8d0c1a6a4ce31dc6c830365d485e23ad560e143d1cb9db89a0c95fbb5b9d53c07121729817a3683db6f1ab65d7e4f38fa7482a11b15ef6c6fd
languageName: node
linkType: hard
"@npmcli/agent@npm:^4.0.0": "@npmcli/agent@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "@npmcli/agent@npm:4.0.0" resolution: "@npmcli/agent@npm:4.0.0"
@ -1303,7 +1377,158 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tybys/wasm-util@npm:^0.10.0": "@snazzah/davey-android-arm-eabi@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-android-arm-eabi@npm:0.1.11"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@snazzah/davey-android-arm64@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-android-arm64@npm:0.1.11"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@snazzah/davey-darwin-arm64@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-darwin-arm64@npm:0.1.11"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@snazzah/davey-darwin-x64@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-darwin-x64@npm:0.1.11"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@snazzah/davey-freebsd-x64@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-freebsd-x64@npm:0.1.11"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@snazzah/davey-linux-arm-gnueabihf@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-linux-arm-gnueabihf@npm:0.1.11"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@snazzah/davey-linux-arm64-gnu@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-linux-arm64-gnu@npm:0.1.11"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@snazzah/davey-linux-arm64-musl@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-linux-arm64-musl@npm:0.1.11"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@snazzah/davey-linux-x64-gnu@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-linux-x64-gnu@npm:0.1.11"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@snazzah/davey-linux-x64-musl@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-linux-x64-musl@npm:0.1.11"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@snazzah/davey-wasm32-wasi@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-wasm32-wasi@npm:0.1.11"
dependencies:
"@napi-rs/wasm-runtime": "npm:^1.1.2"
conditions: cpu=wasm32
languageName: node
linkType: hard
"@snazzah/davey-win32-arm64-msvc@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-win32-arm64-msvc@npm:0.1.11"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@snazzah/davey-win32-ia32-msvc@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-win32-ia32-msvc@npm:0.1.11"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@snazzah/davey-win32-x64-msvc@npm:0.1.11":
version: 0.1.11
resolution: "@snazzah/davey-win32-x64-msvc@npm:0.1.11"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@snazzah/davey@npm:^0.1.9":
version: 0.1.11
resolution: "@snazzah/davey@npm:0.1.11"
dependencies:
"@snazzah/davey-android-arm-eabi": "npm:0.1.11"
"@snazzah/davey-android-arm64": "npm:0.1.11"
"@snazzah/davey-darwin-arm64": "npm:0.1.11"
"@snazzah/davey-darwin-x64": "npm:0.1.11"
"@snazzah/davey-freebsd-x64": "npm:0.1.11"
"@snazzah/davey-linux-arm-gnueabihf": "npm:0.1.11"
"@snazzah/davey-linux-arm64-gnu": "npm:0.1.11"
"@snazzah/davey-linux-arm64-musl": "npm:0.1.11"
"@snazzah/davey-linux-x64-gnu": "npm:0.1.11"
"@snazzah/davey-linux-x64-musl": "npm:0.1.11"
"@snazzah/davey-wasm32-wasi": "npm:0.1.11"
"@snazzah/davey-win32-arm64-msvc": "npm:0.1.11"
"@snazzah/davey-win32-ia32-msvc": "npm:0.1.11"
"@snazzah/davey-win32-x64-msvc": "npm:0.1.11"
dependenciesMeta:
"@snazzah/davey-android-arm-eabi":
optional: true
"@snazzah/davey-android-arm64":
optional: true
"@snazzah/davey-darwin-arm64":
optional: true
"@snazzah/davey-darwin-x64":
optional: true
"@snazzah/davey-freebsd-x64":
optional: true
"@snazzah/davey-linux-arm-gnueabihf":
optional: true
"@snazzah/davey-linux-arm64-gnu":
optional: true
"@snazzah/davey-linux-arm64-musl":
optional: true
"@snazzah/davey-linux-x64-gnu":
optional: true
"@snazzah/davey-linux-x64-musl":
optional: true
"@snazzah/davey-wasm32-wasi":
optional: true
"@snazzah/davey-win32-arm64-msvc":
optional: true
"@snazzah/davey-win32-ia32-msvc":
optional: true
"@snazzah/davey-win32-x64-msvc":
optional: true
checksum: 10c0/f0608e2e799f49272e15b6143c49b15a0c10046bdcbbc389778fc9a716b7c9236222dabc09d105c0225bff77733537bc43959bb724e6c3cc4ffe4225af155b2f
languageName: node
linkType: hard
"@tybys/wasm-util@npm:^0.10.0, @tybys/wasm-util@npm:^0.10.1":
version: 0.10.1 version: 0.10.1
resolution: "@tybys/wasm-util@npm:0.10.1" resolution: "@tybys/wasm-util@npm:0.10.1"
dependencies: dependencies:
@ -1418,6 +1643,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:^10.0.3":
version: 10.17.60
resolution: "@types/node@npm:10.17.60"
checksum: 10c0/0742294912a6e79786cdee9ed77cff6ee8ff007b55d8e21170fc3e5994ad3a8101fea741898091876f8dc32b0a5ae3d64537b7176799e92da56346028d2cbcd2
languageName: node
linkType: hard
"@types/stack-utils@npm:^2.0.3": "@types/stack-utils@npm:^2.0.3":
version: 2.0.3 version: 2.0.3
resolution: "@types/stack-utils@npm:2.0.3" resolution: "@types/stack-utils@npm:2.0.3"
@ -1425,7 +1657,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/ws@npm:^8.5.10": "@types/ws@npm:^8.18.1, @types/ws@npm:^8.5.10":
version: 8.18.1 version: 8.18.1
resolution: "@types/ws@npm:8.18.1" resolution: "@types/ws@npm:8.18.1"
dependencies: dependencies:
@ -1734,6 +1966,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"abbrev@npm:1":
version: 1.1.1
resolution: "abbrev@npm:1.1.1"
checksum: 10c0/3f762677702acb24f65e813070e306c61fafe25d4b2583f9dfc935131f774863f3addd5741572ed576bd69cabe473c5af18e1e108b829cb7b6b4747884f726e6
languageName: node
linkType: hard
"abbrev@npm:^4.0.0": "abbrev@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "abbrev@npm:4.0.0" resolution: "abbrev@npm:4.0.0"
@ -1759,6 +1998,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"agent-base@npm:6":
version: 6.0.2
resolution: "agent-base@npm:6.0.2"
dependencies:
debug: "npm:4"
checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261
languageName: node
linkType: hard
"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": "agent-base@npm:^7.1.0, agent-base@npm:^7.1.2":
version: 7.1.4 version: 7.1.4
resolution: "agent-base@npm:7.1.4" resolution: "agent-base@npm:7.1.4"
@ -1834,6 +2082,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"aproba@npm:^1.0.3 || ^2.0.0":
version: 2.1.0
resolution: "aproba@npm:2.1.0"
checksum: 10c0/ec8c1d351bac0717420c737eb062766fb63bde1552900e0f4fdad9eb064c3824fef23d1c416aa5f7a80f21ca682808e902d79b7c9ae756d342b5f1884f36932f
languageName: node
linkType: hard
"are-we-there-yet@npm:^2.0.0":
version: 2.0.0
resolution: "are-we-there-yet@npm:2.0.0"
dependencies:
delegates: "npm:^1.0.0"
readable-stream: "npm:^3.6.0"
checksum: 10c0/375f753c10329153c8d66dc95e8f8b6c7cc2aa66e05cb0960bd69092b10dae22900cacc7d653ad11d26b3ecbdbfe1e8bfb6ccf0265ba8077a7d979970f16b99c
languageName: node
linkType: hard
"argparse@npm:^1.0.7": "argparse@npm:^1.0.7":
version: 1.0.10 version: 1.0.10
resolution: "argparse@npm:1.0.10" resolution: "argparse@npm:1.0.10"
@ -2056,6 +2321,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"caseless@npm:^0.12.0":
version: 0.12.0
resolution: "caseless@npm:0.12.0"
checksum: 10c0/ccf64bcb6c0232cdc5b7bd91ddd06e23a4b541f138336d4725233ac538041fb2f29c2e86c3c4a7a61ef990b665348db23a047060b9414c3a6603e9fa61ad4626
languageName: node
linkType: hard
"chalk@npm:^4.1.2": "chalk@npm:^4.1.2":
version: 4.1.2 version: 4.1.2
resolution: "chalk@npm:4.1.2" resolution: "chalk@npm:4.1.2"
@ -2073,6 +2345,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"chownr@npm:^2.0.0":
version: 2.0.0
resolution: "chownr@npm:2.0.0"
checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6
languageName: node
linkType: hard
"chownr@npm:^3.0.0": "chownr@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "chownr@npm:3.0.0" resolution: "chownr@npm:3.0.0"
@ -2142,6 +2421,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"color-support@npm:^1.1.2":
version: 1.1.3
resolution: "color-support@npm:1.1.3"
bin:
color-support: bin.js
checksum: 10c0/8ffeaa270a784dc382f62d9be0a98581db43e11eee301af14734a6d089bd456478b1a8b3e7db7ca7dc5b18a75f828f775c44074020b51c05fc00e6d0992b1cc6
languageName: node
linkType: hard
"concat-map@npm:0.0.1": "concat-map@npm:0.0.1":
version: 0.0.1 version: 0.0.1
resolution: "concat-map@npm:0.0.1" resolution: "concat-map@npm:0.0.1"
@ -2149,6 +2437,25 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"concat-stream@npm:^2.0.0":
version: 2.0.0
resolution: "concat-stream@npm:2.0.0"
dependencies:
buffer-from: "npm:^1.0.0"
inherits: "npm:^2.0.3"
readable-stream: "npm:^3.0.2"
typedarray: "npm:^0.0.6"
checksum: 10c0/29565dd9198fe1d8cf57f6cc71527dbc6ad67e12e4ac9401feb389c53042b2dceedf47034cbe702dfc4fd8df3ae7e6bfeeebe732cc4fa2674e484c13f04c219a
languageName: node
linkType: hard
"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0":
version: 1.1.0
resolution: "console-control-strings@npm:1.1.0"
checksum: 10c0/7ab51d30b52d461412cd467721bb82afe695da78fff8f29fe6f6b9cbaac9a2328e27a22a966014df9532100f6dd85370460be8130b9c677891ba36d96a343f50
languageName: node
linkType: hard
"convert-source-map@npm:^2.0.0": "convert-source-map@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "convert-source-map@npm:2.0.0" resolution: "convert-source-map@npm:2.0.0"
@ -2205,6 +2512,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"delegates@npm:^1.0.0":
version: 1.0.0
resolution: "delegates@npm:1.0.0"
checksum: 10c0/ba05874b91148e1db4bf254750c042bf2215febd23a6d3cda2e64896aef79745fbd4b9996488bd3cafb39ce19dbce0fd6e3b6665275638befffe1c9b312b91b5
languageName: node
linkType: hard
"denque@npm:^2.1.0": "denque@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "denque@npm:2.1.0" resolution: "denque@npm:2.1.0"
@ -2212,6 +2526,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"detect-libc@npm:^2.0.0":
version: 2.1.2
resolution: "detect-libc@npm:2.1.2"
checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
languageName: node
linkType: hard
"detect-newline@npm:^3.1.0": "detect-newline@npm:^3.1.0":
version: 3.1.0 version: 3.1.0
resolution: "detect-newline@npm:3.1.0" resolution: "detect-newline@npm:3.1.0"
@ -2226,6 +2547,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"discord-api-types@npm:^0.38.41":
version: 0.38.43
resolution: "discord-api-types@npm:0.38.43"
checksum: 10c0/8d617f63e415f0a238fddd8ae3cb9f88164b884ce321e4456f4d1c8329e5c8ff2132a2423c78d1fced773d845ece9570b586f49a383f095b0ca3734844cfcaf0
languageName: node
linkType: hard
"discord.js@npm:^14.25.1": "discord.js@npm:^14.25.1":
version: 14.25.1 version: 14.25.1
resolution: "discord.js@npm:14.25.1" resolution: "discord.js@npm:14.25.1"
@ -2637,6 +2965,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ffmpeg-static@npm:^5.3.0":
version: 5.3.0
resolution: "ffmpeg-static@npm:5.3.0"
dependencies:
"@derhuerst/http-basic": "npm:^8.2.0"
env-paths: "npm:^2.2.0"
https-proxy-agent: "npm:^5.0.0"
progress: "npm:^2.0.3"
checksum: 10c0/607fbd40661140e9de38ab2c51b2bd7a5b51da5c5c4ae8d9b6f9af059dec0cad23e1fa2f690d357ba18fe3218dc1f1f9d14acadc941e987d03dce75ae1244f39
languageName: node
linkType: hard
"file-entry-cache@npm:^8.0.0": "file-entry-cache@npm:^8.0.0":
version: 8.0.0 version: 8.0.0
resolution: "file-entry-cache@npm:8.0.0" resolution: "file-entry-cache@npm:8.0.0"
@ -2693,6 +3033,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fs-minipass@npm:^2.0.0":
version: 2.1.0
resolution: "fs-minipass@npm:2.1.0"
dependencies:
minipass: "npm:^3.0.0"
checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004
languageName: node
linkType: hard
"fs-minipass@npm:^3.0.0": "fs-minipass@npm:^3.0.0":
version: 3.0.3 version: 3.0.3
resolution: "fs-minipass@npm:3.0.3" resolution: "fs-minipass@npm:3.0.3"
@ -2728,6 +3077,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"gauge@npm:^3.0.0":
version: 3.0.2
resolution: "gauge@npm:3.0.2"
dependencies:
aproba: "npm:^1.0.3 || ^2.0.0"
color-support: "npm:^1.1.2"
console-control-strings: "npm:^1.0.0"
has-unicode: "npm:^2.0.1"
object-assign: "npm:^4.1.1"
signal-exit: "npm:^3.0.0"
string-width: "npm:^4.2.3"
strip-ansi: "npm:^6.0.1"
wide-align: "npm:^1.1.2"
checksum: 10c0/75230ccaf216471e31025c7d5fcea1629596ca20792de50c596eb18ffb14d8404f927cd55535aab2eeecd18d1e11bd6f23ec3c2e9878d2dda1dc74bccc34b913
languageName: node
linkType: hard
"gensync@npm:^1.0.0-beta.2": "gensync@npm:^1.0.0-beta.2":
version: 1.0.0-beta.2 version: 1.0.0-beta.2
resolution: "gensync@npm:1.0.0-beta.2" resolution: "gensync@npm:1.0.0-beta.2"
@ -2801,7 +3167,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"glob@npm:^7.1.4": "glob@npm:^7.1.3, glob@npm:^7.1.4":
version: 7.2.3 version: 7.2.3
resolution: "glob@npm:7.2.3" resolution: "glob@npm:7.2.3"
dependencies: dependencies:
@ -2847,6 +3213,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"has-unicode@npm:^2.0.1":
version: 2.0.1
resolution: "has-unicode@npm:2.0.1"
checksum: 10c0/ebdb2f4895c26bb08a8a100b62d362e49b2190bcfd84b76bc4be1a3bd4d254ec52d0dd9f2fbcc093fc5eb878b20c52146f9dfd33e2686ed28982187be593b47c
languageName: node
linkType: hard
"html-escaper@npm:^2.0.0": "html-escaper@npm:^2.0.0":
version: 2.0.2 version: 2.0.2
resolution: "html-escaper@npm:2.0.2" resolution: "html-escaper@npm:2.0.2"
@ -2871,6 +3244,25 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"http-response-object@npm:^3.0.1":
version: 3.0.2
resolution: "http-response-object@npm:3.0.2"
dependencies:
"@types/node": "npm:^10.0.3"
checksum: 10c0/f161db99184087798563cb14c48a67eebe9405668a5ed2341faf85d3079a2c00262431df8e0ccbe274dc6415b6729179f12b09f875d13ad33d83401e4b1ed22e
languageName: node
linkType: hard
"https-proxy-agent@npm:^5.0.0":
version: 5.0.1
resolution: "https-proxy-agent@npm:5.0.1"
dependencies:
agent-base: "npm:6"
debug: "npm:4"
checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1
languageName: node
linkType: hard
"https-proxy-agent@npm:^7.0.1": "https-proxy-agent@npm:^7.0.1":
version: 7.0.6 version: 7.0.6
resolution: "https-proxy-agent@npm:7.0.6" resolution: "https-proxy-agent@npm:7.0.6"
@ -2940,7 +3332,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"inherits@npm:2": "inherits@npm:2, inherits@npm:^2.0.3":
version: 2.0.4 version: 2.0.4
resolution: "inherits@npm:2.0.4" resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
@ -3609,6 +4001,8 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "kord@workspace:." resolution: "kord@workspace:."
dependencies: dependencies:
"@discordjs/opus": "npm:^0.10.0"
"@discordjs/voice": "npm:^0.19.2"
"@prisma/client": "npm:6.4.1" "@prisma/client": "npm:6.4.1"
"@types/jest": "npm:^30.0.0" "@types/jest": "npm:^30.0.0"
"@types/node": "npm:^25.5.0" "@types/node": "npm:^25.5.0"
@ -3617,13 +4011,16 @@ __metadata:
discord.js: "npm:^14.25.1" discord.js: "npm:^14.25.1"
dotenv: "npm:^17.3.1" dotenv: "npm:^17.3.1"
eslint: "npm:^10.1.0" eslint: "npm:^10.1.0"
ffmpeg-static: "npm:^5.3.0"
ioredis: "npm:^5.10.1" ioredis: "npm:^5.10.1"
jest: "npm:^30.3.0" jest: "npm:^30.3.0"
prettier: "npm:^3.8.1" prettier: "npm:^3.8.1"
prism-media: "npm:^1.3.5"
prisma: "npm:6.4.1" prisma: "npm:6.4.1"
ts-jest: "npm:^29.4.6" ts-jest: "npm:^29.4.6"
tsx: "npm:^4.21.0" tsx: "npm:^4.21.0"
typescript: "npm:^6.0.2" typescript: "npm:^6.0.2"
youtubei.js: "npm:^17.0.1"
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -3734,6 +4131,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"make-dir@npm:^3.1.0":
version: 3.1.0
resolution: "make-dir@npm:3.1.0"
dependencies:
semver: "npm:^6.0.0"
checksum: 10c0/56aaafefc49c2dfef02c5c95f9b196c4eb6988040cf2c712185c7fe5c99b4091591a7fc4d4eafaaefa70ff763a26f6ab8c3ff60b9e75ea19876f49b18667ecaa
languageName: node
linkType: hard
"make-dir@npm:^4.0.0": "make-dir@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "make-dir@npm:4.0.0" resolution: "make-dir@npm:4.0.0"
@ -3786,6 +4192,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"meriyah@npm:^6.1.4":
version: 6.1.4
resolution: "meriyah@npm:6.1.4"
checksum: 10c0/d792a12df453b9e86c2bb70e73adf41b2d0fa88125381143e4d339d309ddcbb7aeb108e286411dcb19be82539d29b881279c613ae03ce514b7c296c72e9a8d00
languageName: node
linkType: hard
"mimic-fn@npm:^2.1.0": "mimic-fn@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "mimic-fn@npm:2.1.0" resolution: "mimic-fn@npm:2.1.0"
@ -3887,6 +4300,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minipass@npm:^5.0.0":
version: 5.0.0
resolution: "minipass@npm:5.0.0"
checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462
languageName: node
linkType: hard
"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": "minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3":
version: 7.1.3 version: 7.1.3
resolution: "minipass@npm:7.1.3" resolution: "minipass@npm:7.1.3"
@ -3894,6 +4314,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minizlib@npm:^2.1.1":
version: 2.1.2
resolution: "minizlib@npm:2.1.2"
dependencies:
minipass: "npm:^3.0.0"
yallist: "npm:^4.0.0"
checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78
languageName: node
linkType: hard
"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": "minizlib@npm:^3.0.1, minizlib@npm:^3.1.0":
version: 3.1.0 version: 3.1.0
resolution: "minizlib@npm:3.1.0" resolution: "minizlib@npm:3.1.0"
@ -3903,6 +4333,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mkdirp@npm:^1.0.3":
version: 1.0.4
resolution: "mkdirp@npm:1.0.4"
bin:
mkdirp: bin/cmd.js
checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf
languageName: node
linkType: hard
"ms@npm:^2.1.3": "ms@npm:^2.1.3":
version: 2.1.3 version: 2.1.3
resolution: "ms@npm:2.1.3" resolution: "ms@npm:2.1.3"
@ -3940,6 +4379,29 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"node-addon-api@npm:^8.1.0":
version: 8.7.0
resolution: "node-addon-api@npm:8.7.0"
dependencies:
node-gyp: "npm:latest"
checksum: 10c0/31a03b00f6b0753ab08360952fdf80a1abb619dcf8125fa1ab07e3a414da050963440c3a86c77a0334c0be7a71acb5e242dc468b79201ee6151c7b943afe946d
languageName: node
linkType: hard
"node-fetch@npm:^2.6.7":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
whatwg-url: "npm:^5.0.0"
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
languageName: node
linkType: hard
"node-gyp@npm:latest": "node-gyp@npm:latest":
version: 12.2.0 version: 12.2.0
resolution: "node-gyp@npm:12.2.0" resolution: "node-gyp@npm:12.2.0"
@ -3974,6 +4436,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"nopt@npm:^5.0.0":
version: 5.0.0
resolution: "nopt@npm:5.0.0"
dependencies:
abbrev: "npm:1"
bin:
nopt: bin/nopt.js
checksum: 10c0/fc5c4f07155cb455bf5fc3dd149fac421c1a40fd83c6bfe83aa82b52f02c17c5e88301321318adaa27611c8a6811423d51d29deaceab5fa158b585a61a551061
languageName: node
linkType: hard
"nopt@npm:^9.0.0": "nopt@npm:^9.0.0":
version: 9.0.0 version: 9.0.0
resolution: "nopt@npm:9.0.0" resolution: "nopt@npm:9.0.0"
@ -4001,6 +4474,25 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"npmlog@npm:^5.0.1":
version: 5.0.1
resolution: "npmlog@npm:5.0.1"
dependencies:
are-we-there-yet: "npm:^2.0.0"
console-control-strings: "npm:^1.1.0"
gauge: "npm:^3.0.0"
set-blocking: "npm:^2.0.0"
checksum: 10c0/489ba519031013001135c463406f55491a17fc7da295c18a04937fe3a4d523fd65e88dd418a28b967ab743d913fdeba1e29838ce0ad8c75557057c481f7d49fa
languageName: node
linkType: hard
"object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414
languageName: node
linkType: hard
"once@npm:^1.3.0": "once@npm:^1.3.0":
version: 1.4.0 version: 1.4.0
resolution: "once@npm:1.4.0" resolution: "once@npm:1.4.0"
@ -4090,6 +4582,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"parse-cache-control@npm:^1.0.1":
version: 1.0.1
resolution: "parse-cache-control@npm:1.0.1"
checksum: 10c0/330a0d9e3a22a7b0f6e8a973c0b9f51275642ee28544cd0d546420273946d555d20a5c7b49fca24d68d2e698bae0186f0f41f48d62133d3153c32454db05f2df
languageName: node
linkType: hard
"parse-json@npm:^5.2.0": "parse-json@npm:^5.2.0":
version: 5.2.0 version: 5.2.0
resolution: "parse-json@npm:5.2.0" resolution: "parse-json@npm:5.2.0"
@ -4207,6 +4706,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prism-media@npm:^1.3.5":
version: 1.3.5
resolution: "prism-media@npm:1.3.5"
peerDependencies:
"@discordjs/opus": ">=0.8.0 <1.0.0"
ffmpeg-static: ^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0
node-opus: ^0.3.3
opusscript: ^0.0.8
peerDependenciesMeta:
"@discordjs/opus":
optional: true
ffmpeg-static:
optional: true
node-opus:
optional: true
opusscript:
optional: true
checksum: 10c0/3bd9f3b246c8ac7aa744d87d502f65280f5c5635555a08d8ff548da99f29ee2bf92c4696dab1e22c2e773f71f95a24ec4f06d82dea39f07c1d7b90956c56d173
languageName: node
linkType: hard
"prisma@npm:6.4.1": "prisma@npm:6.4.1":
version: 6.4.1 version: 6.4.1
resolution: "prisma@npm:6.4.1" resolution: "prisma@npm:6.4.1"
@ -4236,6 +4756,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"progress@npm:^2.0.3":
version: 2.0.3
resolution: "progress@npm:2.0.3"
checksum: 10c0/1697e07cb1068055dbe9fe858d242368ff5d2073639e652b75a7eb1f2a1a8d4afd404d719de23c7b48481a6aa0040686310e2dac2f53d776daa2176d3f96369c
languageName: node
linkType: hard
"punycode@npm:^2.1.0": "punycode@npm:^2.1.0":
version: 2.3.1 version: 2.3.1
resolution: "punycode@npm:2.3.1" resolution: "punycode@npm:2.3.1"
@ -4257,6 +4784,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"readable-stream@npm:^3.0.2, readable-stream@npm:^3.6.0":
version: 3.6.2
resolution: "readable-stream@npm:3.6.2"
dependencies:
inherits: "npm:^2.0.3"
string_decoder: "npm:^1.1.1"
util-deprecate: "npm:^1.0.1"
checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7
languageName: node
linkType: hard
"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": "redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0":
version: 1.2.0 version: 1.2.0
resolution: "redis-errors@npm:1.2.0" resolution: "redis-errors@npm:1.2.0"
@ -4303,6 +4841,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rimraf@npm:^3.0.2":
version: 3.0.2
resolution: "rimraf@npm:3.0.2"
dependencies:
glob: "npm:^7.1.3"
bin:
rimraf: bin.js
checksum: 10c0/9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8
languageName: node
linkType: hard
"safe-buffer@npm:~5.2.0":
version: 5.2.1
resolution: "safe-buffer@npm:5.2.1"
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
languageName: node
linkType: hard
"safer-buffer@npm:>= 2.1.2 < 3.0.0": "safer-buffer@npm:>= 2.1.2 < 3.0.0":
version: 2.1.2 version: 2.1.2
resolution: "safer-buffer@npm:2.1.2" resolution: "safer-buffer@npm:2.1.2"
@ -4310,7 +4866,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"semver@npm:^6.3.1": "semver@npm:^6.0.0, semver@npm:^6.3.1":
version: 6.3.1 version: 6.3.1
resolution: "semver@npm:6.3.1" resolution: "semver@npm:6.3.1"
bin: bin:
@ -4328,6 +4884,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"set-blocking@npm:^2.0.0":
version: 2.0.0
resolution: "set-blocking@npm:2.0.0"
checksum: 10c0/9f8c1b2d800800d0b589de1477c753492de5c1548d4ade52f57f1d1f5e04af5481554d75ce5e5c43d4004b80a3eb714398d6907027dc0534177b7539119f4454
languageName: node
linkType: hard
"shebang-command@npm:^2.0.0": "shebang-command@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "shebang-command@npm:2.0.0" resolution: "shebang-command@npm:2.0.0"
@ -4344,7 +4907,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"signal-exit@npm:^3.0.3": "signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.3":
version: 3.0.7 version: 3.0.7
resolution: "signal-exit@npm:3.0.7" resolution: "signal-exit@npm:3.0.7"
checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912
@ -4452,7 +5015,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
version: 4.2.3 version: 4.2.3
resolution: "string-width@npm:4.2.3" resolution: "string-width@npm:4.2.3"
dependencies: dependencies:
@ -4474,6 +5037,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"string_decoder@npm:^1.1.1":
version: 1.3.0
resolution: "string_decoder@npm:1.3.0"
dependencies:
safe-buffer: "npm:~5.2.0"
checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d
languageName: node
linkType: hard
"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
version: 6.0.1 version: 6.0.1
resolution: "strip-ansi@npm:6.0.1" resolution: "strip-ansi@npm:6.0.1"
@ -4540,6 +5112,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tar@npm:^6.1.11":
version: 6.2.1
resolution: "tar@npm:6.2.1"
dependencies:
chownr: "npm:^2.0.0"
fs-minipass: "npm:^2.0.0"
minipass: "npm:^5.0.0"
minizlib: "npm:^2.1.1"
mkdirp: "npm:^1.0.3"
yallist: "npm:^4.0.0"
checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537
languageName: node
linkType: hard
"tar@npm:^7.5.4": "tar@npm:^7.5.4":
version: 7.5.13 version: 7.5.13
resolution: "tar@npm:7.5.13" resolution: "tar@npm:7.5.13"
@ -4581,6 +5167,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11
languageName: node
linkType: hard
"ts-api-utils@npm:^2.4.0": "ts-api-utils@npm:^2.4.0":
version: 2.5.0 version: 2.5.0
resolution: "ts-api-utils@npm:2.5.0" resolution: "ts-api-utils@npm:2.5.0"
@ -4637,7 +5230,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3": "tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.8.1":
version: 2.8.1 version: 2.8.1
resolution: "tslib@npm:2.8.1" resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@ -4690,6 +5283,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typedarray@npm:^0.0.6":
version: 0.0.6
resolution: "typedarray@npm:0.0.6"
checksum: 10c0/6005cb31df50eef8b1f3c780eb71a17925f3038a100d82f9406ac2ad1de5eb59f8e6decbdc145b3a1f8e5836e17b0c0002fb698b9fe2516b8f9f9ff602d36412
languageName: node
linkType: hard
"typescript@npm:^6.0.2": "typescript@npm:^6.0.2":
version: 6.0.2 version: 6.0.2
resolution: "typescript@npm:6.0.2" resolution: "typescript@npm:6.0.2"
@ -4830,6 +5430,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"util-deprecate@npm:^1.0.1":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"
checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942
languageName: node
linkType: hard
"v8-to-istanbul@npm:^9.0.1": "v8-to-istanbul@npm:^9.0.1":
version: 9.3.0 version: 9.3.0
resolution: "v8-to-istanbul@npm:9.3.0" resolution: "v8-to-istanbul@npm:9.3.0"
@ -4850,6 +5457,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"
checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db
languageName: node
linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
dependencies:
tr46: "npm:~0.0.3"
webidl-conversions: "npm:^3.0.0"
checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5
languageName: node
linkType: hard
"which@npm:^2.0.1": "which@npm:^2.0.1":
version: 2.0.2 version: 2.0.2
resolution: "which@npm:2.0.2" resolution: "which@npm:2.0.2"
@ -4872,6 +5496,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"wide-align@npm:^1.1.2":
version: 1.1.5
resolution: "wide-align@npm:1.1.5"
dependencies:
string-width: "npm:^1.0.2 || 2 || 3 || 4"
checksum: 10c0/1d9c2a3e36dfb09832f38e2e699c367ef190f96b82c71f809bc0822c306f5379df87bab47bed27ea99106d86447e50eb972d3c516c2f95782807a9d082fbea95
languageName: node
linkType: hard
"word-wrap@npm:^1.2.5": "word-wrap@npm:^1.2.5":
version: 1.2.5 version: 1.2.5
resolution: "word-wrap@npm:1.2.5" resolution: "word-wrap@npm:1.2.5"
@ -4925,7 +5558,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ws@npm:^8.17.0": "ws@npm:^8.17.0, ws@npm:^8.19.0":
version: 8.20.0 version: 8.20.0
resolution: "ws@npm:8.20.0" resolution: "ws@npm:8.20.0"
peerDependencies: peerDependencies:
@ -4996,3 +5629,13 @@ __metadata:
checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f
languageName: node languageName: node
linkType: hard linkType: hard
"youtubei.js@npm:^17.0.1":
version: 17.0.1
resolution: "youtubei.js@npm:17.0.1"
dependencies:
"@bufbuild/protobuf": "npm:^2.0.0"
meriyah: "npm:^6.1.4"
checksum: 10c0/18b188d9a706feca6b6a23582c00b3fdb955a1be823854b9c4b0312ccf7c3539db28a78184fb4927289388e363d5594f39df63e7253874b39317077309088d98
languageName: node
linkType: hard