Compare commits
2 Commits
019cb314be
...
d51eadb8c4
| Author | SHA1 | Date |
|---|---|---|
|
|
d51eadb8c4 | |
|
|
85f5057fd7 |
|
|
@ -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) |
|
||||
|
|
@ -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 응답 변경 시 보완이 필요할 수 있다.
|
||||
|
|
@ -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` 대상이 아니며, 대기열 곡만 삭제된다.
|
||||
|
|
@ -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.
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
- [에러 안내 기능 기획서 (Error Guidance Plan)](Plans/Error_Guidance_Plan.md)
|
||||
- [다국어 지원 기획서 (i18n Plan)](Plans/i18n_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)
|
||||
|
|
@ -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 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: 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)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@
|
|||
"name": "kord",
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"dependencies": {
|
||||
"@discordjs/opus": "^0.10.0",
|
||||
"@discordjs/voice": "^0.19.2",
|
||||
"@prisma/client": "6.4.1",
|
||||
"discord.js": "^14.25.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": {
|
||||
"@types/jest": "^30.0.0",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -8,6 +8,7 @@ import { ErrorReporter, withErrorHandler } from '../errors/ErrorReporter';
|
|||
import { t } from '../i18n';
|
||||
import { getInteractionLocale } from '../i18n/localeHelper';
|
||||
import { handleSetupWizardInteraction } from '../interactions/handlers/setupWizardHandler';
|
||||
import { MusicService } from '../services/MusicService';
|
||||
|
||||
export default {
|
||||
name: Events.InteractionCreate,
|
||||
|
|
@ -22,6 +23,12 @@ export default {
|
|||
await command.execute(interaction, 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_')) {
|
||||
const locale = await getInteractionLocale(interaction);
|
||||
await withErrorHandler(interaction, async () => {
|
||||
|
|
|
|||
|
|
@ -191,6 +191,73 @@ export const en: 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: {
|
||||
title: 'Bot Permission Audit Report',
|
||||
channel: 'Channel',
|
||||
|
|
|
|||
|
|
@ -191,6 +191,73 @@ export const ko: TranslationSchema = {
|
|||
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: {
|
||||
title: '봇 권한 진단 보고서',
|
||||
channel: '채널',
|
||||
|
|
|
|||
|
|
@ -145,6 +145,73 @@ export interface TranslationSchema {
|
|||
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: {
|
||||
title: string;
|
||||
channel: string;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
661
yarn.lock
|
|
@ -381,6 +381,25 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.14.0
|
||||
resolution: "@discordjs/builders@npm:1.14.0"
|
||||
|
|
@ -419,6 +438,35 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.6.1
|
||||
resolution: "@discordjs/rest@npm:2.6.1"
|
||||
|
|
@ -445,6 +493,20 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.2.3
|
||||
resolution: "@discordjs/ws@npm:1.2.3"
|
||||
|
|
@ -1143,6 +1205,18 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 4.0.0
|
||||
resolution: "@npmcli/agent@npm:4.0.0"
|
||||
|
|
@ -1303,7 +1377,158 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "@tybys/wasm-util@npm:0.10.1"
|
||||
dependencies:
|
||||
|
|
@ -1418,6 +1643,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.0.3
|
||||
resolution: "@types/stack-utils@npm:2.0.3"
|
||||
|
|
@ -1425,7 +1657,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8.5.10":
|
||||
"@types/ws@npm:^8.18.1, @types/ws@npm:^8.5.10":
|
||||
version: 8.18.1
|
||||
resolution: "@types/ws@npm:8.18.1"
|
||||
dependencies:
|
||||
|
|
@ -1734,6 +1966,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 4.0.0
|
||||
resolution: "abbrev@npm:4.0.0"
|
||||
|
|
@ -1759,6 +1998,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 7.1.4
|
||||
resolution: "agent-base@npm:7.1.4"
|
||||
|
|
@ -1834,6 +2082,23 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.0.10
|
||||
resolution: "argparse@npm:1.0.10"
|
||||
|
|
@ -2056,6 +2321,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 4.1.2
|
||||
resolution: "chalk@npm:4.1.2"
|
||||
|
|
@ -2073,6 +2345,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 3.0.0
|
||||
resolution: "chownr@npm:3.0.0"
|
||||
|
|
@ -2142,6 +2421,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 0.0.1
|
||||
resolution: "concat-map@npm:0.0.1"
|
||||
|
|
@ -2149,6 +2437,25 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.0.0
|
||||
resolution: "convert-source-map@npm:2.0.0"
|
||||
|
|
@ -2205,6 +2512,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.1.0
|
||||
resolution: "denque@npm:2.1.0"
|
||||
|
|
@ -2212,6 +2526,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 3.1.0
|
||||
resolution: "detect-newline@npm:3.1.0"
|
||||
|
|
@ -2226,6 +2547,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 14.25.1
|
||||
resolution: "discord.js@npm:14.25.1"
|
||||
|
|
@ -2637,6 +2965,18 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 8.0.0
|
||||
resolution: "file-entry-cache@npm:8.0.0"
|
||||
|
|
@ -2693,6 +3033,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 3.0.3
|
||||
resolution: "fs-minipass@npm:3.0.3"
|
||||
|
|
@ -2728,6 +3077,23 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.0.0-beta.2
|
||||
resolution: "gensync@npm:1.0.0-beta.2"
|
||||
|
|
@ -2801,7 +3167,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^7.1.4":
|
||||
"glob@npm:^7.1.3, glob@npm:^7.1.4":
|
||||
version: 7.2.3
|
||||
resolution: "glob@npm:7.2.3"
|
||||
dependencies:
|
||||
|
|
@ -2847,6 +3213,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.0.2
|
||||
resolution: "html-escaper@npm:2.0.2"
|
||||
|
|
@ -2871,6 +3244,25 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 7.0.6
|
||||
resolution: "https-proxy-agent@npm:7.0.6"
|
||||
|
|
@ -2940,7 +3332,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inherits@npm:2":
|
||||
"inherits@npm:2, inherits@npm:^2.0.3":
|
||||
version: 2.0.4
|
||||
resolution: "inherits@npm:2.0.4"
|
||||
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
|
||||
|
|
@ -3609,6 +4001,8 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "kord@workspace:."
|
||||
dependencies:
|
||||
"@discordjs/opus": "npm:^0.10.0"
|
||||
"@discordjs/voice": "npm:^0.19.2"
|
||||
"@prisma/client": "npm:6.4.1"
|
||||
"@types/jest": "npm:^30.0.0"
|
||||
"@types/node": "npm:^25.5.0"
|
||||
|
|
@ -3617,13 +4011,16 @@ __metadata:
|
|||
discord.js: "npm:^14.25.1"
|
||||
dotenv: "npm:^17.3.1"
|
||||
eslint: "npm:^10.1.0"
|
||||
ffmpeg-static: "npm:^5.3.0"
|
||||
ioredis: "npm:^5.10.1"
|
||||
jest: "npm:^30.3.0"
|
||||
prettier: "npm:^3.8.1"
|
||||
prism-media: "npm:^1.3.5"
|
||||
prisma: "npm:6.4.1"
|
||||
ts-jest: "npm:^29.4.6"
|
||||
tsx: "npm:^4.21.0"
|
||||
typescript: "npm:^6.0.2"
|
||||
youtubei.js: "npm:^17.0.1"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
|
@ -3734,6 +4131,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 4.0.0
|
||||
resolution: "make-dir@npm:4.0.0"
|
||||
|
|
@ -3786,6 +4192,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.1.0
|
||||
resolution: "mimic-fn@npm:2.1.0"
|
||||
|
|
@ -3887,6 +4300,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 7.1.3
|
||||
resolution: "minipass@npm:7.1.3"
|
||||
|
|
@ -3894,6 +4314,16 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 3.1.0
|
||||
resolution: "minizlib@npm:3.1.0"
|
||||
|
|
@ -3903,6 +4333,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.1.3
|
||||
resolution: "ms@npm:2.1.3"
|
||||
|
|
@ -3940,6 +4379,29 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 12.2.0
|
||||
resolution: "node-gyp@npm:12.2.0"
|
||||
|
|
@ -3974,6 +4436,17 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 9.0.0
|
||||
resolution: "nopt@npm:9.0.0"
|
||||
|
|
@ -4001,6 +4474,25 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.4.0
|
||||
resolution: "once@npm:1.4.0"
|
||||
|
|
@ -4090,6 +4582,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 5.2.0
|
||||
resolution: "parse-json@npm:5.2.0"
|
||||
|
|
@ -4207,6 +4706,27 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 6.4.1
|
||||
resolution: "prisma@npm:6.4.1"
|
||||
|
|
@ -4236,6 +4756,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.3.1
|
||||
resolution: "punycode@npm:2.3.1"
|
||||
|
|
@ -4257,6 +4784,17 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.2.0
|
||||
resolution: "redis-errors@npm:1.2.0"
|
||||
|
|
@ -4303,6 +4841,24 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.1.2
|
||||
resolution: "safer-buffer@npm:2.1.2"
|
||||
|
|
@ -4310,7 +4866,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^6.3.1":
|
||||
"semver@npm:^6.0.0, semver@npm:^6.3.1":
|
||||
version: 6.3.1
|
||||
resolution: "semver@npm:6.3.1"
|
||||
bin:
|
||||
|
|
@ -4328,6 +4884,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.0.0
|
||||
resolution: "shebang-command@npm:2.0.0"
|
||||
|
|
@ -4344,7 +4907,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"signal-exit@npm:^3.0.3":
|
||||
"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.3":
|
||||
version: 3.0.7
|
||||
resolution: "signal-exit@npm:3.0.7"
|
||||
checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912
|
||||
|
|
@ -4452,7 +5015,7 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "string-width@npm:4.2.3"
|
||||
dependencies:
|
||||
|
|
@ -4474,6 +5037,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 6.0.1
|
||||
resolution: "strip-ansi@npm:6.0.1"
|
||||
|
|
@ -4540,6 +5112,20 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 7.5.13
|
||||
resolution: "tar@npm:7.5.13"
|
||||
|
|
@ -4581,6 +5167,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.5.0
|
||||
resolution: "ts-api-utils@npm:2.5.0"
|
||||
|
|
@ -4637,7 +5230,7 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "tslib@npm:2.8.1"
|
||||
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
|
||||
|
|
@ -4690,6 +5283,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 6.0.2
|
||||
resolution: "typescript@npm:6.0.2"
|
||||
|
|
@ -4830,6 +5430,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 9.3.0
|
||||
resolution: "v8-to-istanbul@npm:9.3.0"
|
||||
|
|
@ -4850,6 +5457,23 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.0.2
|
||||
resolution: "which@npm:2.0.2"
|
||||
|
|
@ -4872,6 +5496,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.2.5
|
||||
resolution: "word-wrap@npm:1.2.5"
|
||||
|
|
@ -4925,7 +5558,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ws@npm:^8.17.0":
|
||||
"ws@npm:^8.17.0, ws@npm:^8.19.0":
|
||||
version: 8.20.0
|
||||
resolution: "ws@npm:8.20.0"
|
||||
peerDependencies:
|
||||
|
|
@ -4996,3 +5629,13 @@ __metadata:
|
|||
checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f
|
||||
languageName: node
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue