soundcord 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +530 -62
- package/dist/chapters.d.ts +11 -0
- package/dist/chapters.d.ts.map +1 -0
- package/dist/chapters.js +80 -0
- package/dist/chapters.js.map +1 -0
- package/dist/deezer.d.ts +41 -0
- package/dist/deezer.d.ts.map +1 -0
- package/dist/deezer.js +205 -0
- package/dist/deezer.js.map +1 -0
- package/dist/filters.d.ts +10 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +190 -0
- package/dist/filters.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -3
- package/dist/index.js.map +1 -1
- package/dist/live.d.ts +13 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +70 -0
- package/dist/live.js.map +1 -0
- package/dist/lyrics.d.ts +19 -0
- package/dist/lyrics.d.ts.map +1 -0
- package/dist/lyrics.js +167 -0
- package/dist/lyrics.js.map +1 -0
- package/dist/persistence.d.ts +61 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +189 -0
- package/dist/persistence.js.map +1 -0
- package/dist/radio.d.ts +22 -0
- package/dist/radio.d.ts.map +1 -0
- package/dist/radio.js +275 -0
- package/dist/radio.js.map +1 -0
- package/dist/setup.js +88 -24
- package/dist/setup.js.map +1 -1
- package/dist/soundcloud.d.ts +29 -0
- package/dist/soundcloud.d.ts.map +1 -0
- package/dist/soundcloud.js +203 -0
- package/dist/soundcloud.js.map +1 -0
- package/dist/spotify.d.ts +16 -0
- package/dist/spotify.d.ts.map +1 -0
- package/dist/spotify.js +157 -0
- package/dist/spotify.js.map +1 -0
- package/dist/types.d.ts +14 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +41 -0
- package/dist/utils.js.map +1 -0
- package/dist/youtube.d.ts +33 -2
- package/dist/youtube.d.ts.map +1 -1
- package/dist/youtube.js +203 -55
- package/dist/youtube.js.map +1 -1
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,96 +1,530 @@
|
|
|
1
1
|
# soundcord
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/soundcord)
|
|
4
|
+
[](https://www.npmjs.com/package/soundcord)
|
|
5
|
+
[](https://github.com/Shuzzo/soundcord)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
All-in-one audio streaming library for Discord bots. YouTube, Spotify, SoundCloud, Deezer, Radio, Lyrics, and more.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Multi-source**: YouTube, Spotify, SoundCloud, Deezer, Radio, Twitch
|
|
12
|
+
- **Lyrics**: Synced lyrics with timestamps (karaoke mode)
|
|
13
|
+
- **YouTube Chapters**: Navigate long videos
|
|
14
|
+
- **Audio Filters**: Bass boost, nightcore, vaporwave, 8D, and more
|
|
15
|
+
- **Quality Selection**: best, medium, low
|
|
16
|
+
- **Live Streams**: YouTube Live & Twitch
|
|
17
|
+
- **Queue Persistence**: Save/restore queues
|
|
18
|
+
- **Zero ytdl-core**: Uses yt-dlp (more reliable)
|
|
19
|
+
- **TypeScript**: Full type support
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
6
22
|
|
|
7
23
|
```bash
|
|
8
24
|
npm install soundcord
|
|
9
25
|
```
|
|
10
26
|
|
|
11
|
-
### Dependencies
|
|
27
|
+
### Setup Dependencies
|
|
12
28
|
|
|
13
|
-
|
|
29
|
+
Run the setup helper to install yt-dlp and ffmpeg:
|
|
14
30
|
|
|
15
31
|
```bash
|
|
16
32
|
npx soundcord-setup
|
|
17
33
|
```
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
35
|
+
This will:
|
|
36
|
+
- Detect your OS and package manager
|
|
37
|
+
- Install yt-dlp and ffmpeg automatically
|
|
38
|
+
- Add to PATH if needed
|
|
22
39
|
|
|
23
|
-
##
|
|
40
|
+
## Quick Start
|
|
24
41
|
|
|
25
42
|
```typescript
|
|
26
43
|
import { YouTube } from 'soundcord';
|
|
27
44
|
|
|
28
45
|
const yt = new YouTube();
|
|
29
46
|
|
|
30
|
-
// Search videos
|
|
31
47
|
const results = await yt.search('never gonna give you up');
|
|
32
|
-
|
|
48
|
+
const stream = await yt.getStream(results[0].id);
|
|
49
|
+
console.log(stream.url);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## YouTube
|
|
33
53
|
|
|
34
|
-
|
|
54
|
+
### Search
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const results = await yt.search('lofi hip hop', 5);
|
|
58
|
+
|
|
59
|
+
for (const video of results) {
|
|
60
|
+
console.log(`${video.title} - ${video.author} (${video.duration}s)`);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Get Video Info
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
35
67
|
const info = await yt.getInfo('dQw4w9WgXcQ');
|
|
36
|
-
console.log(info.title, info.duration);
|
|
68
|
+
console.log(info.title, info.author, info.duration);
|
|
69
|
+
```
|
|
37
70
|
|
|
38
|
-
|
|
71
|
+
### Get Stream URL
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
39
74
|
const stream = await yt.getStream('dQw4w9WgXcQ');
|
|
40
|
-
console.log(stream.url);
|
|
75
|
+
console.log(stream.url, stream.bitrate);
|
|
41
76
|
```
|
|
42
77
|
|
|
43
|
-
###
|
|
78
|
+
### Quality Selection
|
|
44
79
|
|
|
45
80
|
```typescript
|
|
46
|
-
|
|
47
|
-
|
|
81
|
+
const stream = await yt.getStreamWithQuality('dQw4w9WgXcQ', 'best');
|
|
82
|
+
const streamLow = await yt.getStreamWithQuality('dQw4w9WgXcQ', 'low');
|
|
48
83
|
|
|
49
|
-
const
|
|
84
|
+
const qualities = await yt.getAvailableQualities('dQw4w9WgXcQ');
|
|
85
|
+
console.log(qualities);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Playlists
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
if (yt.isPlaylistUrl(url)) {
|
|
92
|
+
const playlist = await yt.getPlaylist(url, 50);
|
|
93
|
+
console.log(`${playlist.title} - ${playlist.videoCount} videos`);
|
|
94
|
+
|
|
95
|
+
for (const video of playlist.videos) {
|
|
96
|
+
console.log(video.title);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Chapters
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const chapters = await yt.getChapters('dQw4w9WgXcQ');
|
|
105
|
+
|
|
106
|
+
for (const chapter of chapters) {
|
|
107
|
+
console.log(`${chapter.title}: ${chapter.start}s - ${chapter.end}s`);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Related Videos (Autoplay)
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const related = await yt.getRelated('dQw4w9WgXcQ', 5);
|
|
115
|
+
const nextVideo = related[0];
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Live Streams
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
if (await yt.isLive('VIDEO_ID')) {
|
|
122
|
+
const stream = await yt.getLiveStream('VIDEO_ID');
|
|
123
|
+
console.log(stream.url);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Spotify
|
|
128
|
+
|
|
129
|
+
Play Spotify tracks, playlists, and albums by searching YouTube.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
if (yt.isSpotifyUrl(url)) {
|
|
133
|
+
const result = await yt.playSpotify(url);
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(result)) {
|
|
136
|
+
console.log(`Playlist with ${result.length} tracks`);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(`Track: ${result.title}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Track
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
const track = await yt.getSpotifyTrack('https://open.spotify.com/track/...');
|
|
147
|
+
console.log(`${track.artist} - ${track.name}`);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Playlist
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const tracks = await yt.getSpotifyPlaylist('https://open.spotify.com/playlist/...', 50);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Album
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const tracks = await yt.getSpotifyAlbum('https://open.spotify.com/album/...');
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## SoundCloud
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { SoundCloud } from 'soundcord';
|
|
166
|
+
|
|
167
|
+
const sc = new SoundCloud();
|
|
168
|
+
|
|
169
|
+
if (sc.isSoundCloudUrl(url)) {
|
|
170
|
+
const track = await sc.getTrack(url);
|
|
171
|
+
console.log(track.title, track.author);
|
|
172
|
+
|
|
173
|
+
const streamUrl = await sc.getStream(track.id);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Playlists
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
if (sc.isSoundCloudPlaylistUrl(url)) {
|
|
181
|
+
const playlist = await sc.getPlaylist(url);
|
|
182
|
+
|
|
183
|
+
for (const track of playlist.tracks) {
|
|
184
|
+
console.log(track.title);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Search
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const results = await sc.search('chill beats', 10);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Deezer
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { Deezer } from 'soundcord';
|
|
199
|
+
|
|
200
|
+
const dz = new Deezer();
|
|
201
|
+
|
|
202
|
+
if (dz.isDeezerUrl(url)) {
|
|
203
|
+
const info = dz.extractDeezerInfo(url);
|
|
204
|
+
|
|
205
|
+
if (info.type === 'track') {
|
|
206
|
+
const track = await dz.getTrack(url);
|
|
207
|
+
console.log(`${track.artist} - ${track.title}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (info.type === 'album') {
|
|
211
|
+
const album = await dz.getAlbum(url);
|
|
212
|
+
console.log(`${album.title} - ${album.tracks.length} tracks`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (info.type === 'playlist') {
|
|
216
|
+
const playlist = await dz.getPlaylist(url);
|
|
217
|
+
console.log(`${playlist.title} - ${playlist.tracks.length} tracks`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Radio
|
|
223
|
+
|
|
224
|
+
15+ preset stations + worldwide search via radio-browser API.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { Radio, RadioPresets } from 'soundcord';
|
|
228
|
+
|
|
229
|
+
const radio = new Radio();
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Presets
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const presets = radio.getPresets();
|
|
236
|
+
|
|
237
|
+
for (const station of presets) {
|
|
238
|
+
console.log(`${station.name} - ${station.genre} (${station.country})`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const fip = radio.getPreset('fip');
|
|
242
|
+
console.log(fip.url);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Available Presets
|
|
246
|
+
|
|
247
|
+
| Name | Genre | Country |
|
|
248
|
+
|------|-------|---------|
|
|
249
|
+
| `franceinter` | Généraliste | France |
|
|
250
|
+
| `franceinfo` | News | France |
|
|
251
|
+
| `fip` | Eclectic | France |
|
|
252
|
+
| `mouv` | Urban/Hip-Hop | France |
|
|
253
|
+
| `francemusique` | Classical | France |
|
|
254
|
+
| `franceculture` | Culture | France |
|
|
255
|
+
| `skyrock` | Hip-Hop/Rap | France |
|
|
256
|
+
| `funradio` | Dance/Electronic | France |
|
|
257
|
+
| `rtl` | Généraliste | France |
|
|
258
|
+
| `rtl2` | Pop/Rock | France |
|
|
259
|
+
| `bbcradio1` | Pop/Rock | UK |
|
|
260
|
+
| `bbcradio2` | Adult Contemporary | UK |
|
|
261
|
+
| `lofi` | Lofi/Chill | International |
|
|
262
|
+
| `jazz` | Jazz | France |
|
|
263
|
+
| `classique` | Classical | France |
|
|
264
|
+
|
|
265
|
+
### Search Stations
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const stations = await radio.search('jazz', 10);
|
|
269
|
+
|
|
270
|
+
for (const station of stations) {
|
|
271
|
+
console.log(`${station.name} - ${station.url}`);
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Search by Genre
|
|
50
276
|
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
277
|
+
```typescript
|
|
278
|
+
const stations = await radio.searchByGenre('rock', 10);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Search by Country
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
const stations = await radio.searchByCountry('France', 10);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Popular Stations
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const popular = await radio.getPopular(20);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Lyrics
|
|
294
|
+
|
|
295
|
+
Get lyrics with optional synchronized timestamps.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
const lyrics = await yt.getLyrics('Artist - Song Title');
|
|
299
|
+
|
|
300
|
+
if (lyrics) {
|
|
301
|
+
console.log(`${lyrics.artist} - ${lyrics.title}`);
|
|
302
|
+
console.log(lyrics.lyrics);
|
|
303
|
+
|
|
304
|
+
if (lyrics.synced) {
|
|
305
|
+
console.log('Synced lyrics available');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Synced Lyrics (Karaoke)
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { Lyrics } from 'soundcord';
|
|
314
|
+
|
|
315
|
+
const lyricsEngine = new Lyrics(15000);
|
|
316
|
+
const synced = await lyricsEngine.getSynced('Artist - Song');
|
|
317
|
+
|
|
318
|
+
if (synced) {
|
|
319
|
+
for (const line of synced) {
|
|
320
|
+
console.log(`[${line.time}s] ${line.text}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Twitch
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
if (yt.isTwitchUrl(url)) {
|
|
329
|
+
const channel = yt.extractTwitchChannel(url);
|
|
330
|
+
const stream = await yt.getTwitchStream(url);
|
|
331
|
+
console.log(stream.url);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Audio Filters
|
|
336
|
+
|
|
337
|
+
15+ built-in audio filters for ffmpeg.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { getFilter, getFilterNames, combineFilters } from 'soundcord';
|
|
341
|
+
|
|
342
|
+
const filters = getFilterNames();
|
|
343
|
+
console.log(filters);
|
|
344
|
+
|
|
345
|
+
const bass = getFilter('bass');
|
|
346
|
+
console.log(bass.ffmpegArgs);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Available Filters
|
|
350
|
+
|
|
351
|
+
| Filter | Description |
|
|
352
|
+
|--------|-------------|
|
|
353
|
+
| `bass` | Bass boost |
|
|
354
|
+
| `nightcore` | Nightcore effect |
|
|
355
|
+
| `vaporwave` | Vaporwave/slowed |
|
|
356
|
+
| `8d` | 8D audio |
|
|
357
|
+
| `karaoke` | Remove vocals |
|
|
358
|
+
| `treble` | Treble boost |
|
|
359
|
+
| `echo` | Echo effect |
|
|
360
|
+
| `flanger` | Flanger effect |
|
|
361
|
+
| `phaser` | Phaser effect |
|
|
362
|
+
| `tremolo` | Tremolo effect |
|
|
363
|
+
| `vibrato` | Vibrato effect |
|
|
364
|
+
| `reverse` | Reverse audio |
|
|
365
|
+
| `pitch_up` | Higher pitch |
|
|
366
|
+
| `pitch_down` | Lower pitch |
|
|
367
|
+
| `speed` | 1.25x speed |
|
|
368
|
+
|
|
369
|
+
### Using with FFmpeg
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
const filter = getFilter('bass');
|
|
373
|
+
const ffmpegArgs = ['-i', streamUrl, ...filter.ffmpegArgs, '-f', 's16le', '-'];
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Combine Filters
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
const combined = combineFilters(['bass', 'nightcore']);
|
|
380
|
+
console.log(combined);
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Queue Persistence
|
|
384
|
+
|
|
385
|
+
Save and restore queues across bot restarts.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { QueuePersistence, BookmarkManager } from 'soundcord';
|
|
389
|
+
|
|
390
|
+
const persistence = new QueuePersistence('./data/queues');
|
|
391
|
+
const bookmarks = new BookmarkManager('./data/bookmarks');
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Save Queue
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
await persistence.save({
|
|
398
|
+
guildId: '123456789',
|
|
399
|
+
songs: [
|
|
400
|
+
{ id: 'dQw4w9WgXcQ', title: 'Never Gonna Give You Up', author: 'Rick Astley', duration: 213 }
|
|
401
|
+
],
|
|
402
|
+
currentIndex: 0,
|
|
403
|
+
position: 45,
|
|
404
|
+
volume: 100,
|
|
405
|
+
loop: 'off'
|
|
56
406
|
});
|
|
407
|
+
```
|
|
57
408
|
|
|
58
|
-
|
|
59
|
-
const { url } = await yt.getStream('dQw4w9WgXcQ');
|
|
60
|
-
const resource = createAudioResource(url);
|
|
61
|
-
const player = createAudioPlayer();
|
|
409
|
+
### Load Queue
|
|
62
410
|
|
|
63
|
-
|
|
64
|
-
|
|
411
|
+
```typescript
|
|
412
|
+
const data = await persistence.load('123456789');
|
|
413
|
+
|
|
414
|
+
if (data) {
|
|
415
|
+
console.log(`Restored ${data.songs.length} songs`);
|
|
416
|
+
}
|
|
65
417
|
```
|
|
66
418
|
|
|
67
|
-
|
|
419
|
+
### Bookmarks
|
|
68
420
|
|
|
69
|
-
|
|
421
|
+
```typescript
|
|
422
|
+
await bookmarks.add('123456789', {
|
|
423
|
+
name: 'My Favorite',
|
|
424
|
+
videoId: 'dQw4w9WgXcQ',
|
|
425
|
+
title: 'Never Gonna Give You Up',
|
|
426
|
+
timestamp: 45
|
|
427
|
+
});
|
|
70
428
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
| `timeout` | number | 15000 | Request timeout in ms |
|
|
74
|
-
| `minDuration` | number | 0 | Min video duration to include in search |
|
|
75
|
-
| `cacheTTL` | number | 300000 | Stream URL cache duration (5min) |
|
|
76
|
-
| `ytdlpPath` | string | auto | Custom path to yt-dlp binary |
|
|
429
|
+
const userBookmarks = await bookmarks.list('123456789');
|
|
430
|
+
```
|
|
77
431
|
|
|
78
|
-
|
|
432
|
+
## Discord.js Integration
|
|
79
433
|
|
|
80
|
-
|
|
81
|
-
|
|
434
|
+
```typescript
|
|
435
|
+
import { YouTube } from 'soundcord';
|
|
436
|
+
import {
|
|
437
|
+
createAudioPlayer,
|
|
438
|
+
createAudioResource,
|
|
439
|
+
joinVoiceChannel,
|
|
440
|
+
StreamType
|
|
441
|
+
} from '@discordjs/voice';
|
|
442
|
+
import { spawn } from 'child_process';
|
|
82
443
|
|
|
83
|
-
|
|
84
|
-
Get video metadata by URL or ID.
|
|
444
|
+
const yt = new YouTube();
|
|
85
445
|
|
|
86
|
-
|
|
87
|
-
|
|
446
|
+
async function play(voiceChannel, query) {
|
|
447
|
+
const connection = joinVoiceChannel({
|
|
448
|
+
channelId: voiceChannel.id,
|
|
449
|
+
guildId: voiceChannel.guild.id,
|
|
450
|
+
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
|
|
451
|
+
selfDeaf: true
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const results = await yt.search(query, 1);
|
|
455
|
+
const stream = await yt.getStream(results[0].id);
|
|
456
|
+
|
|
457
|
+
const ffmpeg = spawn('ffmpeg', [
|
|
458
|
+
'-reconnect', '1',
|
|
459
|
+
'-reconnect_streamed', '1',
|
|
460
|
+
'-reconnect_delay_max', '5',
|
|
461
|
+
'-i', stream.url,
|
|
462
|
+
'-vn',
|
|
463
|
+
'-f', 's16le',
|
|
464
|
+
'-ar', '48000',
|
|
465
|
+
'-ac', '2',
|
|
466
|
+
'-'
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
const resource = createAudioResource(ffmpeg.stdout, {
|
|
470
|
+
inputType: StreamType.Raw
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const player = createAudioPlayer();
|
|
474
|
+
player.play(resource);
|
|
475
|
+
connection.subscribe(player);
|
|
476
|
+
|
|
477
|
+
return results[0];
|
|
478
|
+
}
|
|
479
|
+
```
|
|
88
480
|
|
|
89
|
-
|
|
90
|
-
Check if string is a valid YouTube URL.
|
|
481
|
+
## Configuration
|
|
91
482
|
|
|
92
|
-
|
|
93
|
-
|
|
483
|
+
```typescript
|
|
484
|
+
const yt = new YouTube({
|
|
485
|
+
timeout: 15000,
|
|
486
|
+
minDuration: 0,
|
|
487
|
+
cacheTTL: 30000,
|
|
488
|
+
ytdlpPath: '/custom/path/to/yt-dlp'
|
|
489
|
+
});
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
| Option | Type | Default | Description |
|
|
493
|
+
|--------|------|---------|-------------|
|
|
494
|
+
| `timeout` | number | 15000 | Request timeout (ms) |
|
|
495
|
+
| `minDuration` | number | 0 | Min video duration for search |
|
|
496
|
+
| `cacheTTL` | number | 30000 | Stream URL cache (ms) |
|
|
497
|
+
| `ytdlpPath` | string | auto | Custom yt-dlp path |
|
|
498
|
+
|
|
499
|
+
## Error Handling
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { YouTube, YouTubeError } from 'soundcord';
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
await yt.getStream('invalid');
|
|
506
|
+
} catch (err) {
|
|
507
|
+
if (err instanceof YouTubeError) {
|
|
508
|
+
switch (err.code) {
|
|
509
|
+
case 'NOT_FOUND':
|
|
510
|
+
console.log('Video not found');
|
|
511
|
+
break;
|
|
512
|
+
case 'NETWORK_ERROR':
|
|
513
|
+
console.log('Network error');
|
|
514
|
+
break;
|
|
515
|
+
case 'YTDLP_MISSING':
|
|
516
|
+
console.log('yt-dlp not installed');
|
|
517
|
+
break;
|
|
518
|
+
case 'RATE_LIMITED':
|
|
519
|
+
console.log('Rate limited by YouTube');
|
|
520
|
+
break;
|
|
521
|
+
case 'PARSE_ERROR':
|
|
522
|
+
console.log('Failed to parse response');
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
```
|
|
94
528
|
|
|
95
529
|
## Types
|
|
96
530
|
|
|
@@ -117,35 +551,69 @@ interface StreamInfo {
|
|
|
117
551
|
mimeType: string;
|
|
118
552
|
bitrate: number;
|
|
119
553
|
}
|
|
120
|
-
```
|
|
121
554
|
|
|
122
|
-
|
|
555
|
+
interface PlaylistInfo {
|
|
556
|
+
id: string;
|
|
557
|
+
title: string;
|
|
558
|
+
author: string;
|
|
559
|
+
videoCount: number;
|
|
560
|
+
videos: SearchResult[];
|
|
561
|
+
}
|
|
123
562
|
|
|
124
|
-
|
|
125
|
-
|
|
563
|
+
interface SpotifyTrack {
|
|
564
|
+
name: string;
|
|
565
|
+
artist: string;
|
|
566
|
+
album: string;
|
|
567
|
+
duration: number;
|
|
568
|
+
url: string;
|
|
569
|
+
}
|
|
126
570
|
|
|
127
|
-
|
|
571
|
+
interface RadioStation {
|
|
572
|
+
name: string;
|
|
573
|
+
url: string;
|
|
574
|
+
genre?: string;
|
|
575
|
+
country?: string;
|
|
576
|
+
bitrate?: number;
|
|
577
|
+
}
|
|
128
578
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
579
|
+
interface LyricsResult {
|
|
580
|
+
title: string;
|
|
581
|
+
artist: string;
|
|
582
|
+
lyrics: string;
|
|
583
|
+
synced?: string;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
interface Chapter {
|
|
587
|
+
title: string;
|
|
588
|
+
start: number;
|
|
589
|
+
end: number;
|
|
135
590
|
}
|
|
136
591
|
```
|
|
137
592
|
|
|
593
|
+
## Comparison
|
|
594
|
+
|
|
595
|
+
| Feature | soundcord | discord-player | distube |
|
|
596
|
+
|---------|-----------|----------------|---------|
|
|
597
|
+
| No ytdl-core | ✅ | ❌ | ❌ |
|
|
598
|
+
| Radio streams | ✅ | ❌ | ❌ |
|
|
599
|
+
| Synced lyrics | ✅ | ❌ | Plugin |
|
|
600
|
+
| YouTube chapters | ✅ | ❌ | ❌ |
|
|
601
|
+
| Audio normalization | ✅ | ❌ | ❌ |
|
|
602
|
+
| Quality selection | ✅ | ❌ | ❌ |
|
|
603
|
+
| Queue persistence | ✅ | ❌ | ❌ |
|
|
604
|
+
| Bundle size | ~50KB | ~500KB+ | ~300KB+ |
|
|
605
|
+
|
|
138
606
|
## Disclaimer
|
|
139
607
|
|
|
140
|
-
This library is
|
|
608
|
+
This library is for **educational and personal use only**.
|
|
141
609
|
|
|
142
610
|
By using this software, you agree to:
|
|
143
611
|
- Comply with YouTube's [Terms of Service](https://www.youtube.com/t/terms)
|
|
612
|
+
- Comply with Spotify's [Terms of Service](https://www.spotify.com/legal/end-user-agreement/)
|
|
144
613
|
- Use the library responsibly and ethically
|
|
145
|
-
- Not use it for commercial purposes without proper licensing
|
|
146
614
|
- Respect copyright and intellectual property rights
|
|
147
615
|
|
|
148
|
-
The authors are not responsible for any misuse of this library
|
|
616
|
+
The authors are not responsible for any misuse of this library.
|
|
149
617
|
|
|
150
618
|
## License
|
|
151
619
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chapters.d.ts","sourceRoot":"","sources":["../src/chapters.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,MAAM;IAIrB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CA8E/C"}
|