distube 5.0.1 → 5.0.3
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/dist/index.d.mts +1489 -0
- package/dist/index.d.ts +23 -6
- package/dist/index.js +710 -831
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2463 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +41 -31
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2463 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// src/type.ts
|
|
11
|
+
var Events = /* @__PURE__ */ ((Events2) => {
|
|
12
|
+
Events2["ERROR"] = "error";
|
|
13
|
+
Events2["ADD_LIST"] = "addList";
|
|
14
|
+
Events2["ADD_SONG"] = "addSong";
|
|
15
|
+
Events2["PLAY_SONG"] = "playSong";
|
|
16
|
+
Events2["FINISH_SONG"] = "finishSong";
|
|
17
|
+
Events2["EMPTY"] = "empty";
|
|
18
|
+
Events2["FINISH"] = "finish";
|
|
19
|
+
Events2["INIT_QUEUE"] = "initQueue";
|
|
20
|
+
Events2["NO_RELATED"] = "noRelated";
|
|
21
|
+
Events2["DISCONNECT"] = "disconnect";
|
|
22
|
+
Events2["DELETE_QUEUE"] = "deleteQueue";
|
|
23
|
+
Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
|
|
24
|
+
Events2["DEBUG"] = "debug";
|
|
25
|
+
return Events2;
|
|
26
|
+
})(Events || {});
|
|
27
|
+
var RepeatMode = /* @__PURE__ */ ((RepeatMode2) => {
|
|
28
|
+
RepeatMode2[RepeatMode2["DISABLED"] = 0] = "DISABLED";
|
|
29
|
+
RepeatMode2[RepeatMode2["SONG"] = 1] = "SONG";
|
|
30
|
+
RepeatMode2[RepeatMode2["QUEUE"] = 2] = "QUEUE";
|
|
31
|
+
return RepeatMode2;
|
|
32
|
+
})(RepeatMode || {});
|
|
33
|
+
var PluginType = /* @__PURE__ */ ((PluginType2) => {
|
|
34
|
+
PluginType2["EXTRACTOR"] = "extractor";
|
|
35
|
+
PluginType2["INFO_EXTRACTOR"] = "info-extractor";
|
|
36
|
+
PluginType2["PLAYABLE_EXTRACTOR"] = "playable-extractor";
|
|
37
|
+
return PluginType2;
|
|
38
|
+
})(PluginType || {});
|
|
39
|
+
|
|
40
|
+
// src/constant.ts
|
|
41
|
+
var defaultFilters = {
|
|
42
|
+
"3d": "apulsator=hz=0.125",
|
|
43
|
+
bassboost: "bass=g=10",
|
|
44
|
+
echo: "aecho=0.8:0.9:1000:0.3",
|
|
45
|
+
flanger: "flanger",
|
|
46
|
+
gate: "agate",
|
|
47
|
+
haas: "haas",
|
|
48
|
+
karaoke: "stereotools=mlev=0.1",
|
|
49
|
+
nightcore: "asetrate=48000*1.25,aresample=48000,bass=g=5",
|
|
50
|
+
reverse: "areverse",
|
|
51
|
+
vaporwave: "asetrate=48000*0.8,aresample=48000,atempo=1.1",
|
|
52
|
+
mcompand: "mcompand",
|
|
53
|
+
phaser: "aphaser",
|
|
54
|
+
tremolo: "tremolo",
|
|
55
|
+
surround: "surround",
|
|
56
|
+
earwax: "earwax"
|
|
57
|
+
};
|
|
58
|
+
var defaultOptions = {
|
|
59
|
+
plugins: [],
|
|
60
|
+
emitNewSongOnly: false,
|
|
61
|
+
savePreviousSongs: true,
|
|
62
|
+
nsfw: false,
|
|
63
|
+
emitAddSongWhenCreatingQueue: true,
|
|
64
|
+
emitAddListWhenCreatingQueue: true,
|
|
65
|
+
joinNewVoiceChannel: true
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/struct/DisTubeError.ts
|
|
69
|
+
import { inspect } from "node:util";
|
|
70
|
+
var ERROR_MESSAGES = {
|
|
71
|
+
INVALID_TYPE: /* @__PURE__ */ __name((expected, got, name) => `Expected ${Array.isArray(expected) ? expected.map((e) => typeof e === "number" ? e : `'${e}'`).join(" or ") : `'${expected}'`}${name ? ` for '${name}'` : ""}, but got ${inspect(got)} (${typeof got})`, "INVALID_TYPE"),
|
|
72
|
+
NUMBER_COMPARE: /* @__PURE__ */ __name((name, expected, value) => `'${name}' must be ${expected} ${value}`, "NUMBER_COMPARE"),
|
|
73
|
+
EMPTY_ARRAY: /* @__PURE__ */ __name((name) => `'${name}' is an empty array`, "EMPTY_ARRAY"),
|
|
74
|
+
EMPTY_FILTERED_ARRAY: /* @__PURE__ */ __name((name, type) => `There is no valid '${type}' in the '${name}' array`, "EMPTY_FILTERED_ARRAY"),
|
|
75
|
+
EMPTY_STRING: /* @__PURE__ */ __name((name) => `'${name}' string must not be empty`, "EMPTY_STRING"),
|
|
76
|
+
INVALID_KEY: /* @__PURE__ */ __name((obj, key) => `'${key}' does not need to be provided in ${obj}`, "INVALID_KEY"),
|
|
77
|
+
MISSING_KEY: /* @__PURE__ */ __name((obj, key) => `'${key}' needs to be provided in ${obj}`, "MISSING_KEY"),
|
|
78
|
+
MISSING_KEYS: /* @__PURE__ */ __name((obj, key, all) => `${key.map((k) => `'${k}'`).join(all ? " and " : " or ")} need to be provided in ${obj}`, "MISSING_KEYS"),
|
|
79
|
+
MISSING_INTENTS: /* @__PURE__ */ __name((i) => `${i} intent must be provided for the Client`, "MISSING_INTENTS"),
|
|
80
|
+
DISABLED_OPTION: /* @__PURE__ */ __name((o) => `DisTubeOptions.${o} is disabled`, "DISABLED_OPTION"),
|
|
81
|
+
ENABLED_OPTION: /* @__PURE__ */ __name((o) => `DisTubeOptions.${o} is enabled`, "ENABLED_OPTION"),
|
|
82
|
+
NOT_IN_VOICE: "User is not in any voice channel",
|
|
83
|
+
VOICE_FULL: "The voice channel is full",
|
|
84
|
+
VOICE_ALREADY_CREATED: "This guild already has a voice connection which is not managed by DisTube",
|
|
85
|
+
VOICE_CONNECT_FAILED: /* @__PURE__ */ __name((s) => `Cannot connect to the voice channel after ${s} seconds`, "VOICE_CONNECT_FAILED"),
|
|
86
|
+
VOICE_MISSING_PERMS: "I do not have permission to join this voice channel",
|
|
87
|
+
VOICE_RECONNECT_FAILED: "Cannot reconnect to the voice channel",
|
|
88
|
+
VOICE_DIFFERENT_GUILD: "Cannot join a voice channel in a different guild",
|
|
89
|
+
VOICE_DIFFERENT_CLIENT: "Cannot join a voice channel created by a different client",
|
|
90
|
+
FFMPEG_EXITED: /* @__PURE__ */ __name((code) => `ffmpeg exited with code ${code}`, "FFMPEG_EXITED"),
|
|
91
|
+
FFMPEG_NOT_INSTALLED: /* @__PURE__ */ __name((path) => `ffmpeg is not installed at '${path}' path`, "FFMPEG_NOT_INSTALLED"),
|
|
92
|
+
ENCRYPTION_LIBRARIES_MISSING: "Cannot play audio as no valid encryption package is installed.\nPlease install sodium-native, sodium, libsodium-wrappers, or tweetnacl.",
|
|
93
|
+
NO_QUEUE: "There is no playing queue in this guild",
|
|
94
|
+
QUEUE_EXIST: "This guild has a Queue already",
|
|
95
|
+
QUEUE_STOPPED: "The queue has been stopped already",
|
|
96
|
+
PAUSED: "The queue has been paused already",
|
|
97
|
+
RESUMED: "The queue has been playing already",
|
|
98
|
+
NO_PREVIOUS: "There is no previous song in this queue",
|
|
99
|
+
NO_UP_NEXT: "There is no up next song",
|
|
100
|
+
NO_SONG_POSITION: "Does not have any song at this position",
|
|
101
|
+
NO_PLAYING_SONG: "There is no playing song in the queue",
|
|
102
|
+
NO_EXTRACTOR_PLUGIN: "There is no extractor plugin in the DisTubeOptions.plugins, please add one for searching songs",
|
|
103
|
+
NO_RELATED: "Cannot find any related songs",
|
|
104
|
+
CANNOT_PLAY_RELATED: "Cannot play the related song",
|
|
105
|
+
UNAVAILABLE_VIDEO: "This video is unavailable",
|
|
106
|
+
UNPLAYABLE_FORMATS: "No playable format found",
|
|
107
|
+
NON_NSFW: "Cannot play age-restricted content in non-NSFW channel",
|
|
108
|
+
NOT_SUPPORTED_URL: "This url is not supported",
|
|
109
|
+
NOT_SUPPORTED_SONG: /* @__PURE__ */ __name((song) => `There is no plugin supporting this song (${song})`, "NOT_SUPPORTED_SONG"),
|
|
110
|
+
NO_VALID_SONG: "'songs' array does not have any valid Song or url",
|
|
111
|
+
CANNOT_RESOLVE_SONG: /* @__PURE__ */ __name((t) => `Cannot resolve ${inspect(t)} to a Song`, "CANNOT_RESOLVE_SONG"),
|
|
112
|
+
CANNOT_GET_STREAM_URL: /* @__PURE__ */ __name((song) => `Cannot get stream url with this song (${song})`, "CANNOT_GET_STREAM_URL"),
|
|
113
|
+
CANNOT_GET_SEARCH_QUERY: /* @__PURE__ */ __name((song) => `Cannot get search query with this song (${song})`, "CANNOT_GET_SEARCH_QUERY"),
|
|
114
|
+
NO_RESULT: /* @__PURE__ */ __name((query) => `Cannot find any song with this query (${query})`, "NO_RESULT"),
|
|
115
|
+
NO_STREAM_URL: /* @__PURE__ */ __name((song) => `No stream url attached (${song})`, "NO_STREAM_URL"),
|
|
116
|
+
EMPTY_FILTERED_PLAYLIST: "There is no valid video in the playlist\nMaybe age-restricted contents is filtered because you are in non-NSFW channel",
|
|
117
|
+
EMPTY_PLAYLIST: "There is no valid video in the playlist"
|
|
118
|
+
};
|
|
119
|
+
var haveCode = /* @__PURE__ */ __name((code) => Object.keys(ERROR_MESSAGES).includes(code), "haveCode");
|
|
120
|
+
var parseMessage = /* @__PURE__ */ __name((m, ...args) => typeof m === "string" ? m : m(...args), "parseMessage");
|
|
121
|
+
var getErrorMessage = /* @__PURE__ */ __name((code, ...args) => haveCode(code) ? parseMessage(ERROR_MESSAGES[code], ...args) : args[0], "getErrorMessage");
|
|
122
|
+
var DisTubeError = class _DisTubeError extends Error {
|
|
123
|
+
static {
|
|
124
|
+
__name(this, "DisTubeError");
|
|
125
|
+
}
|
|
126
|
+
errorCode;
|
|
127
|
+
constructor(code, ...args) {
|
|
128
|
+
super(getErrorMessage(code, ...args));
|
|
129
|
+
this.errorCode = code;
|
|
130
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, _DisTubeError);
|
|
131
|
+
}
|
|
132
|
+
get name() {
|
|
133
|
+
return `DisTubeError [${this.errorCode}]`;
|
|
134
|
+
}
|
|
135
|
+
get code() {
|
|
136
|
+
return this.errorCode;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/struct/TaskQueue.ts
|
|
141
|
+
var Task = class {
|
|
142
|
+
static {
|
|
143
|
+
__name(this, "Task");
|
|
144
|
+
}
|
|
145
|
+
resolve;
|
|
146
|
+
promise;
|
|
147
|
+
constructor() {
|
|
148
|
+
this.promise = new Promise((res) => {
|
|
149
|
+
this.resolve = res;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var TaskQueue = class {
|
|
154
|
+
static {
|
|
155
|
+
__name(this, "TaskQueue");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The task array
|
|
159
|
+
*/
|
|
160
|
+
#tasks = [];
|
|
161
|
+
/**
|
|
162
|
+
* Waits for last task finished and queues a new task
|
|
163
|
+
*/
|
|
164
|
+
queuing() {
|
|
165
|
+
const next = this.remaining ? this.#tasks[this.#tasks.length - 1].promise : Promise.resolve();
|
|
166
|
+
this.#tasks.push(new Task());
|
|
167
|
+
return next;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Removes the finished task and processes the next task
|
|
171
|
+
*/
|
|
172
|
+
resolve() {
|
|
173
|
+
this.#tasks.shift()?.resolve();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The remaining number of tasks
|
|
177
|
+
*/
|
|
178
|
+
get remaining() {
|
|
179
|
+
return this.#tasks.length;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/struct/Playlist.ts
|
|
184
|
+
var Playlist = class {
|
|
185
|
+
static {
|
|
186
|
+
__name(this, "Playlist");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Playlist source.
|
|
190
|
+
*/
|
|
191
|
+
source;
|
|
192
|
+
/**
|
|
193
|
+
* Songs in the playlist.
|
|
194
|
+
*/
|
|
195
|
+
songs;
|
|
196
|
+
/**
|
|
197
|
+
* Playlist ID.
|
|
198
|
+
*/
|
|
199
|
+
id;
|
|
200
|
+
/**
|
|
201
|
+
* Playlist name.
|
|
202
|
+
*/
|
|
203
|
+
name;
|
|
204
|
+
/**
|
|
205
|
+
* Playlist URL.
|
|
206
|
+
*/
|
|
207
|
+
url;
|
|
208
|
+
/**
|
|
209
|
+
* Playlist thumbnail.
|
|
210
|
+
*/
|
|
211
|
+
thumbnail;
|
|
212
|
+
#metadata;
|
|
213
|
+
#member;
|
|
214
|
+
/**
|
|
215
|
+
* Create a Playlist
|
|
216
|
+
* @param playlist - Raw playlist info
|
|
217
|
+
* @param options - Optional data
|
|
218
|
+
*/
|
|
219
|
+
constructor(playlist, { member, metadata } = {}) {
|
|
220
|
+
if (!Array.isArray(playlist.songs) || !playlist.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
|
|
221
|
+
this.source = playlist.source.toLowerCase();
|
|
222
|
+
this.songs = playlist.songs;
|
|
223
|
+
this.name = playlist.name;
|
|
224
|
+
this.id = playlist.id;
|
|
225
|
+
this.url = playlist.url;
|
|
226
|
+
this.thumbnail = playlist.thumbnail;
|
|
227
|
+
this.member = member;
|
|
228
|
+
this.songs.forEach((s) => s.playlist = this);
|
|
229
|
+
this.metadata = metadata;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Playlist duration in second.
|
|
233
|
+
*/
|
|
234
|
+
get duration() {
|
|
235
|
+
return this.songs.reduce((prev, next) => prev + next.duration, 0);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Formatted duration string `hh:mm:ss`.
|
|
239
|
+
*/
|
|
240
|
+
get formattedDuration() {
|
|
241
|
+
return formatDuration(this.duration);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* User requested.
|
|
245
|
+
*/
|
|
246
|
+
get member() {
|
|
247
|
+
return this.#member;
|
|
248
|
+
}
|
|
249
|
+
set member(member) {
|
|
250
|
+
if (!isMemberInstance(member)) return;
|
|
251
|
+
this.#member = member;
|
|
252
|
+
this.songs.forEach((s) => s.member = this.member);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* User requested.
|
|
256
|
+
*/
|
|
257
|
+
get user() {
|
|
258
|
+
return this.member?.user;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Optional metadata that can be used to identify the playlist.
|
|
262
|
+
*/
|
|
263
|
+
get metadata() {
|
|
264
|
+
return this.#metadata;
|
|
265
|
+
}
|
|
266
|
+
set metadata(metadata) {
|
|
267
|
+
this.#metadata = metadata;
|
|
268
|
+
this.songs.forEach((s) => s.metadata = metadata);
|
|
269
|
+
}
|
|
270
|
+
toString() {
|
|
271
|
+
return `${this.name} (${this.songs.length} songs)`;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/struct/Song.ts
|
|
276
|
+
var Song = class {
|
|
277
|
+
static {
|
|
278
|
+
__name(this, "Song");
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The source of this song info
|
|
282
|
+
*/
|
|
283
|
+
source;
|
|
284
|
+
/**
|
|
285
|
+
* Song ID.
|
|
286
|
+
*/
|
|
287
|
+
id;
|
|
288
|
+
/**
|
|
289
|
+
* Song name.
|
|
290
|
+
*/
|
|
291
|
+
name;
|
|
292
|
+
/**
|
|
293
|
+
* Indicates if the song is an active live.
|
|
294
|
+
*/
|
|
295
|
+
isLive;
|
|
296
|
+
/**
|
|
297
|
+
* Song duration.
|
|
298
|
+
*/
|
|
299
|
+
duration;
|
|
300
|
+
/**
|
|
301
|
+
* Formatted duration string (`hh:mm:ss`, `mm:ss` or `Live`).
|
|
302
|
+
*/
|
|
303
|
+
formattedDuration;
|
|
304
|
+
/**
|
|
305
|
+
* Song URL.
|
|
306
|
+
*/
|
|
307
|
+
url;
|
|
308
|
+
/**
|
|
309
|
+
* Song thumbnail.
|
|
310
|
+
*/
|
|
311
|
+
thumbnail;
|
|
312
|
+
/**
|
|
313
|
+
* Song view count
|
|
314
|
+
*/
|
|
315
|
+
views;
|
|
316
|
+
/**
|
|
317
|
+
* Song like count
|
|
318
|
+
*/
|
|
319
|
+
likes;
|
|
320
|
+
/**
|
|
321
|
+
* Song dislike count
|
|
322
|
+
*/
|
|
323
|
+
dislikes;
|
|
324
|
+
/**
|
|
325
|
+
* Song repost (share) count
|
|
326
|
+
*/
|
|
327
|
+
reposts;
|
|
328
|
+
/**
|
|
329
|
+
* Song uploader
|
|
330
|
+
*/
|
|
331
|
+
uploader;
|
|
332
|
+
/**
|
|
333
|
+
* Whether or not an age-restricted content
|
|
334
|
+
*/
|
|
335
|
+
ageRestricted;
|
|
336
|
+
/**
|
|
337
|
+
* Stream info
|
|
338
|
+
*/
|
|
339
|
+
stream;
|
|
340
|
+
/**
|
|
341
|
+
* The plugin that created this song
|
|
342
|
+
*/
|
|
343
|
+
plugin;
|
|
344
|
+
#metadata;
|
|
345
|
+
#member;
|
|
346
|
+
#playlist;
|
|
347
|
+
/**
|
|
348
|
+
* Create a Song
|
|
349
|
+
*
|
|
350
|
+
* @param info - Raw song info
|
|
351
|
+
* @param options - Optional data
|
|
352
|
+
*/
|
|
353
|
+
constructor(info, { member, metadata } = {}) {
|
|
354
|
+
this.source = info.source.toLowerCase();
|
|
355
|
+
this.metadata = metadata;
|
|
356
|
+
this.member = member;
|
|
357
|
+
this.id = info.id;
|
|
358
|
+
this.name = info.name;
|
|
359
|
+
this.isLive = info.isLive;
|
|
360
|
+
this.duration = this.isLive || !info.duration ? 0 : info.duration;
|
|
361
|
+
this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
|
|
362
|
+
this.url = info.url;
|
|
363
|
+
this.thumbnail = info.thumbnail;
|
|
364
|
+
this.views = info.views;
|
|
365
|
+
this.likes = info.likes;
|
|
366
|
+
this.dislikes = info.dislikes;
|
|
367
|
+
this.reposts = info.reposts;
|
|
368
|
+
this.uploader = {
|
|
369
|
+
name: info.uploader?.name,
|
|
370
|
+
url: info.uploader?.url
|
|
371
|
+
};
|
|
372
|
+
this.ageRestricted = info.ageRestricted;
|
|
373
|
+
this.stream = { playFromSource: info.playFromSource };
|
|
374
|
+
this.plugin = info.plugin;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The playlist this song belongs to
|
|
378
|
+
*/
|
|
379
|
+
get playlist() {
|
|
380
|
+
return this.#playlist;
|
|
381
|
+
}
|
|
382
|
+
set playlist(playlist) {
|
|
383
|
+
if (!(playlist instanceof Playlist)) throw new DisTubeError("INVALID_TYPE", "Playlist", playlist, "Song#playlist");
|
|
384
|
+
this.#playlist = playlist;
|
|
385
|
+
this.member = playlist.member;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* User requested to play this song.
|
|
389
|
+
*/
|
|
390
|
+
get member() {
|
|
391
|
+
return this.#member;
|
|
392
|
+
}
|
|
393
|
+
set member(member) {
|
|
394
|
+
if (isMemberInstance(member)) this.#member = member;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* User requested to play this song.
|
|
398
|
+
*/
|
|
399
|
+
get user() {
|
|
400
|
+
return this.member?.user;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Optional metadata that can be used to identify the song. This is attached by the
|
|
404
|
+
* {@link DisTube#play} method.
|
|
405
|
+
*/
|
|
406
|
+
get metadata() {
|
|
407
|
+
return this.#metadata;
|
|
408
|
+
}
|
|
409
|
+
set metadata(metadata) {
|
|
410
|
+
this.#metadata = metadata;
|
|
411
|
+
}
|
|
412
|
+
toString() {
|
|
413
|
+
return this.name || this.url || this.id || "Unknown";
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/core/DisTubeBase.ts
|
|
418
|
+
var DisTubeBase = class {
|
|
419
|
+
static {
|
|
420
|
+
__name(this, "DisTubeBase");
|
|
421
|
+
}
|
|
422
|
+
distube;
|
|
423
|
+
constructor(distube) {
|
|
424
|
+
this.distube = distube;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Emit the {@link DisTube} of this base
|
|
428
|
+
* @param eventName - Event name
|
|
429
|
+
* @param args - arguments
|
|
430
|
+
*/
|
|
431
|
+
emit(eventName, ...args) {
|
|
432
|
+
return this.distube.emit(eventName, ...args);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Emit error event
|
|
436
|
+
* @param error - error
|
|
437
|
+
* @param queue - The queue encountered the error
|
|
438
|
+
* @param song - The playing song when encountered the error
|
|
439
|
+
*/
|
|
440
|
+
emitError(error, queue, song) {
|
|
441
|
+
this.distube.emitError(error, queue, song);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Emit debug event
|
|
445
|
+
* @param message - debug message
|
|
446
|
+
*/
|
|
447
|
+
debug(message) {
|
|
448
|
+
this.distube.debug(message);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* The queue manager
|
|
452
|
+
*/
|
|
453
|
+
get queues() {
|
|
454
|
+
return this.distube.queues;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* The voice manager
|
|
458
|
+
*/
|
|
459
|
+
get voices() {
|
|
460
|
+
return this.distube.voices;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Discord.js client
|
|
464
|
+
*/
|
|
465
|
+
get client() {
|
|
466
|
+
return this.distube.client;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* DisTube options
|
|
470
|
+
*/
|
|
471
|
+
get options() {
|
|
472
|
+
return this.distube.options;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* DisTube handler
|
|
476
|
+
*/
|
|
477
|
+
get handler() {
|
|
478
|
+
return this.distube.handler;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* DisTube plugins
|
|
482
|
+
*/
|
|
483
|
+
get plugins() {
|
|
484
|
+
return this.distube.plugins;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/core/DisTubeVoice.ts
|
|
489
|
+
import { Constants } from "discord.js";
|
|
490
|
+
import { TypedEmitter } from "tiny-typed-emitter";
|
|
491
|
+
import {
|
|
492
|
+
AudioPlayerStatus,
|
|
493
|
+
VoiceConnectionDisconnectReason,
|
|
494
|
+
VoiceConnectionStatus,
|
|
495
|
+
createAudioPlayer,
|
|
496
|
+
entersState,
|
|
497
|
+
joinVoiceChannel
|
|
498
|
+
} from "@discordjs/voice";
|
|
499
|
+
var DisTubeVoice = class extends TypedEmitter {
|
|
500
|
+
static {
|
|
501
|
+
__name(this, "DisTubeVoice");
|
|
502
|
+
}
|
|
503
|
+
id;
|
|
504
|
+
voices;
|
|
505
|
+
audioPlayer;
|
|
506
|
+
connection;
|
|
507
|
+
emittedError;
|
|
508
|
+
isDisconnected = false;
|
|
509
|
+
stream;
|
|
510
|
+
#channel;
|
|
511
|
+
#volume = 100;
|
|
512
|
+
constructor(voiceManager, channel) {
|
|
513
|
+
super();
|
|
514
|
+
this.voices = voiceManager;
|
|
515
|
+
this.id = channel.guildId;
|
|
516
|
+
this.channel = channel;
|
|
517
|
+
this.voices.add(this.id, this);
|
|
518
|
+
this.audioPlayer = createAudioPlayer().on(AudioPlayerStatus.Idle, (oldState) => {
|
|
519
|
+
if (oldState.status !== AudioPlayerStatus.Idle) this.emit("finish");
|
|
520
|
+
}).on("error", (error) => {
|
|
521
|
+
if (this.emittedError) return;
|
|
522
|
+
this.emittedError = true;
|
|
523
|
+
this.emit("error", error);
|
|
524
|
+
});
|
|
525
|
+
this.connection.on(VoiceConnectionStatus.Disconnected, (_, newState) => {
|
|
526
|
+
if (newState.reason === VoiceConnectionDisconnectReason.Manual) {
|
|
527
|
+
this.leave();
|
|
528
|
+
} else if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
|
|
529
|
+
entersState(this.connection, VoiceConnectionStatus.Connecting, 5e3).catch(() => {
|
|
530
|
+
if (![VoiceConnectionStatus.Ready, VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
|
|
531
|
+
this.leave();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
} else if (this.connection.rejoinAttempts < 5) {
|
|
535
|
+
setTimeout(
|
|
536
|
+
() => {
|
|
537
|
+
this.connection.rejoin();
|
|
538
|
+
},
|
|
539
|
+
(this.connection.rejoinAttempts + 1) * 5e3
|
|
540
|
+
).unref();
|
|
541
|
+
} else if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) {
|
|
542
|
+
this.leave(new DisTubeError("VOICE_RECONNECT_FAILED"));
|
|
543
|
+
}
|
|
544
|
+
}).on(VoiceConnectionStatus.Destroyed, () => {
|
|
545
|
+
this.leave();
|
|
546
|
+
}).on("error", () => void 0);
|
|
547
|
+
this.connection.subscribe(this.audioPlayer);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* The voice channel id the bot is in
|
|
551
|
+
*/
|
|
552
|
+
get channelId() {
|
|
553
|
+
return this.connection?.joinConfig?.channelId ?? void 0;
|
|
554
|
+
}
|
|
555
|
+
get channel() {
|
|
556
|
+
if (!this.channelId) return this.#channel;
|
|
557
|
+
if (this.#channel?.id === this.channelId) return this.#channel;
|
|
558
|
+
const channel = this.voices.client.channels.cache.get(this.channelId);
|
|
559
|
+
if (!channel) return this.#channel;
|
|
560
|
+
for (const type of Constants.VoiceBasedChannelTypes) {
|
|
561
|
+
if (channel.type === type) {
|
|
562
|
+
this.#channel = channel;
|
|
563
|
+
return channel;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return this.#channel;
|
|
567
|
+
}
|
|
568
|
+
set channel(channel) {
|
|
569
|
+
if (!isSupportedVoiceChannel(channel)) {
|
|
570
|
+
throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", channel, "DisTubeVoice#channel");
|
|
571
|
+
}
|
|
572
|
+
if (channel.guildId !== this.id) throw new DisTubeError("VOICE_DIFFERENT_GUILD");
|
|
573
|
+
if (channel.client.user?.id !== this.voices.client.user?.id) throw new DisTubeError("VOICE_DIFFERENT_CLIENT");
|
|
574
|
+
if (channel.id === this.channelId) return;
|
|
575
|
+
if (!channel.joinable) {
|
|
576
|
+
if (channel.full) throw new DisTubeError("VOICE_FULL");
|
|
577
|
+
else throw new DisTubeError("VOICE_MISSING_PERMS");
|
|
578
|
+
}
|
|
579
|
+
this.connection = this.#join(channel);
|
|
580
|
+
this.#channel = channel;
|
|
581
|
+
}
|
|
582
|
+
#join(channel) {
|
|
583
|
+
return joinVoiceChannel({
|
|
584
|
+
channelId: channel.id,
|
|
585
|
+
guildId: this.id,
|
|
586
|
+
adapterCreator: channel.guild.voiceAdapterCreator,
|
|
587
|
+
group: channel.client.user?.id
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Join a voice channel with this connection
|
|
592
|
+
* @param channel - A voice channel
|
|
593
|
+
*/
|
|
594
|
+
async join(channel) {
|
|
595
|
+
const TIMEOUT = 3e4;
|
|
596
|
+
if (channel) this.channel = channel;
|
|
597
|
+
try {
|
|
598
|
+
await entersState(this.connection, VoiceConnectionStatus.Ready, TIMEOUT);
|
|
599
|
+
} catch {
|
|
600
|
+
if (this.connection.state.status === VoiceConnectionStatus.Ready) return this;
|
|
601
|
+
if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) this.connection.destroy();
|
|
602
|
+
this.voices.remove(this.id);
|
|
603
|
+
throw new DisTubeError("VOICE_CONNECT_FAILED", TIMEOUT / 1e3);
|
|
604
|
+
}
|
|
605
|
+
return this;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Leave the voice channel of this connection
|
|
609
|
+
* @param error - Optional, an error to emit with 'error' event.
|
|
610
|
+
*/
|
|
611
|
+
leave(error) {
|
|
612
|
+
this.stop(true);
|
|
613
|
+
if (!this.isDisconnected) {
|
|
614
|
+
this.emit("disconnect", error);
|
|
615
|
+
this.isDisconnected = true;
|
|
616
|
+
}
|
|
617
|
+
if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) this.connection.destroy();
|
|
618
|
+
this.voices.remove(this.id);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Stop the playing stream
|
|
622
|
+
* @param force - If true, will force the {@link DisTubeVoice#audioPlayer} to enter the Idle state even
|
|
623
|
+
* if the {@link DisTubeStream#audioResource} has silence padding frames.
|
|
624
|
+
*/
|
|
625
|
+
stop(force = false) {
|
|
626
|
+
this.audioPlayer.stop(force);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Play a {@link DisTubeStream}
|
|
630
|
+
* @param dtStream - DisTubeStream
|
|
631
|
+
*/
|
|
632
|
+
play(dtStream) {
|
|
633
|
+
if (!checkEncryptionLibraries()) {
|
|
634
|
+
dtStream.kill();
|
|
635
|
+
throw new DisTubeError("ENCRYPTION_LIBRARIES_MISSING");
|
|
636
|
+
}
|
|
637
|
+
this.emittedError = false;
|
|
638
|
+
dtStream.on("error", (error) => {
|
|
639
|
+
if (this.emittedError || error.code === "ERR_STREAM_PREMATURE_CLOSE") return;
|
|
640
|
+
this.emittedError = true;
|
|
641
|
+
this.emit("error", error);
|
|
642
|
+
});
|
|
643
|
+
if (this.audioPlayer.state.status !== AudioPlayerStatus.Paused) this.audioPlayer.play(dtStream.audioResource);
|
|
644
|
+
this.stream?.kill();
|
|
645
|
+
this.stream = dtStream;
|
|
646
|
+
this.volume = this.#volume;
|
|
647
|
+
dtStream.spawn();
|
|
648
|
+
}
|
|
649
|
+
set volume(volume) {
|
|
650
|
+
if (typeof volume !== "number" || isNaN(volume)) {
|
|
651
|
+
throw new DisTubeError("INVALID_TYPE", "number", volume, "volume");
|
|
652
|
+
}
|
|
653
|
+
if (volume < 0) {
|
|
654
|
+
throw new DisTubeError("NUMBER_COMPARE", "Volume", "bigger or equal to", 0);
|
|
655
|
+
}
|
|
656
|
+
this.#volume = volume;
|
|
657
|
+
this.stream?.setVolume(Math.pow(this.#volume / 100, 0.5 / Math.log10(2)));
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Get or set the volume percentage
|
|
661
|
+
*/
|
|
662
|
+
get volume() {
|
|
663
|
+
return this.#volume;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Playback duration of the audio resource in seconds
|
|
667
|
+
*/
|
|
668
|
+
get playbackDuration() {
|
|
669
|
+
return (this.stream?.audioResource?.playbackDuration ?? 0) / 1e3;
|
|
670
|
+
}
|
|
671
|
+
pause() {
|
|
672
|
+
this.audioPlayer.pause();
|
|
673
|
+
}
|
|
674
|
+
unpause() {
|
|
675
|
+
const state = this.audioPlayer.state;
|
|
676
|
+
if (state.status !== AudioPlayerStatus.Paused) return;
|
|
677
|
+
if (this.stream?.audioResource && state.resource !== this.stream.audioResource) {
|
|
678
|
+
this.audioPlayer.play(this.stream.audioResource);
|
|
679
|
+
} else {
|
|
680
|
+
this.audioPlayer.unpause();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Whether the bot is self-deafened
|
|
685
|
+
*/
|
|
686
|
+
get selfDeaf() {
|
|
687
|
+
return this.connection.joinConfig.selfDeaf;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Whether the bot is self-muted
|
|
691
|
+
*/
|
|
692
|
+
get selfMute() {
|
|
693
|
+
return this.connection.joinConfig.selfMute;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Self-deafens/undeafens the bot.
|
|
697
|
+
* @param selfDeaf - Whether or not the bot should be self-deafened
|
|
698
|
+
* @returns true if the voice state was successfully updated, otherwise false
|
|
699
|
+
*/
|
|
700
|
+
setSelfDeaf(selfDeaf) {
|
|
701
|
+
if (typeof selfDeaf !== "boolean") {
|
|
702
|
+
throw new DisTubeError("INVALID_TYPE", "boolean", selfDeaf, "selfDeaf");
|
|
703
|
+
}
|
|
704
|
+
return this.connection.rejoin({
|
|
705
|
+
...this.connection.joinConfig,
|
|
706
|
+
selfDeaf
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Self-mutes/unmutes the bot.
|
|
711
|
+
* @param selfMute - Whether or not the bot should be self-muted
|
|
712
|
+
* @returns true if the voice state was successfully updated, otherwise false
|
|
713
|
+
*/
|
|
714
|
+
setSelfMute(selfMute) {
|
|
715
|
+
if (typeof selfMute !== "boolean") {
|
|
716
|
+
throw new DisTubeError("INVALID_TYPE", "boolean", selfMute, "selfMute");
|
|
717
|
+
}
|
|
718
|
+
return this.connection.rejoin({
|
|
719
|
+
...this.connection.joinConfig,
|
|
720
|
+
selfMute
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* The voice state of this connection
|
|
725
|
+
*/
|
|
726
|
+
get voiceState() {
|
|
727
|
+
return this.channel?.guild?.members?.me?.voice;
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// src/core/DisTubeStream.ts
|
|
732
|
+
import { Transform } from "stream";
|
|
733
|
+
import { spawn, spawnSync } from "child_process";
|
|
734
|
+
import { TypedEmitter as TypedEmitter2 } from "tiny-typed-emitter";
|
|
735
|
+
import { StreamType, createAudioResource } from "@discordjs/voice";
|
|
736
|
+
var checked = process.env.NODE_ENV === "test";
|
|
737
|
+
var checkFFmpeg = /* @__PURE__ */ __name((distube) => {
|
|
738
|
+
if (checked) return;
|
|
739
|
+
const path = distube.options.ffmpeg.path;
|
|
740
|
+
const debug = /* @__PURE__ */ __name((str) => distube.emit("ffmpegDebug" /* FFMPEG_DEBUG */, str), "debug");
|
|
741
|
+
try {
|
|
742
|
+
debug(`[test] spawn ffmpeg at '${path}' path`);
|
|
743
|
+
const process2 = spawnSync(path, ["-h"], { windowsHide: true, shell: true, encoding: "utf-8" });
|
|
744
|
+
if (process2.error) throw process2.error;
|
|
745
|
+
if (process2.stderr && !process2.stdout) throw new Error(process2.stderr);
|
|
746
|
+
const result = process2.output.join("\n");
|
|
747
|
+
const version2 = /ffmpeg version (\S+)/iu.exec(result)?.[1];
|
|
748
|
+
if (!version2) throw new Error("Invalid FFmpeg version");
|
|
749
|
+
debug(`[test] ffmpeg version: ${version2}`);
|
|
750
|
+
} catch (e) {
|
|
751
|
+
debug(`[test] failed to spawn ffmpeg at '${path}': ${e?.stack ?? e}`);
|
|
752
|
+
throw new DisTubeError("FFMPEG_NOT_INSTALLED", path);
|
|
753
|
+
}
|
|
754
|
+
checked = true;
|
|
755
|
+
}, "checkFFmpeg");
|
|
756
|
+
var DisTubeStream = class extends TypedEmitter2 {
|
|
757
|
+
static {
|
|
758
|
+
__name(this, "DisTubeStream");
|
|
759
|
+
}
|
|
760
|
+
#ffmpegPath;
|
|
761
|
+
#opts;
|
|
762
|
+
process;
|
|
763
|
+
stream;
|
|
764
|
+
audioResource;
|
|
765
|
+
/**
|
|
766
|
+
* Create a DisTubeStream to play with {@link DisTubeVoice}
|
|
767
|
+
* @param url - Stream URL
|
|
768
|
+
* @param options - Stream options
|
|
769
|
+
*/
|
|
770
|
+
constructor(url, options) {
|
|
771
|
+
super();
|
|
772
|
+
const { ffmpeg, seek } = options;
|
|
773
|
+
const opts = {
|
|
774
|
+
reconnect: 1,
|
|
775
|
+
reconnect_streamed: 1,
|
|
776
|
+
reconnect_delay_max: 5,
|
|
777
|
+
analyzeduration: 0,
|
|
778
|
+
hide_banner: true,
|
|
779
|
+
...ffmpeg.args.global,
|
|
780
|
+
...ffmpeg.args.input,
|
|
781
|
+
i: url,
|
|
782
|
+
ar: 48e3,
|
|
783
|
+
ac: 2,
|
|
784
|
+
...ffmpeg.args.output,
|
|
785
|
+
f: "s16le"
|
|
786
|
+
};
|
|
787
|
+
if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
|
|
788
|
+
const fileUrl = new URL(url);
|
|
789
|
+
if (fileUrl.protocol === "file:") {
|
|
790
|
+
opts.reconnect = null;
|
|
791
|
+
opts.reconnect_streamed = null;
|
|
792
|
+
opts.reconnect_delay_max = null;
|
|
793
|
+
opts.i = fileUrl.hostname + fileUrl.pathname;
|
|
794
|
+
}
|
|
795
|
+
this.#ffmpegPath = ffmpeg.path;
|
|
796
|
+
this.#opts = [
|
|
797
|
+
...Object.entries(opts).flatMap(
|
|
798
|
+
([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
|
|
799
|
+
).flat(),
|
|
800
|
+
"pipe:1"
|
|
801
|
+
];
|
|
802
|
+
this.stream = new VolumeTransformer();
|
|
803
|
+
this.stream.on("close", () => this.kill()).on("error", (err) => {
|
|
804
|
+
this.debug(`[stream] error: ${err.message}`);
|
|
805
|
+
this.emit("error", err);
|
|
806
|
+
}).on("finish", () => this.debug("[stream] log: stream finished"));
|
|
807
|
+
this.audioResource = createAudioResource(this.stream, { inputType: StreamType.Raw, inlineVolume: false });
|
|
808
|
+
}
|
|
809
|
+
spawn() {
|
|
810
|
+
this.debug(`[process] spawn: ${this.#ffmpegPath} ${this.#opts.join(" ")}`);
|
|
811
|
+
this.process = spawn(this.#ffmpegPath, this.#opts, {
|
|
812
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
813
|
+
shell: false,
|
|
814
|
+
windowsHide: true
|
|
815
|
+
}).on("error", (err) => {
|
|
816
|
+
this.debug(`[process] error: ${err.message}`);
|
|
817
|
+
this.emit("error", err);
|
|
818
|
+
}).on("exit", (code, signal) => {
|
|
819
|
+
this.debug(`[process] exit: code=${code ?? "unknown"} signal=${signal ?? "unknown"}`);
|
|
820
|
+
if (!code || [0, 255].includes(code)) return;
|
|
821
|
+
this.debug(`[process] error: ffmpeg exited with code ${code}`);
|
|
822
|
+
this.emit("error", new DisTubeError("FFMPEG_EXITED", code));
|
|
823
|
+
});
|
|
824
|
+
if (!this.process.stdout || !this.process.stderr) {
|
|
825
|
+
this.kill();
|
|
826
|
+
throw new Error("Failed to create ffmpeg process");
|
|
827
|
+
}
|
|
828
|
+
this.process.stdout.pipe(this.stream);
|
|
829
|
+
this.process.stderr.setEncoding("utf8")?.on("data", (data) => {
|
|
830
|
+
const lines = data.split(/\r\n|\r|\n/u);
|
|
831
|
+
for (const line of lines) {
|
|
832
|
+
if (/^\s*$/.test(line)) continue;
|
|
833
|
+
this.debug(`[ffmpeg] log: ${line}`);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
debug(debug) {
|
|
838
|
+
this.emit("debug", debug);
|
|
839
|
+
}
|
|
840
|
+
setVolume(volume) {
|
|
841
|
+
this.stream.vol = volume;
|
|
842
|
+
}
|
|
843
|
+
kill() {
|
|
844
|
+
if (!this.stream.destroyed) this.stream.destroy();
|
|
845
|
+
if (this.process && !this.process.killed) this.process.kill("SIGKILL");
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
var VolumeTransformer = class extends Transform {
|
|
849
|
+
static {
|
|
850
|
+
__name(this, "VolumeTransformer");
|
|
851
|
+
}
|
|
852
|
+
buffer = Buffer.allocUnsafe(0);
|
|
853
|
+
extrema = [-Math.pow(2, 16 - 1), Math.pow(2, 16 - 1) - 1];
|
|
854
|
+
vol = 1;
|
|
855
|
+
_transform(newChunk, _encoding, done) {
|
|
856
|
+
const { vol } = this;
|
|
857
|
+
if (vol === 1) {
|
|
858
|
+
this.push(newChunk);
|
|
859
|
+
done();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const bytes = 2;
|
|
863
|
+
const chunk = Buffer.concat([this.buffer, newChunk]);
|
|
864
|
+
const readableLength = Math.floor(chunk.length / bytes) * bytes;
|
|
865
|
+
for (let i = 0; i < readableLength; i += bytes) {
|
|
866
|
+
const value = chunk.readInt16LE(i);
|
|
867
|
+
const clampedValue = Math.min(this.extrema[1], Math.max(this.extrema[0], value * vol));
|
|
868
|
+
chunk.writeInt16LE(clampedValue, i);
|
|
869
|
+
}
|
|
870
|
+
this.buffer = chunk.subarray(readableLength);
|
|
871
|
+
this.push(chunk.subarray(0, readableLength));
|
|
872
|
+
done();
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
// src/core/DisTubeHandler.ts
|
|
877
|
+
import { request } from "undici";
|
|
878
|
+
var REDIRECT_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
879
|
+
var DisTubeHandler = class extends DisTubeBase {
|
|
880
|
+
static {
|
|
881
|
+
__name(this, "DisTubeHandler");
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Resolve a url or a supported object to a {@link Song} or {@link Playlist}
|
|
885
|
+
* @throws {@link DisTubeError}
|
|
886
|
+
* @param input - Resolvable input
|
|
887
|
+
* @param options - Optional options
|
|
888
|
+
* @returns Resolved
|
|
889
|
+
*/
|
|
890
|
+
async resolve(input, options = {}) {
|
|
891
|
+
if (input instanceof Song || input instanceof Playlist) {
|
|
892
|
+
if ("metadata" in options) input.metadata = options.metadata;
|
|
893
|
+
if ("member" in options) input.member = options.member;
|
|
894
|
+
return input;
|
|
895
|
+
}
|
|
896
|
+
if (typeof input === "string") {
|
|
897
|
+
if (isURL(input)) {
|
|
898
|
+
const plugin = await this._getPluginFromURL(input) || await this._getPluginFromURL(input = await this.followRedirectLink(input));
|
|
899
|
+
if (!plugin) throw new DisTubeError("NOT_SUPPORTED_URL");
|
|
900
|
+
this.debug(`[${plugin.constructor.name}] Resolving from url: ${input}`);
|
|
901
|
+
return plugin.resolve(input, options);
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
const song = await this.#searchSong(input, options);
|
|
905
|
+
if (song) return song;
|
|
906
|
+
} catch {
|
|
907
|
+
throw new DisTubeError("NO_RESULT", input);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
throw new DisTubeError("CANNOT_RESOLVE_SONG", input);
|
|
911
|
+
}
|
|
912
|
+
async _getPluginFromURL(url) {
|
|
913
|
+
for (const plugin of this.plugins) if (await plugin.validate(url)) return plugin;
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
async _getPluginFromSong(song, types, validate = true) {
|
|
917
|
+
if (!types || types.includes(song.plugin?.type)) return song.plugin;
|
|
918
|
+
if (!song.url) return null;
|
|
919
|
+
for (const plugin of this.plugins) {
|
|
920
|
+
if ((!types || types.includes(plugin?.type)) && (!validate || await plugin.validate(song.url))) {
|
|
921
|
+
return plugin;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
async #searchSong(query, options = {}, getStreamURL = false) {
|
|
927
|
+
const plugins = this.plugins.filter((p) => p.type === "extractor" /* EXTRACTOR */);
|
|
928
|
+
if (!plugins.length) throw new DisTubeError("NO_EXTRACTOR_PLUGIN");
|
|
929
|
+
for (const plugin of plugins) {
|
|
930
|
+
this.debug(`[${plugin.constructor.name}] Searching for song: ${query}`);
|
|
931
|
+
const result = await plugin.searchSong(query, options);
|
|
932
|
+
if (result) {
|
|
933
|
+
if (getStreamURL && result.stream.playFromSource) result.stream.url = await plugin.getStreamURL(result);
|
|
934
|
+
return result;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Get {@link Song}'s stream info and attach it to the song.
|
|
941
|
+
* @param song - A Song
|
|
942
|
+
*/
|
|
943
|
+
async attachStreamInfo(song) {
|
|
944
|
+
if (song.stream.playFromSource) {
|
|
945
|
+
if (song.stream.url) return;
|
|
946
|
+
this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
|
|
947
|
+
const plugin = await this._getPluginFromSong(song, ["extractor" /* EXTRACTOR */, "playable-extractor" /* PLAYABLE_EXTRACTOR */]);
|
|
948
|
+
if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
|
|
949
|
+
this.debug(`[${plugin.constructor.name}] Getting stream URL: ${song}`);
|
|
950
|
+
song.stream.url = await plugin.getStreamURL(song);
|
|
951
|
+
if (!song.stream.url) throw new DisTubeError("CANNOT_GET_STREAM_URL", song.toString());
|
|
952
|
+
} else {
|
|
953
|
+
if (song.stream.song?.stream?.playFromSource && song.stream.song.stream.url) return;
|
|
954
|
+
this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
|
|
955
|
+
const plugin = await this._getPluginFromSong(song, ["info-extractor" /* INFO_EXTRACTOR */]);
|
|
956
|
+
if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
|
|
957
|
+
this.debug(`[${plugin.constructor.name}] Creating search query for: ${song}`);
|
|
958
|
+
const query = await plugin.createSearchQuery(song);
|
|
959
|
+
if (!query) throw new DisTubeError("CANNOT_GET_SEARCH_QUERY", song.toString());
|
|
960
|
+
const altSong = await this.#searchSong(query, { metadata: song.metadata, member: song.member }, true);
|
|
961
|
+
if (!altSong || !altSong.stream.playFromSource) throw new DisTubeError("NO_RESULT", query || song.toString());
|
|
962
|
+
song.stream.song = altSong;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async followRedirectLink(url, maxRedirect = 5) {
|
|
966
|
+
if (maxRedirect === 0) return url;
|
|
967
|
+
const res = await request(url, {
|
|
968
|
+
method: "HEAD",
|
|
969
|
+
headers: {
|
|
970
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.3"
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
if (REDIRECT_CODES.has(res.statusCode ?? 200)) {
|
|
974
|
+
let location = res.headers.location;
|
|
975
|
+
if (typeof location !== "string") location = location?.[0] ?? url;
|
|
976
|
+
return this.followRedirectLink(location, --maxRedirect);
|
|
977
|
+
}
|
|
978
|
+
return url;
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
// src/core/DisTubeOptions.ts
|
|
983
|
+
var Options = class {
|
|
984
|
+
static {
|
|
985
|
+
__name(this, "Options");
|
|
986
|
+
}
|
|
987
|
+
plugins;
|
|
988
|
+
emitNewSongOnly;
|
|
989
|
+
savePreviousSongs;
|
|
990
|
+
customFilters;
|
|
991
|
+
nsfw;
|
|
992
|
+
emitAddSongWhenCreatingQueue;
|
|
993
|
+
emitAddListWhenCreatingQueue;
|
|
994
|
+
joinNewVoiceChannel;
|
|
995
|
+
ffmpeg;
|
|
996
|
+
constructor(options) {
|
|
997
|
+
if (typeof options !== "object" || Array.isArray(options)) {
|
|
998
|
+
throw new DisTubeError("INVALID_TYPE", "object", options, "DisTubeOptions");
|
|
999
|
+
}
|
|
1000
|
+
const opts = { ...defaultOptions, ...options };
|
|
1001
|
+
this.plugins = opts.plugins;
|
|
1002
|
+
this.emitNewSongOnly = opts.emitNewSongOnly;
|
|
1003
|
+
this.savePreviousSongs = opts.savePreviousSongs;
|
|
1004
|
+
this.customFilters = opts.customFilters;
|
|
1005
|
+
this.nsfw = opts.nsfw;
|
|
1006
|
+
this.emitAddSongWhenCreatingQueue = opts.emitAddSongWhenCreatingQueue;
|
|
1007
|
+
this.emitAddListWhenCreatingQueue = opts.emitAddListWhenCreatingQueue;
|
|
1008
|
+
this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
|
|
1009
|
+
this.ffmpeg = this.#ffmpegOption(options);
|
|
1010
|
+
checkInvalidKey(opts, this, "DisTubeOptions");
|
|
1011
|
+
this.#validateOptions();
|
|
1012
|
+
}
|
|
1013
|
+
#validateOptions(options = this) {
|
|
1014
|
+
const booleanOptions = /* @__PURE__ */ new Set([
|
|
1015
|
+
"emitNewSongOnly",
|
|
1016
|
+
"savePreviousSongs",
|
|
1017
|
+
"joinNewVoiceChannel",
|
|
1018
|
+
"nsfw",
|
|
1019
|
+
"emitAddSongWhenCreatingQueue",
|
|
1020
|
+
"emitAddListWhenCreatingQueue"
|
|
1021
|
+
]);
|
|
1022
|
+
const numberOptions = /* @__PURE__ */ new Set();
|
|
1023
|
+
const stringOptions = /* @__PURE__ */ new Set();
|
|
1024
|
+
const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
|
|
1025
|
+
const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
|
|
1026
|
+
for (const [key, value] of Object.entries(options)) {
|
|
1027
|
+
if (value === void 0 && optionalOptions.has(key)) continue;
|
|
1028
|
+
if (key === "plugins" && !Array.isArray(value)) {
|
|
1029
|
+
throw new DisTubeError("INVALID_TYPE", "Array<Plugin>", value, `DisTubeOptions.${key}`);
|
|
1030
|
+
} else if (booleanOptions.has(key)) {
|
|
1031
|
+
if (typeof value !== "boolean") {
|
|
1032
|
+
throw new DisTubeError("INVALID_TYPE", "boolean", value, `DisTubeOptions.${key}`);
|
|
1033
|
+
}
|
|
1034
|
+
} else if (numberOptions.has(key)) {
|
|
1035
|
+
if (typeof value !== "number" || isNaN(value)) {
|
|
1036
|
+
throw new DisTubeError("INVALID_TYPE", "number", value, `DisTubeOptions.${key}`);
|
|
1037
|
+
}
|
|
1038
|
+
} else if (stringOptions.has(key)) {
|
|
1039
|
+
if (typeof value !== "string") {
|
|
1040
|
+
throw new DisTubeError("INVALID_TYPE", "string", value, `DisTubeOptions.${key}`);
|
|
1041
|
+
}
|
|
1042
|
+
} else if (objectOptions.has(key)) {
|
|
1043
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1044
|
+
throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.${key}`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
#ffmpegOption(opts) {
|
|
1050
|
+
const args = { global: {}, input: {}, output: {} };
|
|
1051
|
+
if (opts.ffmpeg?.args) {
|
|
1052
|
+
if (opts.ffmpeg.args.global) args.global = opts.ffmpeg.args.global;
|
|
1053
|
+
if (opts.ffmpeg.args.input) args.input = opts.ffmpeg.args.input;
|
|
1054
|
+
if (opts.ffmpeg.args.output) args.output = opts.ffmpeg.args.output;
|
|
1055
|
+
}
|
|
1056
|
+
const path = opts.ffmpeg?.path ?? "ffmpeg";
|
|
1057
|
+
if (typeof path !== "string") {
|
|
1058
|
+
throw new DisTubeError("INVALID_TYPE", "string", path, "DisTubeOptions.ffmpeg.path");
|
|
1059
|
+
}
|
|
1060
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1061
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1062
|
+
throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.ffmpeg.${key}`);
|
|
1063
|
+
}
|
|
1064
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1065
|
+
if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean" && !Array.isArray(v) && v !== null && v !== void 0) {
|
|
1066
|
+
throw new DisTubeError(
|
|
1067
|
+
"INVALID_TYPE",
|
|
1068
|
+
["string", "number", "boolean", "Array<string | null | undefined>", "null", "undefined"],
|
|
1069
|
+
v,
|
|
1070
|
+
`DisTubeOptions.ffmpeg.${key}.${k}`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return { path, args };
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// src/core/manager/BaseManager.ts
|
|
1080
|
+
import { Collection } from "discord.js";
|
|
1081
|
+
var BaseManager = class extends DisTubeBase {
|
|
1082
|
+
static {
|
|
1083
|
+
__name(this, "BaseManager");
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* The collection of items for this manager.
|
|
1087
|
+
*/
|
|
1088
|
+
collection = new Collection();
|
|
1089
|
+
/**
|
|
1090
|
+
* The size of the collection.
|
|
1091
|
+
*/
|
|
1092
|
+
get size() {
|
|
1093
|
+
return this.collection.size;
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
// src/core/manager/GuildIdManager.ts
|
|
1098
|
+
var GuildIdManager = class extends BaseManager {
|
|
1099
|
+
static {
|
|
1100
|
+
__name(this, "GuildIdManager");
|
|
1101
|
+
}
|
|
1102
|
+
add(idOrInstance, data) {
|
|
1103
|
+
const id = resolveGuildId(idOrInstance);
|
|
1104
|
+
const existing = this.get(id);
|
|
1105
|
+
if (existing) return this;
|
|
1106
|
+
this.collection.set(id, data);
|
|
1107
|
+
return this;
|
|
1108
|
+
}
|
|
1109
|
+
get(idOrInstance) {
|
|
1110
|
+
return this.collection.get(resolveGuildId(idOrInstance));
|
|
1111
|
+
}
|
|
1112
|
+
remove(idOrInstance) {
|
|
1113
|
+
return this.collection.delete(resolveGuildId(idOrInstance));
|
|
1114
|
+
}
|
|
1115
|
+
has(idOrInstance) {
|
|
1116
|
+
return this.collection.has(resolveGuildId(idOrInstance));
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// src/core/manager/DisTubeVoiceManager.ts
|
|
1121
|
+
import { VoiceConnectionStatus as VoiceConnectionStatus2, getVoiceConnection } from "@discordjs/voice";
|
|
1122
|
+
var DisTubeVoiceManager = class extends GuildIdManager {
|
|
1123
|
+
static {
|
|
1124
|
+
__name(this, "DisTubeVoiceManager");
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Create a {@link DisTubeVoice} instance
|
|
1128
|
+
* @param channel - A voice channel to join
|
|
1129
|
+
*/
|
|
1130
|
+
create(channel) {
|
|
1131
|
+
const existing = this.get(channel.guildId);
|
|
1132
|
+
if (existing) {
|
|
1133
|
+
existing.channel = channel;
|
|
1134
|
+
return existing;
|
|
1135
|
+
}
|
|
1136
|
+
if (getVoiceConnection(resolveGuildId(channel), this.client.user?.id) || getVoiceConnection(resolveGuildId(channel))) {
|
|
1137
|
+
throw new DisTubeError("VOICE_ALREADY_CREATED");
|
|
1138
|
+
}
|
|
1139
|
+
return new DisTubeVoice(this, channel);
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Join a voice channel and wait until the connection is ready
|
|
1143
|
+
* @param channel - A voice channel to join
|
|
1144
|
+
*/
|
|
1145
|
+
join(channel) {
|
|
1146
|
+
const existing = this.get(channel.guildId);
|
|
1147
|
+
if (existing) return existing.join(channel);
|
|
1148
|
+
return this.create(channel).join();
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Leave the connected voice channel in a guild
|
|
1152
|
+
* @param guild - Queue Resolvable
|
|
1153
|
+
*/
|
|
1154
|
+
leave(guild) {
|
|
1155
|
+
const voice = this.get(guild);
|
|
1156
|
+
if (voice) {
|
|
1157
|
+
voice.leave();
|
|
1158
|
+
} else {
|
|
1159
|
+
const connection = getVoiceConnection(resolveGuildId(guild), this.client.user?.id) ?? getVoiceConnection(resolveGuildId(guild));
|
|
1160
|
+
if (connection && connection.state.status !== VoiceConnectionStatus2.Destroyed) {
|
|
1161
|
+
connection.destroy();
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
// src/core/manager/FilterManager.ts
|
|
1168
|
+
var FilterManager = class extends BaseManager {
|
|
1169
|
+
static {
|
|
1170
|
+
__name(this, "FilterManager");
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* The queue to manage
|
|
1174
|
+
*/
|
|
1175
|
+
queue;
|
|
1176
|
+
constructor(queue) {
|
|
1177
|
+
super(queue.distube);
|
|
1178
|
+
this.queue = queue;
|
|
1179
|
+
}
|
|
1180
|
+
#resolve(filter) {
|
|
1181
|
+
if (typeof filter === "object" && typeof filter.name === "string" && typeof filter.value === "string") {
|
|
1182
|
+
return filter;
|
|
1183
|
+
}
|
|
1184
|
+
if (typeof filter === "string" && Object.prototype.hasOwnProperty.call(this.distube.filters, filter)) {
|
|
1185
|
+
return {
|
|
1186
|
+
name: filter,
|
|
1187
|
+
value: this.distube.filters[filter]
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
|
|
1191
|
+
}
|
|
1192
|
+
#apply() {
|
|
1193
|
+
this.queue._beginTime = this.queue.currentTime;
|
|
1194
|
+
this.queue.play(false);
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Enable a filter or multiple filters to the manager
|
|
1198
|
+
* @param filterOrFilters - The filter or filters to enable
|
|
1199
|
+
* @param override - Wether or not override the applied filter with new filter value
|
|
1200
|
+
*/
|
|
1201
|
+
add(filterOrFilters, override = false) {
|
|
1202
|
+
if (Array.isArray(filterOrFilters)) {
|
|
1203
|
+
for (const filter of filterOrFilters) {
|
|
1204
|
+
const ft = this.#resolve(filter);
|
|
1205
|
+
if (override || !this.has(ft)) this.collection.set(ft.name, ft);
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
const ft = this.#resolve(filterOrFilters);
|
|
1209
|
+
if (override || !this.has(ft)) this.collection.set(ft.name, ft);
|
|
1210
|
+
}
|
|
1211
|
+
this.#apply();
|
|
1212
|
+
return this;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Clear enabled filters of the manager
|
|
1216
|
+
*/
|
|
1217
|
+
clear() {
|
|
1218
|
+
return this.set([]);
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Set the filters applied to the manager
|
|
1222
|
+
* @param filters - The filters to apply
|
|
1223
|
+
*/
|
|
1224
|
+
set(filters) {
|
|
1225
|
+
if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
|
|
1226
|
+
this.collection.clear();
|
|
1227
|
+
for (const f of filters) {
|
|
1228
|
+
const filter = this.#resolve(f);
|
|
1229
|
+
this.collection.set(filter.name, filter);
|
|
1230
|
+
}
|
|
1231
|
+
this.#apply();
|
|
1232
|
+
return this;
|
|
1233
|
+
}
|
|
1234
|
+
#removeFn(f) {
|
|
1235
|
+
return this.collection.delete(this.#resolve(f).name);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Disable a filter or multiple filters
|
|
1239
|
+
* @param filterOrFilters - The filter or filters to disable
|
|
1240
|
+
*/
|
|
1241
|
+
remove(filterOrFilters) {
|
|
1242
|
+
if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
|
|
1243
|
+
else this.#removeFn(filterOrFilters);
|
|
1244
|
+
this.#apply();
|
|
1245
|
+
return this;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Check whether a filter enabled or not
|
|
1249
|
+
* @param filter - The filter to check
|
|
1250
|
+
*/
|
|
1251
|
+
has(filter) {
|
|
1252
|
+
return this.collection.has(typeof filter === "string" ? filter : this.#resolve(filter).name);
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Array of enabled filter names
|
|
1256
|
+
*/
|
|
1257
|
+
get names() {
|
|
1258
|
+
return [...this.collection.keys()];
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Array of enabled filters
|
|
1262
|
+
*/
|
|
1263
|
+
get values() {
|
|
1264
|
+
return [...this.collection.values()];
|
|
1265
|
+
}
|
|
1266
|
+
get ffmpegArgs() {
|
|
1267
|
+
return this.size ? { af: this.values.map((f) => f.value).join(",") } : {};
|
|
1268
|
+
}
|
|
1269
|
+
toString() {
|
|
1270
|
+
return this.names.toString();
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
// src/core/manager/QueueManager.ts
|
|
1275
|
+
var QueueManager = class extends GuildIdManager {
|
|
1276
|
+
static {
|
|
1277
|
+
__name(this, "QueueManager");
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Create a {@link Queue}
|
|
1281
|
+
* @param channel - A voice channel
|
|
1282
|
+
* @param textChannel - Default text channel
|
|
1283
|
+
* @returns Returns `true` if encounter an error
|
|
1284
|
+
*/
|
|
1285
|
+
async create(channel, textChannel) {
|
|
1286
|
+
if (this.has(channel.guildId)) throw new DisTubeError("QUEUE_EXIST");
|
|
1287
|
+
this.debug(`[QueueManager] Creating queue for guild: ${channel.guildId}`);
|
|
1288
|
+
const voice = this.voices.create(channel);
|
|
1289
|
+
const queue = new Queue(this.distube, voice, textChannel);
|
|
1290
|
+
await queue._taskQueue.queuing();
|
|
1291
|
+
try {
|
|
1292
|
+
checkFFmpeg(this.distube);
|
|
1293
|
+
this.debug(`[QueueManager] Joining voice channel: ${channel.id}`);
|
|
1294
|
+
await voice.join();
|
|
1295
|
+
this.#voiceEventHandler(queue);
|
|
1296
|
+
this.add(queue.id, queue);
|
|
1297
|
+
this.emit("initQueue" /* INIT_QUEUE */, queue);
|
|
1298
|
+
return queue;
|
|
1299
|
+
} finally {
|
|
1300
|
+
queue._taskQueue.resolve();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Listen to DisTubeVoice events and handle the Queue
|
|
1305
|
+
* @param queue - Queue
|
|
1306
|
+
*/
|
|
1307
|
+
#voiceEventHandler(queue) {
|
|
1308
|
+
queue._listeners = {
|
|
1309
|
+
disconnect: /* @__PURE__ */ __name((error) => {
|
|
1310
|
+
queue.remove();
|
|
1311
|
+
this.emit("disconnect" /* DISCONNECT */, queue);
|
|
1312
|
+
if (error) this.emitError(error, queue, queue.songs?.[0]);
|
|
1313
|
+
}, "disconnect"),
|
|
1314
|
+
error: /* @__PURE__ */ __name((error) => this.#handlePlayingError(queue, error), "error"),
|
|
1315
|
+
finish: /* @__PURE__ */ __name(() => this.#handleSongFinish(queue), "finish")
|
|
1316
|
+
};
|
|
1317
|
+
for (const event of objectKeys(queue._listeners)) {
|
|
1318
|
+
queue.voice.on(event, queue._listeners[event]);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Whether or not emit playSong event
|
|
1323
|
+
* @param queue - Queue
|
|
1324
|
+
*/
|
|
1325
|
+
#emitPlaySong(queue) {
|
|
1326
|
+
if (!this.options.emitNewSongOnly) return true;
|
|
1327
|
+
if (queue.repeatMode === 1 /* SONG */) return queue._next || queue._prev;
|
|
1328
|
+
return queue.songs[0].id !== queue.songs[1].id;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Handle the queue when a Song finish
|
|
1332
|
+
* @param queue - queue
|
|
1333
|
+
*/
|
|
1334
|
+
async #handleSongFinish(queue) {
|
|
1335
|
+
this.debug(`[QueueManager] Handling song finish: ${queue.id}`);
|
|
1336
|
+
const song = queue.songs[0];
|
|
1337
|
+
this.emit("finishSong" /* FINISH_SONG */, queue, queue.songs[0]);
|
|
1338
|
+
await queue._taskQueue.queuing();
|
|
1339
|
+
try {
|
|
1340
|
+
if (queue.stopped) return;
|
|
1341
|
+
if (queue.repeatMode === 2 /* QUEUE */ && !queue._prev) queue.songs.push(song);
|
|
1342
|
+
if (queue._prev) {
|
|
1343
|
+
if (queue.repeatMode === 2 /* QUEUE */) queue.songs.unshift(queue.songs.pop());
|
|
1344
|
+
else queue.songs.unshift(queue.previousSongs.pop());
|
|
1345
|
+
}
|
|
1346
|
+
if (queue.songs.length <= 1 && (queue._next || queue.repeatMode === 0 /* DISABLED */)) {
|
|
1347
|
+
if (queue.autoplay) {
|
|
1348
|
+
try {
|
|
1349
|
+
this.debug(`[QueueManager] Adding related song: ${queue.id}`);
|
|
1350
|
+
await queue.addRelatedSong();
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
this.debug(`[${queue.id}] Add related song error: ${e.message}`);
|
|
1353
|
+
this.emit("noRelated" /* NO_RELATED */, queue, e);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (queue.songs.length <= 1) {
|
|
1357
|
+
this.debug(`[${queue.id}] Queue is empty, stopping...`);
|
|
1358
|
+
if (!queue.autoplay) this.emit("finish" /* FINISH */, queue);
|
|
1359
|
+
queue.remove();
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const emitPlaySong = this.#emitPlaySong(queue);
|
|
1364
|
+
if (!queue._prev && (queue.repeatMode !== 1 /* SONG */ || queue._next)) {
|
|
1365
|
+
const prev = queue.songs.shift();
|
|
1366
|
+
if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
|
|
1367
|
+
else queue.previousSongs.push({ id: prev.id });
|
|
1368
|
+
}
|
|
1369
|
+
queue._next = queue._prev = false;
|
|
1370
|
+
queue._beginTime = 0;
|
|
1371
|
+
if (song !== queue.songs[0]) {
|
|
1372
|
+
const playedSong = song.stream.playFromSource ? song : song.stream.song;
|
|
1373
|
+
if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
|
|
1374
|
+
}
|
|
1375
|
+
await this.playSong(queue, emitPlaySong);
|
|
1376
|
+
} finally {
|
|
1377
|
+
queue._taskQueue.resolve();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Handle error while playing
|
|
1382
|
+
* @param queue - queue
|
|
1383
|
+
* @param error - error
|
|
1384
|
+
*/
|
|
1385
|
+
#handlePlayingError(queue, error) {
|
|
1386
|
+
const song = queue.songs.shift();
|
|
1387
|
+
try {
|
|
1388
|
+
error.name = "PlayingError";
|
|
1389
|
+
} catch {
|
|
1390
|
+
}
|
|
1391
|
+
this.debug(`[${queue.id}] Error while playing: ${error.stack || error.message}`);
|
|
1392
|
+
this.emitError(error, queue, song);
|
|
1393
|
+
if (queue.songs.length > 0) {
|
|
1394
|
+
this.debug(`[${queue.id}] Playing next song: ${queue.songs[0]}`);
|
|
1395
|
+
queue._next = queue._prev = false;
|
|
1396
|
+
queue._beginTime = 0;
|
|
1397
|
+
this.playSong(queue);
|
|
1398
|
+
} else {
|
|
1399
|
+
this.debug(`[${queue.id}] Queue is empty, stopping...`);
|
|
1400
|
+
queue.stop();
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Play a song on voice connection with queue properties
|
|
1405
|
+
* @param queue - The guild queue to play
|
|
1406
|
+
* @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
|
|
1407
|
+
*/
|
|
1408
|
+
async playSong(queue, emitPlaySong = true) {
|
|
1409
|
+
if (!queue) return;
|
|
1410
|
+
if (queue.stopped || !queue.songs.length) {
|
|
1411
|
+
queue.stop();
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
try {
|
|
1415
|
+
const song = queue.songs[0];
|
|
1416
|
+
this.debug(`[${queue.id}] Getting stream from: ${song}`);
|
|
1417
|
+
await this.handler.attachStreamInfo(song);
|
|
1418
|
+
const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
|
|
1419
|
+
const stream = willPlaySong?.stream;
|
|
1420
|
+
if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
|
|
1421
|
+
this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
|
|
1422
|
+
const streamOptions = {
|
|
1423
|
+
ffmpeg: {
|
|
1424
|
+
path: this.options.ffmpeg.path,
|
|
1425
|
+
args: {
|
|
1426
|
+
global: { ...queue.ffmpegArgs.global },
|
|
1427
|
+
input: { ...queue.ffmpegArgs.input },
|
|
1428
|
+
output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
|
|
1429
|
+
}
|
|
1430
|
+
},
|
|
1431
|
+
seek: willPlaySong.duration ? queue._beginTime : void 0
|
|
1432
|
+
};
|
|
1433
|
+
const dtStream = new DisTubeStream(stream.url, streamOptions);
|
|
1434
|
+
dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
|
|
1435
|
+
this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
|
|
1436
|
+
queue.voice.play(dtStream);
|
|
1437
|
+
if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
this.#handlePlayingError(queue, e);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
// src/struct/Queue.ts
|
|
1445
|
+
var Queue = class extends DisTubeBase {
|
|
1446
|
+
static {
|
|
1447
|
+
__name(this, "Queue");
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Queue id (Guild id)
|
|
1451
|
+
*/
|
|
1452
|
+
id;
|
|
1453
|
+
/**
|
|
1454
|
+
* Voice connection of this queue.
|
|
1455
|
+
*/
|
|
1456
|
+
voice;
|
|
1457
|
+
/**
|
|
1458
|
+
* List of songs in the queue (The first one is the playing song)
|
|
1459
|
+
*/
|
|
1460
|
+
songs;
|
|
1461
|
+
/**
|
|
1462
|
+
* List of the previous songs.
|
|
1463
|
+
*/
|
|
1464
|
+
previousSongs;
|
|
1465
|
+
/**
|
|
1466
|
+
* Whether stream is currently stopped.
|
|
1467
|
+
*/
|
|
1468
|
+
stopped;
|
|
1469
|
+
/**
|
|
1470
|
+
* Whether or not the stream is currently playing.
|
|
1471
|
+
*/
|
|
1472
|
+
playing;
|
|
1473
|
+
/**
|
|
1474
|
+
* Whether or not the stream is currently paused.
|
|
1475
|
+
*/
|
|
1476
|
+
paused;
|
|
1477
|
+
/**
|
|
1478
|
+
* Type of repeat mode (`0` is disabled, `1` is repeating a song, `2` is repeating
|
|
1479
|
+
* all the queue). Default value: `0` (disabled)
|
|
1480
|
+
*/
|
|
1481
|
+
repeatMode;
|
|
1482
|
+
/**
|
|
1483
|
+
* Whether or not the autoplay mode is enabled. Default value: `false`
|
|
1484
|
+
*/
|
|
1485
|
+
autoplay;
|
|
1486
|
+
/**
|
|
1487
|
+
* FFmpeg arguments for the current queue. Default value is defined with {@link DisTubeOptions}.ffmpeg.args.
|
|
1488
|
+
* `af` output argument will be replaced with {@link Queue#filters} manager
|
|
1489
|
+
*/
|
|
1490
|
+
ffmpegArgs;
|
|
1491
|
+
/**
|
|
1492
|
+
* The text channel of the Queue. (Default: where the first command is called).
|
|
1493
|
+
*/
|
|
1494
|
+
textChannel;
|
|
1495
|
+
#filters;
|
|
1496
|
+
/**
|
|
1497
|
+
* What time in the song to begin (in seconds).
|
|
1498
|
+
*/
|
|
1499
|
+
_beginTime;
|
|
1500
|
+
/**
|
|
1501
|
+
* Whether or not the last song was skipped to next song.
|
|
1502
|
+
*/
|
|
1503
|
+
_next;
|
|
1504
|
+
/**
|
|
1505
|
+
* Whether or not the last song was skipped to previous song.
|
|
1506
|
+
*/
|
|
1507
|
+
_prev;
|
|
1508
|
+
/**
|
|
1509
|
+
* Task queuing system
|
|
1510
|
+
*/
|
|
1511
|
+
_taskQueue;
|
|
1512
|
+
/**
|
|
1513
|
+
* {@link DisTubeVoice} listener
|
|
1514
|
+
*/
|
|
1515
|
+
_listeners;
|
|
1516
|
+
/**
|
|
1517
|
+
* Create a queue for the guild
|
|
1518
|
+
* @param distube - DisTube
|
|
1519
|
+
* @param voice - Voice connection
|
|
1520
|
+
* @param textChannel - Default text channel
|
|
1521
|
+
*/
|
|
1522
|
+
constructor(distube, voice, textChannel) {
|
|
1523
|
+
super(distube);
|
|
1524
|
+
this.voice = voice;
|
|
1525
|
+
this.id = voice.id;
|
|
1526
|
+
this.volume = 50;
|
|
1527
|
+
this.songs = [];
|
|
1528
|
+
this.previousSongs = [];
|
|
1529
|
+
this.stopped = false;
|
|
1530
|
+
this._next = false;
|
|
1531
|
+
this._prev = false;
|
|
1532
|
+
this.playing = false;
|
|
1533
|
+
this.paused = false;
|
|
1534
|
+
this.repeatMode = 0 /* DISABLED */;
|
|
1535
|
+
this.autoplay = false;
|
|
1536
|
+
this.#filters = new FilterManager(this);
|
|
1537
|
+
this._beginTime = 0;
|
|
1538
|
+
this.textChannel = textChannel;
|
|
1539
|
+
this._taskQueue = new TaskQueue();
|
|
1540
|
+
this._listeners = void 0;
|
|
1541
|
+
this.ffmpegArgs = {
|
|
1542
|
+
global: { ...this.options.ffmpeg.args.global },
|
|
1543
|
+
input: { ...this.options.ffmpeg.args.input },
|
|
1544
|
+
output: { ...this.options.ffmpeg.args.output }
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* The client user as a `GuildMember` of this queue's guild
|
|
1549
|
+
*/
|
|
1550
|
+
get clientMember() {
|
|
1551
|
+
return this.voice.channel.guild.members.me ?? void 0;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* The filter manager of the queue
|
|
1555
|
+
*/
|
|
1556
|
+
get filters() {
|
|
1557
|
+
return this.#filters;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Formatted duration string.
|
|
1561
|
+
*/
|
|
1562
|
+
get formattedDuration() {
|
|
1563
|
+
return formatDuration(this.duration);
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Queue's duration.
|
|
1567
|
+
*/
|
|
1568
|
+
get duration() {
|
|
1569
|
+
return this.songs.length ? this.songs.reduce((prev, next) => prev + next.duration, 0) : 0;
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* What time in the song is playing (in seconds).
|
|
1573
|
+
*/
|
|
1574
|
+
get currentTime() {
|
|
1575
|
+
return this.voice.playbackDuration + this._beginTime;
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Formatted {@link Queue#currentTime} string.
|
|
1579
|
+
*/
|
|
1580
|
+
get formattedCurrentTime() {
|
|
1581
|
+
return formatDuration(this.currentTime);
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* The voice channel playing in.
|
|
1585
|
+
*/
|
|
1586
|
+
get voiceChannel() {
|
|
1587
|
+
return this.clientMember?.voice?.channel ?? null;
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Get or set the stream volume. Default value: `50`.
|
|
1591
|
+
*/
|
|
1592
|
+
get volume() {
|
|
1593
|
+
return this.voice.volume;
|
|
1594
|
+
}
|
|
1595
|
+
set volume(value) {
|
|
1596
|
+
this.voice.volume = value;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* @throws {DisTubeError}
|
|
1600
|
+
* @param song - Song to add
|
|
1601
|
+
* @param position - Position to add, \<= 0 to add to the end of the queue
|
|
1602
|
+
* @returns The guild queue
|
|
1603
|
+
*/
|
|
1604
|
+
addToQueue(song, position = 0) {
|
|
1605
|
+
if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
|
|
1606
|
+
if (!song || Array.isArray(song) && !song.length) {
|
|
1607
|
+
throw new DisTubeError("INVALID_TYPE", ["Song", "Array<Song>"], song, "song");
|
|
1608
|
+
}
|
|
1609
|
+
if (typeof position !== "number" || !Number.isInteger(position)) {
|
|
1610
|
+
throw new DisTubeError("INVALID_TYPE", "integer", position, "position");
|
|
1611
|
+
}
|
|
1612
|
+
if (position <= 0) {
|
|
1613
|
+
if (Array.isArray(song)) this.songs.push(...song);
|
|
1614
|
+
else this.songs.push(song);
|
|
1615
|
+
} else if (Array.isArray(song)) {
|
|
1616
|
+
this.songs.splice(position, 0, ...song);
|
|
1617
|
+
} else {
|
|
1618
|
+
this.songs.splice(position, 0, song);
|
|
1619
|
+
}
|
|
1620
|
+
return this;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* @returns `true` if the queue is playing
|
|
1624
|
+
*/
|
|
1625
|
+
isPlaying() {
|
|
1626
|
+
return this.playing;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* @returns `true` if the queue is paused
|
|
1630
|
+
*/
|
|
1631
|
+
isPaused() {
|
|
1632
|
+
return this.paused;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Pause the guild stream
|
|
1636
|
+
* @returns The guild queue
|
|
1637
|
+
*/
|
|
1638
|
+
pause() {
|
|
1639
|
+
if (this.paused) throw new DisTubeError("PAUSED");
|
|
1640
|
+
this.paused = true;
|
|
1641
|
+
this.voice.pause();
|
|
1642
|
+
return this;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Resume the guild stream
|
|
1646
|
+
* @returns The guild queue
|
|
1647
|
+
*/
|
|
1648
|
+
resume() {
|
|
1649
|
+
if (!this.paused) throw new DisTubeError("RESUMED");
|
|
1650
|
+
this.paused = false;
|
|
1651
|
+
this.voice.unpause();
|
|
1652
|
+
return this;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Set the guild stream's volume
|
|
1656
|
+
* @param percent - The percentage of volume you want to set
|
|
1657
|
+
* @returns The guild queue
|
|
1658
|
+
*/
|
|
1659
|
+
setVolume(percent) {
|
|
1660
|
+
this.volume = percent;
|
|
1661
|
+
return this;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Skip the playing song if there is a next song in the queue. <info>If {@link
|
|
1665
|
+
* Queue#autoplay} is `true` and there is no up next song, DisTube will add and
|
|
1666
|
+
* play a related song.</info>
|
|
1667
|
+
* @returns The song will skip to
|
|
1668
|
+
*/
|
|
1669
|
+
async skip() {
|
|
1670
|
+
await this._taskQueue.queuing();
|
|
1671
|
+
try {
|
|
1672
|
+
if (this.songs.length <= 1) {
|
|
1673
|
+
if (this.autoplay) await this.addRelatedSong();
|
|
1674
|
+
else throw new DisTubeError("NO_UP_NEXT");
|
|
1675
|
+
}
|
|
1676
|
+
const song = this.songs[1];
|
|
1677
|
+
this._next = true;
|
|
1678
|
+
this.voice.stop();
|
|
1679
|
+
return song;
|
|
1680
|
+
} finally {
|
|
1681
|
+
this._taskQueue.resolve();
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Play the previous song if exists
|
|
1686
|
+
* @returns The guild queue
|
|
1687
|
+
*/
|
|
1688
|
+
async previous() {
|
|
1689
|
+
await this._taskQueue.queuing();
|
|
1690
|
+
try {
|
|
1691
|
+
if (!this.options.savePreviousSongs) throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
|
|
1692
|
+
if (this.previousSongs?.length === 0 && this.repeatMode !== 2 /* QUEUE */) {
|
|
1693
|
+
throw new DisTubeError("NO_PREVIOUS");
|
|
1694
|
+
}
|
|
1695
|
+
const song = this.repeatMode === 2 ? this.songs[this.songs.length - 1] : this.previousSongs[this.previousSongs.length - 1];
|
|
1696
|
+
this._prev = true;
|
|
1697
|
+
this.voice.stop();
|
|
1698
|
+
return song;
|
|
1699
|
+
} finally {
|
|
1700
|
+
this._taskQueue.resolve();
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Shuffle the queue's songs
|
|
1705
|
+
* @returns The guild queue
|
|
1706
|
+
*/
|
|
1707
|
+
async shuffle() {
|
|
1708
|
+
await this._taskQueue.queuing();
|
|
1709
|
+
try {
|
|
1710
|
+
const playing = this.songs.shift();
|
|
1711
|
+
if (playing === void 0) return this;
|
|
1712
|
+
for (let i = this.songs.length - 1; i > 0; i--) {
|
|
1713
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1714
|
+
[this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
|
|
1715
|
+
}
|
|
1716
|
+
this.songs.unshift(playing);
|
|
1717
|
+
return this;
|
|
1718
|
+
} finally {
|
|
1719
|
+
this._taskQueue.resolve();
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Jump to the song position in the queue. The next one is 1, 2,... The previous
|
|
1724
|
+
* one is -1, -2,...
|
|
1725
|
+
* if `num` is invalid number
|
|
1726
|
+
* @param position - The song position to play
|
|
1727
|
+
* @returns The new Song will be played
|
|
1728
|
+
*/
|
|
1729
|
+
async jump(position) {
|
|
1730
|
+
await this._taskQueue.queuing();
|
|
1731
|
+
try {
|
|
1732
|
+
if (typeof position !== "number") throw new DisTubeError("INVALID_TYPE", "number", position, "position");
|
|
1733
|
+
if (!position || position > this.songs.length || -position > this.previousSongs.length) {
|
|
1734
|
+
throw new DisTubeError("NO_SONG_POSITION");
|
|
1735
|
+
}
|
|
1736
|
+
let nextSong;
|
|
1737
|
+
if (position > 0) {
|
|
1738
|
+
const nextSongs = this.songs.splice(position - 1);
|
|
1739
|
+
if (this.options.savePreviousSongs) {
|
|
1740
|
+
this.previousSongs.push(...this.songs);
|
|
1741
|
+
} else {
|
|
1742
|
+
this.previousSongs.push(...this.songs.map((s) => ({ id: s.id })));
|
|
1743
|
+
}
|
|
1744
|
+
this.songs = nextSongs;
|
|
1745
|
+
this._next = true;
|
|
1746
|
+
nextSong = nextSongs[1];
|
|
1747
|
+
} else if (!this.options.savePreviousSongs) {
|
|
1748
|
+
throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
|
|
1749
|
+
} else {
|
|
1750
|
+
this._prev = true;
|
|
1751
|
+
if (position !== -1) this.songs.unshift(...this.previousSongs.splice(position + 1));
|
|
1752
|
+
nextSong = this.previousSongs[this.previousSongs.length - 1];
|
|
1753
|
+
}
|
|
1754
|
+
this.voice.stop();
|
|
1755
|
+
return nextSong;
|
|
1756
|
+
} finally {
|
|
1757
|
+
this._taskQueue.resolve();
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Set the repeat mode of the guild queue.
|
|
1762
|
+
* Toggle mode `(Disabled -> Song -> Queue -> Disabled ->...)` if `mode` is `undefined`
|
|
1763
|
+
* @param mode - The repeat modes (toggle if `undefined`)
|
|
1764
|
+
* @returns The new repeat mode
|
|
1765
|
+
*/
|
|
1766
|
+
setRepeatMode(mode) {
|
|
1767
|
+
if (mode !== void 0 && !Object.values(RepeatMode).includes(mode)) {
|
|
1768
|
+
throw new DisTubeError("INVALID_TYPE", ["RepeatMode", "undefined"], mode, "mode");
|
|
1769
|
+
}
|
|
1770
|
+
if (mode === void 0) this.repeatMode = (this.repeatMode + 1) % 3;
|
|
1771
|
+
else if (this.repeatMode === mode) this.repeatMode = 0 /* DISABLED */;
|
|
1772
|
+
else this.repeatMode = mode;
|
|
1773
|
+
return this.repeatMode;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Set the playing time to another position
|
|
1777
|
+
* @param time - Time in seconds
|
|
1778
|
+
* @returns The guild queue
|
|
1779
|
+
*/
|
|
1780
|
+
seek(time) {
|
|
1781
|
+
if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
|
|
1782
|
+
if (isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
|
|
1783
|
+
this._beginTime = time;
|
|
1784
|
+
this.play(false);
|
|
1785
|
+
return this;
|
|
1786
|
+
}
|
|
1787
|
+
async #getRelatedSong(current) {
|
|
1788
|
+
const plugin = await this.handler._getPluginFromSong(current);
|
|
1789
|
+
if (plugin) return plugin.getRelatedSongs(current);
|
|
1790
|
+
return [];
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Add a related song of the playing song to the queue
|
|
1794
|
+
* @returns The added song
|
|
1795
|
+
*/
|
|
1796
|
+
async addRelatedSong() {
|
|
1797
|
+
const current = this.songs?.[0];
|
|
1798
|
+
if (!current) throw new DisTubeError("NO_PLAYING_SONG");
|
|
1799
|
+
const prevIds = this.previousSongs.map((p) => p.id);
|
|
1800
|
+
const relatedSongs = (await this.#getRelatedSong(current)).filter((s) => !prevIds.includes(s.id));
|
|
1801
|
+
this.debug(`[${this.id}] Getting related songs from: ${current}`);
|
|
1802
|
+
if (!relatedSongs.length && !current.stream.playFromSource) {
|
|
1803
|
+
const altSong = current.stream.song;
|
|
1804
|
+
if (altSong) relatedSongs.push(...(await this.#getRelatedSong(altSong)).filter((s) => !prevIds.includes(s.id)));
|
|
1805
|
+
this.debug(`[${this.id}] Getting related songs from streamed song: ${altSong}`);
|
|
1806
|
+
}
|
|
1807
|
+
const song = relatedSongs[0];
|
|
1808
|
+
if (!song) throw new DisTubeError("NO_RELATED");
|
|
1809
|
+
song.metadata = current.metadata;
|
|
1810
|
+
song.member = this.clientMember;
|
|
1811
|
+
this.addToQueue(song);
|
|
1812
|
+
return song;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Stop the guild stream and delete the queue
|
|
1816
|
+
*/
|
|
1817
|
+
async stop() {
|
|
1818
|
+
await this._taskQueue.queuing();
|
|
1819
|
+
try {
|
|
1820
|
+
this.playing = false;
|
|
1821
|
+
this.paused = false;
|
|
1822
|
+
this.stopped = true;
|
|
1823
|
+
this.voice.stop();
|
|
1824
|
+
this.remove();
|
|
1825
|
+
} finally {
|
|
1826
|
+
this._taskQueue.resolve();
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Remove the queue from the manager
|
|
1831
|
+
*/
|
|
1832
|
+
remove() {
|
|
1833
|
+
this.stopped = true;
|
|
1834
|
+
this.songs = [];
|
|
1835
|
+
this.previousSongs = [];
|
|
1836
|
+
if (this._listeners) {
|
|
1837
|
+
for (const event of objectKeys(this._listeners)) {
|
|
1838
|
+
this.voice.off(event, this._listeners[event]);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
this.queues.remove(this.id);
|
|
1842
|
+
this.emit("deleteQueue" /* DELETE_QUEUE */, this);
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Toggle autoplay mode
|
|
1846
|
+
* @returns Autoplay mode state
|
|
1847
|
+
*/
|
|
1848
|
+
toggleAutoplay() {
|
|
1849
|
+
this.autoplay = !this.autoplay;
|
|
1850
|
+
return this.autoplay;
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Play the first song in the queue
|
|
1854
|
+
* @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
|
|
1855
|
+
*/
|
|
1856
|
+
play(emitPlaySong = true) {
|
|
1857
|
+
if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
|
|
1858
|
+
this.playing = true;
|
|
1859
|
+
return this.queues.playSong(this, emitPlaySong);
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
// src/struct/Plugin.ts
|
|
1864
|
+
var Plugin = class {
|
|
1865
|
+
static {
|
|
1866
|
+
__name(this, "Plugin");
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* DisTube
|
|
1870
|
+
*/
|
|
1871
|
+
distube;
|
|
1872
|
+
init(distube) {
|
|
1873
|
+
this.distube = distube;
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
// src/struct/ExtractorPlugin.ts
|
|
1878
|
+
var ExtractorPlugin = class extends Plugin {
|
|
1879
|
+
static {
|
|
1880
|
+
__name(this, "ExtractorPlugin");
|
|
1881
|
+
}
|
|
1882
|
+
type = "extractor" /* EXTRACTOR */;
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1885
|
+
// src/struct/InfoExtratorPlugin.ts
|
|
1886
|
+
var InfoExtractorPlugin = class extends Plugin {
|
|
1887
|
+
static {
|
|
1888
|
+
__name(this, "InfoExtractorPlugin");
|
|
1889
|
+
}
|
|
1890
|
+
type = "info-extractor" /* INFO_EXTRACTOR */;
|
|
1891
|
+
};
|
|
1892
|
+
|
|
1893
|
+
// src/struct/PlayableExtratorPlugin.ts
|
|
1894
|
+
var PlayableExtractorPlugin = class extends Plugin {
|
|
1895
|
+
static {
|
|
1896
|
+
__name(this, "PlayableExtractorPlugin");
|
|
1897
|
+
}
|
|
1898
|
+
type = "playable-extractor" /* PLAYABLE_EXTRACTOR */;
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1901
|
+
// src/util.ts
|
|
1902
|
+
import { URL as URL2 } from "url";
|
|
1903
|
+
import { Constants as Constants2, GatewayIntentBits, IntentsBitField, SnowflakeUtil } from "discord.js";
|
|
1904
|
+
var formatInt = /* @__PURE__ */ __name((int) => int < 10 ? `0${int}` : int, "formatInt");
|
|
1905
|
+
function formatDuration(sec) {
|
|
1906
|
+
if (!sec || !Number(sec)) return "00:00";
|
|
1907
|
+
const seconds = Math.floor(sec % 60);
|
|
1908
|
+
const minutes = Math.floor(sec % 3600 / 60);
|
|
1909
|
+
const hours = Math.floor(sec / 3600);
|
|
1910
|
+
if (hours > 0) return `${formatInt(hours)}:${formatInt(minutes)}:${formatInt(seconds)}`;
|
|
1911
|
+
if (minutes > 0) return `${formatInt(minutes)}:${formatInt(seconds)}`;
|
|
1912
|
+
return `00:${formatInt(seconds)}`;
|
|
1913
|
+
}
|
|
1914
|
+
__name(formatDuration, "formatDuration");
|
|
1915
|
+
var SUPPORTED_PROTOCOL = ["https:", "http:", "file:"];
|
|
1916
|
+
function isURL(input) {
|
|
1917
|
+
if (typeof input !== "string" || input.includes(" ")) return false;
|
|
1918
|
+
try {
|
|
1919
|
+
const url = new URL2(input);
|
|
1920
|
+
if (!SUPPORTED_PROTOCOL.some((p) => p === url.protocol)) return false;
|
|
1921
|
+
} catch {
|
|
1922
|
+
return false;
|
|
1923
|
+
}
|
|
1924
|
+
return true;
|
|
1925
|
+
}
|
|
1926
|
+
__name(isURL, "isURL");
|
|
1927
|
+
function checkIntents(options) {
|
|
1928
|
+
const intents = options.intents instanceof IntentsBitField ? options.intents : new IntentsBitField(options.intents);
|
|
1929
|
+
if (!intents.has(GatewayIntentBits.GuildVoiceStates)) throw new DisTubeError("MISSING_INTENTS", "GuildVoiceStates");
|
|
1930
|
+
}
|
|
1931
|
+
__name(checkIntents, "checkIntents");
|
|
1932
|
+
function isVoiceChannelEmpty(voiceState) {
|
|
1933
|
+
const guild = voiceState.guild;
|
|
1934
|
+
const clientId = voiceState.client.user?.id;
|
|
1935
|
+
if (!guild || !clientId) return false;
|
|
1936
|
+
const voiceChannel = guild.members.me?.voice?.channel;
|
|
1937
|
+
if (!voiceChannel) return false;
|
|
1938
|
+
const members = voiceChannel.members.filter((m) => !m.user.bot);
|
|
1939
|
+
return !members.size;
|
|
1940
|
+
}
|
|
1941
|
+
__name(isVoiceChannelEmpty, "isVoiceChannelEmpty");
|
|
1942
|
+
function isSnowflake(id) {
|
|
1943
|
+
try {
|
|
1944
|
+
return SnowflakeUtil.deconstruct(id).timestamp > SnowflakeUtil.epoch;
|
|
1945
|
+
} catch {
|
|
1946
|
+
return false;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
__name(isSnowflake, "isSnowflake");
|
|
1950
|
+
function isMemberInstance(member) {
|
|
1951
|
+
return Boolean(member) && isSnowflake(member.id) && isSnowflake(member.guild?.id) && isSnowflake(member.user?.id) && member.id === member.user.id;
|
|
1952
|
+
}
|
|
1953
|
+
__name(isMemberInstance, "isMemberInstance");
|
|
1954
|
+
function isTextChannelInstance(channel) {
|
|
1955
|
+
return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && Constants2.TextBasedChannelTypes.includes(channel.type) && typeof channel.send === "function" && (typeof channel.nsfw === "boolean" || typeof channel.parent?.nsfw === "boolean");
|
|
1956
|
+
}
|
|
1957
|
+
__name(isTextChannelInstance, "isTextChannelInstance");
|
|
1958
|
+
function isMessageInstance(message) {
|
|
1959
|
+
return Boolean(message) && isSnowflake(message.id) && isSnowflake(message.guildId || message.guild?.id) && isMemberInstance(message.member) && isTextChannelInstance(message.channel) && Constants2.NonSystemMessageTypes.includes(message.type) && message.member.id === message.author?.id;
|
|
1960
|
+
}
|
|
1961
|
+
__name(isMessageInstance, "isMessageInstance");
|
|
1962
|
+
function isSupportedVoiceChannel(channel) {
|
|
1963
|
+
return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && Constants2.VoiceBasedChannelTypes.includes(channel.type);
|
|
1964
|
+
}
|
|
1965
|
+
__name(isSupportedVoiceChannel, "isSupportedVoiceChannel");
|
|
1966
|
+
function isGuildInstance(guild) {
|
|
1967
|
+
return Boolean(guild) && isSnowflake(guild.id) && isSnowflake(guild.ownerId) && typeof guild.name === "string";
|
|
1968
|
+
}
|
|
1969
|
+
__name(isGuildInstance, "isGuildInstance");
|
|
1970
|
+
function resolveGuildId(resolvable) {
|
|
1971
|
+
let guildId;
|
|
1972
|
+
if (typeof resolvable === "string") {
|
|
1973
|
+
guildId = resolvable;
|
|
1974
|
+
} else if (isObject(resolvable)) {
|
|
1975
|
+
if ("guildId" in resolvable && resolvable.guildId) {
|
|
1976
|
+
guildId = resolvable.guildId;
|
|
1977
|
+
} else if (resolvable instanceof Queue || resolvable instanceof DisTubeVoice || isGuildInstance(resolvable)) {
|
|
1978
|
+
guildId = resolvable.id;
|
|
1979
|
+
} else if ("guild" in resolvable && isGuildInstance(resolvable.guild)) {
|
|
1980
|
+
guildId = resolvable.guild.id;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
if (!isSnowflake(guildId)) throw new DisTubeError("INVALID_TYPE", "GuildIdResolvable", resolvable);
|
|
1984
|
+
return guildId;
|
|
1985
|
+
}
|
|
1986
|
+
__name(resolveGuildId, "resolveGuildId");
|
|
1987
|
+
function isClientInstance(client) {
|
|
1988
|
+
return Boolean(client) && typeof client.login === "function";
|
|
1989
|
+
}
|
|
1990
|
+
__name(isClientInstance, "isClientInstance");
|
|
1991
|
+
function checkInvalidKey(target, source, sourceName) {
|
|
1992
|
+
if (!isObject(target)) throw new DisTubeError("INVALID_TYPE", "object", target, sourceName);
|
|
1993
|
+
const sourceKeys = Array.isArray(source) ? source : objectKeys(source);
|
|
1994
|
+
const invalidKey = objectKeys(target).find((key) => !sourceKeys.includes(key));
|
|
1995
|
+
if (invalidKey) throw new DisTubeError("INVALID_KEY", sourceName, invalidKey);
|
|
1996
|
+
}
|
|
1997
|
+
__name(checkInvalidKey, "checkInvalidKey");
|
|
1998
|
+
function isObject(obj) {
|
|
1999
|
+
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
|
|
2000
|
+
}
|
|
2001
|
+
__name(isObject, "isObject");
|
|
2002
|
+
function objectKeys(obj) {
|
|
2003
|
+
if (!isObject(obj)) return [];
|
|
2004
|
+
return Object.keys(obj);
|
|
2005
|
+
}
|
|
2006
|
+
__name(objectKeys, "objectKeys");
|
|
2007
|
+
function isNsfwChannel(channel) {
|
|
2008
|
+
if (!isTextChannelInstance(channel)) return false;
|
|
2009
|
+
if (channel.isThread()) return channel.parent?.nsfw ?? false;
|
|
2010
|
+
return channel.nsfw;
|
|
2011
|
+
}
|
|
2012
|
+
__name(isNsfwChannel, "isNsfwChannel");
|
|
2013
|
+
var isTruthy = /* @__PURE__ */ __name((x) => Boolean(x), "isTruthy");
|
|
2014
|
+
var checkEncryptionLibraries = /* @__PURE__ */ __name(() => {
|
|
2015
|
+
for (const lib of ["sodium-native", "sodium", "libsodium-wrappers", "tweetnacl"]) {
|
|
2016
|
+
try {
|
|
2017
|
+
__require(lib);
|
|
2018
|
+
return true;
|
|
2019
|
+
} catch {
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return false;
|
|
2023
|
+
}, "checkEncryptionLibraries");
|
|
2024
|
+
|
|
2025
|
+
// src/DisTube.ts
|
|
2026
|
+
import { TypedEmitter as TypedEmitter3 } from "tiny-typed-emitter";
|
|
2027
|
+
var DisTube = class extends TypedEmitter3 {
|
|
2028
|
+
static {
|
|
2029
|
+
__name(this, "DisTube");
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* @event
|
|
2033
|
+
* Emitted after DisTube add a new playlist to the playing {@link Queue}.
|
|
2034
|
+
* @param queue - The guild queue
|
|
2035
|
+
* @param playlist - Playlist info
|
|
2036
|
+
*/
|
|
2037
|
+
static ["addList" /* ADD_LIST */];
|
|
2038
|
+
/**
|
|
2039
|
+
* @event
|
|
2040
|
+
* Emitted after DisTube add a new song to the playing {@link Queue}.
|
|
2041
|
+
* @param queue - The guild queue
|
|
2042
|
+
* @param song - Added song
|
|
2043
|
+
*/
|
|
2044
|
+
static ["addSong" /* ADD_SONG */];
|
|
2045
|
+
/**
|
|
2046
|
+
* @event
|
|
2047
|
+
* Emitted when a {@link Queue} is deleted with any reasons.
|
|
2048
|
+
* @param queue - The guild queue
|
|
2049
|
+
*/
|
|
2050
|
+
static ["deleteQueue" /* DELETE_QUEUE */];
|
|
2051
|
+
/**
|
|
2052
|
+
* @event
|
|
2053
|
+
* Emitted when the bot is disconnected to a voice channel.
|
|
2054
|
+
* @param queue - The guild queue
|
|
2055
|
+
*/
|
|
2056
|
+
static ["disconnect" /* DISCONNECT */];
|
|
2057
|
+
/**
|
|
2058
|
+
* @event
|
|
2059
|
+
* Emitted when DisTube encounters an error while playing songs.
|
|
2060
|
+
* @param error - error
|
|
2061
|
+
* @param queue - The queue encountered the error
|
|
2062
|
+
* @param song - The playing song when encountered the error
|
|
2063
|
+
*/
|
|
2064
|
+
static ["error" /* ERROR */];
|
|
2065
|
+
/**
|
|
2066
|
+
* @event
|
|
2067
|
+
* Emitted for logging FFmpeg debug information.
|
|
2068
|
+
* @param debug - Debug message string.
|
|
2069
|
+
*/
|
|
2070
|
+
static ["ffmpegDebug" /* FFMPEG_DEBUG */];
|
|
2071
|
+
/**
|
|
2072
|
+
* @event
|
|
2073
|
+
* Emitted to provide debug information from DisTube's operation.
|
|
2074
|
+
* Useful for troubleshooting or logging purposes.
|
|
2075
|
+
*
|
|
2076
|
+
* @param debug - Debug message string.
|
|
2077
|
+
*/
|
|
2078
|
+
static ["debug" /* DEBUG */];
|
|
2079
|
+
/**
|
|
2080
|
+
* @event
|
|
2081
|
+
* Emitted when there is no more song in the queue and {@link Queue#autoplay} is `false`.
|
|
2082
|
+
* @param queue - The guild queue
|
|
2083
|
+
*/
|
|
2084
|
+
static ["finish" /* FINISH */];
|
|
2085
|
+
/**
|
|
2086
|
+
* @event
|
|
2087
|
+
* Emitted when DisTube finished a song.
|
|
2088
|
+
* @param queue - The guild queue
|
|
2089
|
+
* @param song - Finished song
|
|
2090
|
+
*/
|
|
2091
|
+
static ["finishSong" /* FINISH_SONG */];
|
|
2092
|
+
/**
|
|
2093
|
+
* @event
|
|
2094
|
+
* Emitted when DisTube initialize a queue to change queue default properties.
|
|
2095
|
+
* @param queue - The guild queue
|
|
2096
|
+
*/
|
|
2097
|
+
static ["initQueue" /* INIT_QUEUE */];
|
|
2098
|
+
/**
|
|
2099
|
+
* @event
|
|
2100
|
+
* Emitted when {@link Queue#autoplay} is `true`, {@link Queue#songs} is empty, and
|
|
2101
|
+
* DisTube cannot find related songs to play.
|
|
2102
|
+
* @param queue - The guild queue
|
|
2103
|
+
*/
|
|
2104
|
+
static ["noRelated" /* NO_RELATED */];
|
|
2105
|
+
/**
|
|
2106
|
+
* @event
|
|
2107
|
+
* Emitted when DisTube play a song.
|
|
2108
|
+
* If {@link DisTubeOptions}.emitNewSongOnly is `true`, this event is not emitted
|
|
2109
|
+
* when looping a song or next song is the previous one.
|
|
2110
|
+
* @param queue - The guild queue
|
|
2111
|
+
* @param song - Playing song
|
|
2112
|
+
*/
|
|
2113
|
+
static ["playSong" /* PLAY_SONG */];
|
|
2114
|
+
/**
|
|
2115
|
+
* DisTube internal handler
|
|
2116
|
+
*/
|
|
2117
|
+
handler;
|
|
2118
|
+
/**
|
|
2119
|
+
* DisTube options
|
|
2120
|
+
*/
|
|
2121
|
+
options;
|
|
2122
|
+
/**
|
|
2123
|
+
* Discord.js v14 client
|
|
2124
|
+
*/
|
|
2125
|
+
client;
|
|
2126
|
+
/**
|
|
2127
|
+
* Queues manager
|
|
2128
|
+
*/
|
|
2129
|
+
queues;
|
|
2130
|
+
/**
|
|
2131
|
+
* DisTube voice connections manager
|
|
2132
|
+
*/
|
|
2133
|
+
voices;
|
|
2134
|
+
/**
|
|
2135
|
+
* DisTube plugins
|
|
2136
|
+
*/
|
|
2137
|
+
plugins;
|
|
2138
|
+
/**
|
|
2139
|
+
* DisTube ffmpeg audio filters
|
|
2140
|
+
*/
|
|
2141
|
+
filters;
|
|
2142
|
+
/**
|
|
2143
|
+
* Create a new DisTube class.
|
|
2144
|
+
* @throws {@link DisTubeError}
|
|
2145
|
+
* @param client - Discord.JS client
|
|
2146
|
+
* @param opts - Custom DisTube options
|
|
2147
|
+
*/
|
|
2148
|
+
constructor(client, opts = {}) {
|
|
2149
|
+
super();
|
|
2150
|
+
this.setMaxListeners(1);
|
|
2151
|
+
if (!isClientInstance(client)) throw new DisTubeError("INVALID_TYPE", "Discord.Client", client, "client");
|
|
2152
|
+
this.client = client;
|
|
2153
|
+
checkIntents(client.options);
|
|
2154
|
+
this.options = new Options(opts);
|
|
2155
|
+
this.voices = new DisTubeVoiceManager(this);
|
|
2156
|
+
this.handler = new DisTubeHandler(this);
|
|
2157
|
+
this.queues = new QueueManager(this);
|
|
2158
|
+
this.filters = { ...defaultFilters, ...this.options.customFilters };
|
|
2159
|
+
this.plugins = [...this.options.plugins];
|
|
2160
|
+
this.plugins.forEach((p) => p.init(this));
|
|
2161
|
+
}
|
|
2162
|
+
static get version() {
|
|
2163
|
+
return version;
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* DisTube version
|
|
2167
|
+
*/
|
|
2168
|
+
get version() {
|
|
2169
|
+
return version;
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Play / add a song or playlist from url.
|
|
2173
|
+
* Search and play a song (with {@link ExtractorPlugin}) if it is not a valid url.
|
|
2174
|
+
* @throws {@link DisTubeError}
|
|
2175
|
+
* @param voiceChannel - The channel will be joined if the bot isn't in any channels, the bot will be
|
|
2176
|
+
* moved to this channel if {@link DisTubeOptions}.joinNewVoiceChannel is `true`
|
|
2177
|
+
* @param song - URL | Search string | {@link Song} | {@link Playlist}
|
|
2178
|
+
* @param options - Optional options
|
|
2179
|
+
*/
|
|
2180
|
+
async play(voiceChannel, song, options = {}) {
|
|
2181
|
+
if (!isSupportedVoiceChannel(voiceChannel)) {
|
|
2182
|
+
throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", voiceChannel, "voiceChannel");
|
|
2183
|
+
}
|
|
2184
|
+
if (!isObject(options)) throw new DisTubeError("INVALID_TYPE", "object", options, "options");
|
|
2185
|
+
const { textChannel, member, skip, message, metadata } = {
|
|
2186
|
+
member: voiceChannel.guild.members.me ?? void 0,
|
|
2187
|
+
textChannel: options?.message?.channel,
|
|
2188
|
+
skip: false,
|
|
2189
|
+
...options
|
|
2190
|
+
};
|
|
2191
|
+
const position = Number(options.position) || (skip ? 1 : 0);
|
|
2192
|
+
if (message && !isMessageInstance(message)) {
|
|
2193
|
+
throw new DisTubeError("INVALID_TYPE", ["Discord.Message", "a falsy value"], message, "options.message");
|
|
2194
|
+
}
|
|
2195
|
+
if (textChannel && !isTextChannelInstance(textChannel)) {
|
|
2196
|
+
throw new DisTubeError("INVALID_TYPE", "Discord.GuildTextBasedChannel", textChannel, "options.textChannel");
|
|
2197
|
+
}
|
|
2198
|
+
if (member && !isMemberInstance(member)) {
|
|
2199
|
+
throw new DisTubeError("INVALID_TYPE", "Discord.GuildMember", member, "options.member");
|
|
2200
|
+
}
|
|
2201
|
+
const queue = this.getQueue(voiceChannel) || await this.queues.create(voiceChannel, textChannel);
|
|
2202
|
+
await queue._taskQueue.queuing();
|
|
2203
|
+
try {
|
|
2204
|
+
this.debug(`[${queue.id}] Playing input: ${song}`);
|
|
2205
|
+
const resolved = await this.handler.resolve(song, { member, metadata });
|
|
2206
|
+
const isNsfw = isNsfwChannel(queue?.textChannel || textChannel);
|
|
2207
|
+
if (resolved instanceof Playlist) {
|
|
2208
|
+
if (!this.options.nsfw && !isNsfw) {
|
|
2209
|
+
resolved.songs = resolved.songs.filter((s) => !s.ageRestricted);
|
|
2210
|
+
if (!resolved.songs.length) throw new DisTubeError("EMPTY_FILTERED_PLAYLIST");
|
|
2211
|
+
}
|
|
2212
|
+
if (!resolved.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
|
|
2213
|
+
this.debug(`[${queue.id}] Adding playlist to queue: ${resolved.songs.length} songs`);
|
|
2214
|
+
queue.addToQueue(resolved.songs, position);
|
|
2215
|
+
if (queue.playing || this.options.emitAddListWhenCreatingQueue) this.emit("addList" /* ADD_LIST */, queue, resolved);
|
|
2216
|
+
} else {
|
|
2217
|
+
if (!this.options.nsfw && resolved.ageRestricted && !isNsfwChannel(queue?.textChannel || textChannel)) {
|
|
2218
|
+
throw new DisTubeError("NON_NSFW");
|
|
2219
|
+
}
|
|
2220
|
+
this.debug(`[${queue.id}] Adding song to queue: ${resolved.name || resolved.url || resolved.id || resolved}`);
|
|
2221
|
+
queue.addToQueue(resolved, position);
|
|
2222
|
+
if (queue.playing || this.options.emitAddSongWhenCreatingQueue) this.emit("addSong" /* ADD_SONG */, queue, resolved);
|
|
2223
|
+
}
|
|
2224
|
+
if (!queue.playing) await queue.play();
|
|
2225
|
+
else if (skip) await queue.skip();
|
|
2226
|
+
} catch (e) {
|
|
2227
|
+
if (!(e instanceof DisTubeError)) {
|
|
2228
|
+
this.debug(`[${queue.id}] Unexpected error while playing song: ${e.stack || e.message}`);
|
|
2229
|
+
try {
|
|
2230
|
+
e.name = "PlayError";
|
|
2231
|
+
e.message = `${typeof song === "string" ? song : song.url}
|
|
2232
|
+
${e.message}`;
|
|
2233
|
+
} catch {
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
throw e;
|
|
2237
|
+
} finally {
|
|
2238
|
+
queue._taskQueue.resolve();
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Create a custom playlist
|
|
2243
|
+
* @param songs - Array of url or Song
|
|
2244
|
+
* @param options - Optional options
|
|
2245
|
+
*/
|
|
2246
|
+
async createCustomPlaylist(songs, { member, parallel, metadata, name, source, url, thumbnail } = {}) {
|
|
2247
|
+
if (!Array.isArray(songs)) throw new DisTubeError("INVALID_TYPE", "Array", songs, "songs");
|
|
2248
|
+
if (!songs.length) throw new DisTubeError("EMPTY_ARRAY", "songs");
|
|
2249
|
+
const filteredSongs = songs.filter((song) => song instanceof Song || isURL(song));
|
|
2250
|
+
if (!filteredSongs.length) throw new DisTubeError("NO_VALID_SONG");
|
|
2251
|
+
if (member && !isMemberInstance(member)) {
|
|
2252
|
+
throw new DisTubeError("INVALID_TYPE", "Discord.Member", member, "options.member");
|
|
2253
|
+
}
|
|
2254
|
+
let resolvedSongs;
|
|
2255
|
+
if (parallel !== false) {
|
|
2256
|
+
const promises = filteredSongs.map(
|
|
2257
|
+
(song) => this.handler.resolve(song, { member, metadata }).catch(() => void 0)
|
|
2258
|
+
);
|
|
2259
|
+
resolvedSongs = (await Promise.all(promises)).filter((s) => s instanceof Song);
|
|
2260
|
+
} else {
|
|
2261
|
+
resolvedSongs = [];
|
|
2262
|
+
for (const song of filteredSongs) {
|
|
2263
|
+
const resolved = await this.handler.resolve(song, { member, metadata }).catch(() => void 0);
|
|
2264
|
+
if (resolved instanceof Song) resolvedSongs.push(resolved);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
return new Playlist(
|
|
2268
|
+
{
|
|
2269
|
+
source: source || "custom",
|
|
2270
|
+
name,
|
|
2271
|
+
url,
|
|
2272
|
+
thumbnail: thumbnail || resolvedSongs.find((s) => s.thumbnail)?.thumbnail,
|
|
2273
|
+
songs: resolvedSongs
|
|
2274
|
+
},
|
|
2275
|
+
{ member, metadata }
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Get the guild queue
|
|
2280
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2281
|
+
*/
|
|
2282
|
+
getQueue(guild) {
|
|
2283
|
+
return this.queues.get(guild);
|
|
2284
|
+
}
|
|
2285
|
+
#getQueue(guild) {
|
|
2286
|
+
const queue = this.getQueue(guild);
|
|
2287
|
+
if (!queue) throw new DisTubeError("NO_QUEUE");
|
|
2288
|
+
return queue;
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Pause the guild stream
|
|
2292
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2293
|
+
* @returns The guild queue
|
|
2294
|
+
*/
|
|
2295
|
+
pause(guild) {
|
|
2296
|
+
return this.#getQueue(guild).pause();
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Resume the guild stream
|
|
2300
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2301
|
+
* @returns The guild queue
|
|
2302
|
+
*/
|
|
2303
|
+
resume(guild) {
|
|
2304
|
+
return this.#getQueue(guild).resume();
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Stop the guild stream
|
|
2308
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2309
|
+
*/
|
|
2310
|
+
stop(guild) {
|
|
2311
|
+
return this.#getQueue(guild).stop();
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Set the guild stream's volume
|
|
2315
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2316
|
+
* @param percent - The percentage of volume you want to set
|
|
2317
|
+
* @returns The guild queue
|
|
2318
|
+
*/
|
|
2319
|
+
setVolume(guild, percent) {
|
|
2320
|
+
return this.#getQueue(guild).setVolume(percent);
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Skip the playing song if there is a next song in the queue. <info>If {@link
|
|
2324
|
+
* Queue#autoplay} is `true` and there is no up next song, DisTube will add and
|
|
2325
|
+
* play a related song.</info>
|
|
2326
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2327
|
+
* @returns The new Song will be played
|
|
2328
|
+
*/
|
|
2329
|
+
skip(guild) {
|
|
2330
|
+
return this.#getQueue(guild).skip();
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Play the previous song
|
|
2334
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2335
|
+
* @returns The new Song will be played
|
|
2336
|
+
*/
|
|
2337
|
+
previous(guild) {
|
|
2338
|
+
return this.#getQueue(guild).previous();
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Shuffle the guild queue songs
|
|
2342
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2343
|
+
* @returns The guild queue
|
|
2344
|
+
*/
|
|
2345
|
+
shuffle(guild) {
|
|
2346
|
+
return this.#getQueue(guild).shuffle();
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Jump to the song number in the queue. The next one is 1, 2,... The previous one
|
|
2350
|
+
* is -1, -2,...
|
|
2351
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2352
|
+
* @param num - The song number to play
|
|
2353
|
+
* @returns The new Song will be played
|
|
2354
|
+
*/
|
|
2355
|
+
jump(guild, num) {
|
|
2356
|
+
return this.#getQueue(guild).jump(num);
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Set the repeat mode of the guild queue.
|
|
2360
|
+
* Toggle mode `(Disabled -> Song -> Queue -> Disabled ->...)` if `mode` is `undefined`
|
|
2361
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2362
|
+
* @param mode - The repeat modes (toggle if `undefined`)
|
|
2363
|
+
* @returns The new repeat mode
|
|
2364
|
+
*/
|
|
2365
|
+
setRepeatMode(guild, mode) {
|
|
2366
|
+
return this.#getQueue(guild).setRepeatMode(mode);
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Toggle autoplay mode
|
|
2370
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2371
|
+
* @returns Autoplay mode state
|
|
2372
|
+
*/
|
|
2373
|
+
toggleAutoplay(guild) {
|
|
2374
|
+
const queue = this.#getQueue(guild);
|
|
2375
|
+
queue.autoplay = !queue.autoplay;
|
|
2376
|
+
return queue.autoplay;
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Add related song to the queue
|
|
2380
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2381
|
+
* @returns The guild queue
|
|
2382
|
+
*/
|
|
2383
|
+
addRelatedSong(guild) {
|
|
2384
|
+
return this.#getQueue(guild).addRelatedSong();
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Set the playing time to another position
|
|
2388
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
2389
|
+
* @param time - Time in seconds
|
|
2390
|
+
* @returns Seeked queue
|
|
2391
|
+
*/
|
|
2392
|
+
seek(guild, time) {
|
|
2393
|
+
return this.#getQueue(guild).seek(time);
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Emit error event
|
|
2397
|
+
* @param error - error
|
|
2398
|
+
* @param queue - The queue encountered the error
|
|
2399
|
+
* @param song - The playing song when encountered the error
|
|
2400
|
+
*/
|
|
2401
|
+
emitError(error, queue, song) {
|
|
2402
|
+
this.emit("error" /* ERROR */, error, queue, song);
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Emit debug event
|
|
2406
|
+
* @param message - debug message
|
|
2407
|
+
*/
|
|
2408
|
+
debug(message) {
|
|
2409
|
+
this.emit("debug" /* DEBUG */, message);
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
|
|
2413
|
+
// src/index.ts
|
|
2414
|
+
var version = "5.0.3";
|
|
2415
|
+
export {
|
|
2416
|
+
BaseManager,
|
|
2417
|
+
DisTube,
|
|
2418
|
+
DisTubeBase,
|
|
2419
|
+
DisTubeError,
|
|
2420
|
+
DisTubeHandler,
|
|
2421
|
+
DisTubeStream,
|
|
2422
|
+
DisTubeVoice,
|
|
2423
|
+
DisTubeVoiceManager,
|
|
2424
|
+
Events,
|
|
2425
|
+
ExtractorPlugin,
|
|
2426
|
+
FilterManager,
|
|
2427
|
+
GuildIdManager,
|
|
2428
|
+
InfoExtractorPlugin,
|
|
2429
|
+
Options,
|
|
2430
|
+
PlayableExtractorPlugin,
|
|
2431
|
+
Playlist,
|
|
2432
|
+
Plugin,
|
|
2433
|
+
PluginType,
|
|
2434
|
+
Queue,
|
|
2435
|
+
QueueManager,
|
|
2436
|
+
RepeatMode,
|
|
2437
|
+
Song,
|
|
2438
|
+
TaskQueue,
|
|
2439
|
+
checkEncryptionLibraries,
|
|
2440
|
+
checkFFmpeg,
|
|
2441
|
+
checkIntents,
|
|
2442
|
+
checkInvalidKey,
|
|
2443
|
+
DisTube as default,
|
|
2444
|
+
defaultFilters,
|
|
2445
|
+
defaultOptions,
|
|
2446
|
+
formatDuration,
|
|
2447
|
+
isClientInstance,
|
|
2448
|
+
isGuildInstance,
|
|
2449
|
+
isMemberInstance,
|
|
2450
|
+
isMessageInstance,
|
|
2451
|
+
isNsfwChannel,
|
|
2452
|
+
isObject,
|
|
2453
|
+
isSnowflake,
|
|
2454
|
+
isSupportedVoiceChannel,
|
|
2455
|
+
isTextChannelInstance,
|
|
2456
|
+
isTruthy,
|
|
2457
|
+
isURL,
|
|
2458
|
+
isVoiceChannelEmpty,
|
|
2459
|
+
objectKeys,
|
|
2460
|
+
resolveGuildId,
|
|
2461
|
+
version
|
|
2462
|
+
};
|
|
2463
|
+
//# sourceMappingURL=index.mjs.map
|