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