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