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