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