discord-player 5.3.0-dev.3 → 5.3.2-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Player.js CHANGED
@@ -1,581 +1,582 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Player = void 0;
4
- const tslib_1 = require("tslib");
5
- const discord_js_1 = require("discord.js");
6
- const tiny_typed_emitter_1 = require("tiny-typed-emitter");
7
- const Queue_1 = require("./Structures/Queue");
8
- const VoiceUtils_1 = require("./VoiceInterface/VoiceUtils");
9
- const types_1 = require("./types/types");
10
- const Track_1 = tslib_1.__importDefault(require("./Structures/Track"));
11
- const QueryResolver_1 = require("./utils/QueryResolver");
12
- const youtube_sr_1 = tslib_1.__importDefault(require("youtube-sr"));
13
- const Util_1 = require("./utils/Util");
14
- const spotify_url_info_1 = tslib_1.__importDefault(require("spotify-url-info"));
15
- const PlayerError_1 = require("./Structures/PlayerError");
16
- const ytdl_core_1 = require("ytdl-core");
17
- const soundcloud_scraper_1 = require("soundcloud-scraper");
18
- const Playlist_1 = require("./Structures/Playlist");
19
- const ExtractorModel_1 = require("./Structures/ExtractorModel");
20
- const voice_1 = require("@discordjs/voice");
21
- const soundcloud = new soundcloud_scraper_1.Client();
22
- class Player extends tiny_typed_emitter_1.TypedEmitter {
23
- /**
24
- * Creates new Discord Player
25
- * @param {Client} client The Discord Client
26
- * @param {PlayerInitOptions} [options] The player init options
27
- */
28
- constructor(client, options = {}) {
29
- super();
30
- this.options = {
31
- autoRegisterExtractor: true,
32
- ytdlOptions: {
33
- highWaterMark: 1 << 25
34
- },
35
- connectionTimeout: 20000
36
- };
37
- this.queues = new discord_js_1.Collection();
38
- this.voiceUtils = new VoiceUtils_1.VoiceUtils();
39
- this.extractors = new discord_js_1.Collection();
40
- this.requiredEvents = ["error", "connectionError"];
41
- /**
42
- * The discord.js client
43
- * @type {Client}
44
- */
45
- this.client = client;
46
- if (this.client?.options?.intents && !new discord_js_1.IntentsBitField(this.client?.options?.intents).has(discord_js_1.IntentsBitField.Flags.GuildVoiceStates)) {
47
- throw new PlayerError_1.PlayerError('client is missing "GuildVoiceStates" intent');
48
- }
49
- /**
50
- * The extractors collection
51
- * @type {ExtractorModel}
52
- */
53
- this.options = Object.assign(this.options, options);
54
- this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this));
55
- if (this.options?.autoRegisterExtractor) {
56
- let nv; // eslint-disable-line @typescript-eslint/no-explicit-any
57
- if ((nv = Util_1.Util.require("@discord-player/extractor"))) {
58
- ["Attachment", "Facebook", "Reverbnation", "Vimeo"].forEach((ext) => void this.use(ext, nv[ext]));
59
- }
60
- }
61
- }
62
- /**
63
- * Handles voice state update
64
- * @param {VoiceState} oldState The old voice state
65
- * @param {VoiceState} newState The new voice state
66
- * @returns {void}
67
- * @private
68
- */
69
- _handleVoiceState(oldState, newState) {
70
- const queue = this.getQueue(oldState.guild.id);
71
- if (!queue || !queue.connection)
72
- return;
73
- if (oldState.channelId && !newState.channelId && newState.member.id === newState.guild.members.me.id) {
74
- try {
75
- queue.destroy();
76
- }
77
- catch {
78
- /* noop */
79
- }
80
- return void this.emit("botDisconnect", queue);
81
- }
82
- if (!oldState.channelId && newState.channelId && newState.member.id === newState.guild.members.me.id) {
83
- if (!oldState.serverMute && newState.serverMute) {
84
- // state.serverMute can be null
85
- queue.setPaused(!!newState.serverMute);
86
- }
87
- else if (!oldState.suppress && newState.suppress) {
88
- // state.suppress can be null
89
- queue.setPaused(!!newState.suppress);
90
- if (newState.suppress) {
91
- newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util_1.Util.noop);
92
- }
93
- }
94
- }
95
- if (oldState.channelId === newState.channelId && newState.member.id === newState.guild.members.me.id) {
96
- if (!oldState.serverMute && newState.serverMute) {
97
- // state.serverMute can be null
98
- queue.setPaused(!!newState.serverMute);
99
- }
100
- else if (!oldState.suppress && newState.suppress) {
101
- // state.suppress can be null
102
- queue.setPaused(!!newState.suppress);
103
- if (newState.suppress) {
104
- newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util_1.Util.noop);
105
- }
106
- }
107
- }
108
- if (queue.connection && !newState.channelId && oldState.channelId === queue.connection.channel.id) {
109
- if (!Util_1.Util.isVoiceEmpty(queue.connection.channel))
110
- return;
111
- const timeout = setTimeout(() => {
112
- if (!Util_1.Util.isVoiceEmpty(queue.connection.channel))
113
- return;
114
- if (!this.queues.has(queue.guild.id))
115
- return;
116
- if (queue.options.leaveOnEmpty)
117
- queue.destroy(true);
118
- this.emit("channelEmpty", queue);
119
- }, queue.options.leaveOnEmptyCooldown || 0).unref();
120
- queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
121
- }
122
- if (queue.connection && newState.channelId && newState.channelId === queue.connection.channel.id) {
123
- const emptyTimeout = queue._cooldownsTimeout.get(`empty_${oldState.guild.id}`);
124
- const channelEmpty = Util_1.Util.isVoiceEmpty(queue.connection.channel);
125
- if (!channelEmpty && emptyTimeout) {
126
- clearTimeout(emptyTimeout);
127
- queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
128
- }
129
- }
130
- if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId && newState.member.id === newState.guild.members.me.id) {
131
- if (queue.connection && newState.member.id === newState.guild.members.me.id)
132
- queue.connection.channel = newState.channel;
133
- const emptyTimeout = queue._cooldownsTimeout.get(`empty_${oldState.guild.id}`);
134
- const channelEmpty = Util_1.Util.isVoiceEmpty(queue.connection.channel);
135
- if (!channelEmpty && emptyTimeout) {
136
- clearTimeout(emptyTimeout);
137
- queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
138
- }
139
- else {
140
- const timeout = setTimeout(() => {
141
- if (queue.connection && !Util_1.Util.isVoiceEmpty(queue.connection.channel))
142
- return;
143
- if (!this.queues.has(queue.guild.id))
144
- return;
145
- if (queue.options.leaveOnEmpty)
146
- queue.destroy(true);
147
- this.emit("channelEmpty", queue);
148
- }, queue.options.leaveOnEmptyCooldown || 0).unref();
149
- queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
150
- }
151
- }
152
- }
153
- /**
154
- * Creates a queue for a guild if not available, else returns existing queue
155
- * @param {GuildResolvable} guild The guild
156
- * @param {PlayerOptions} queueInitOptions Queue init options
157
- * @returns {Queue}
158
- */
159
- createQueue(guild, queueInitOptions = {}) {
160
- guild = this.client.guilds.resolve(guild);
161
- if (!guild)
162
- throw new PlayerError_1.PlayerError("Unknown Guild", PlayerError_1.ErrorStatusCode.UNKNOWN_GUILD);
163
- if (this.queues.has(guild.id))
164
- return this.queues.get(guild.id);
165
- const _meta = queueInitOptions.metadata;
166
- delete queueInitOptions["metadata"];
167
- queueInitOptions.volumeSmoothness ?? (queueInitOptions.volumeSmoothness = 0.08);
168
- queueInitOptions.ytdlOptions ?? (queueInitOptions.ytdlOptions = this.options.ytdlOptions);
169
- const queue = new Queue_1.Queue(this, guild, queueInitOptions);
170
- queue.metadata = _meta;
171
- this.queues.set(guild.id, queue);
172
- return queue;
173
- }
174
- /**
175
- * Returns the queue if available
176
- * @param {GuildResolvable} guild The guild id
177
- * @returns {Queue}
178
- */
179
- getQueue(guild) {
180
- guild = this.client.guilds.resolve(guild);
181
- if (!guild)
182
- throw new PlayerError_1.PlayerError("Unknown Guild", PlayerError_1.ErrorStatusCode.UNKNOWN_GUILD);
183
- return this.queues.get(guild.id);
184
- }
185
- /**
186
- * Deletes a queue and returns deleted queue object
187
- * @param {GuildResolvable} guild The guild id to remove
188
- * @returns {Queue}
189
- */
190
- deleteQueue(guild) {
191
- guild = this.client.guilds.resolve(guild);
192
- if (!guild)
193
- throw new PlayerError_1.PlayerError("Unknown Guild", PlayerError_1.ErrorStatusCode.UNKNOWN_GUILD);
194
- const prev = this.getQueue(guild);
195
- try {
196
- prev.destroy();
197
- }
198
- catch { } // eslint-disable-line no-empty
199
- this.queues.delete(guild.id);
200
- return prev;
201
- }
202
- /**
203
- * @typedef {object} PlayerSearchResult
204
- * @property {Playlist} [playlist] The playlist (if any)
205
- * @property {Track[]} tracks The tracks
206
- */
207
- /**
208
- * Search tracks
209
- * @param {string|Track} query The search query
210
- * @param {SearchOptions} options The search options
211
- * @returns {Promise<PlayerSearchResult>}
212
- */
213
- async search(query, options) {
214
- if (query instanceof Track_1.default)
215
- return { playlist: query.playlist || null, tracks: [query] };
216
- if (!options)
217
- throw new PlayerError_1.PlayerError("DiscordPlayer#search needs search options!", PlayerError_1.ErrorStatusCode.INVALID_ARG_TYPE);
218
- options.requestedBy = this.client.users.resolve(options.requestedBy);
219
- if (!("searchEngine" in options))
220
- options.searchEngine = types_1.QueryType.AUTO;
221
- if (typeof options.searchEngine === "string" && this.extractors.has(options.searchEngine)) {
222
- const extractor = this.extractors.get(options.searchEngine);
223
- if (!extractor.validate(query))
224
- return { playlist: null, tracks: [] };
225
- const data = await extractor.handle(query);
226
- if (data && data.data.length) {
227
- const playlist = !data.playlist
228
- ? null
229
- : new Playlist_1.Playlist(this, {
230
- ...data.playlist,
231
- tracks: []
232
- });
233
- const tracks = data.data.map((m) => new Track_1.default(this, {
234
- ...m,
235
- requestedBy: options.requestedBy,
236
- duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.duration)),
237
- playlist: playlist
238
- }));
239
- if (playlist)
240
- playlist.tracks = tracks;
241
- return { playlist: playlist, tracks: tracks };
242
- }
243
- }
244
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
245
- for (const [_, extractor] of this.extractors) {
246
- if (options.blockExtractor)
247
- break;
248
- if (!extractor.validate(query))
249
- continue;
250
- const data = await extractor.handle(query);
251
- if (data && data.data.length) {
252
- const playlist = !data.playlist
253
- ? null
254
- : new Playlist_1.Playlist(this, {
255
- ...data.playlist,
256
- tracks: []
257
- });
258
- const tracks = data.data.map((m) => new Track_1.default(this, {
259
- ...m,
260
- requestedBy: options.requestedBy,
261
- duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.duration)),
262
- playlist: playlist
263
- }));
264
- if (playlist)
265
- playlist.tracks = tracks;
266
- return { playlist: playlist, tracks: tracks };
267
- }
268
- }
269
- const qt = options.searchEngine === types_1.QueryType.AUTO ? QueryResolver_1.QueryResolver.resolve(query) : options.searchEngine;
270
- switch (qt) {
271
- case types_1.QueryType.YOUTUBE_VIDEO: {
272
- const info = await (0, ytdl_core_1.getInfo)(query, this.options.ytdlOptions).catch(Util_1.Util.noop);
273
- if (!info)
274
- return { playlist: null, tracks: [] };
275
- const track = new Track_1.default(this, {
276
- title: info.videoDetails.title,
277
- description: info.videoDetails.description,
278
- author: info.videoDetails.author?.name,
279
- url: info.videoDetails.video_url,
280
- requestedBy: options.requestedBy,
281
- thumbnail: Util_1.Util.last(info.videoDetails.thumbnails)?.url,
282
- views: parseInt(info.videoDetails.viewCount.replace(/[^0-9]/g, "")) || 0,
283
- duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(parseInt(info.videoDetails.lengthSeconds) * 1000)),
284
- source: "youtube",
285
- raw: info
286
- });
287
- return { playlist: null, tracks: [track] };
288
- }
289
- case types_1.QueryType.YOUTUBE_SEARCH: {
290
- const videos = await youtube_sr_1.default.search(query, {
291
- type: "video"
292
- }).catch(Util_1.Util.noop);
293
- if (!videos)
294
- return { playlist: null, tracks: [] };
295
- const tracks = videos.map((m) => {
296
- m.source = "youtube"; // eslint-disable-line @typescript-eslint/no-explicit-any
297
- return new Track_1.default(this, {
298
- title: m.title,
299
- description: m.description,
300
- author: m.channel?.name,
301
- url: m.url,
302
- requestedBy: options.requestedBy,
303
- thumbnail: m.thumbnail?.displayThumbnailURL("maxresdefault"),
304
- views: m.views,
305
- duration: m.durationFormatted,
306
- source: "youtube",
307
- raw: m
308
- });
309
- });
310
- return { playlist: null, tracks };
311
- }
312
- case types_1.QueryType.SOUNDCLOUD_TRACK:
313
- case types_1.QueryType.SOUNDCLOUD_SEARCH: {
314
- const result = QueryResolver_1.QueryResolver.resolve(query) === types_1.QueryType.SOUNDCLOUD_TRACK ? [{ url: query }] : await soundcloud.search(query, "track").catch(() => []);
315
- if (!result || !result.length)
316
- return { playlist: null, tracks: [] };
317
- const res = [];
318
- for (const r of result) {
319
- const trackInfo = await soundcloud.getSongInfo(r.url).catch(Util_1.Util.noop);
320
- if (!trackInfo)
321
- continue;
322
- const track = new Track_1.default(this, {
323
- title: trackInfo.title,
324
- url: trackInfo.url,
325
- duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(trackInfo.duration)),
326
- description: trackInfo.description,
327
- thumbnail: trackInfo.thumbnail,
328
- views: trackInfo.playCount,
329
- author: trackInfo.author.name,
330
- requestedBy: options.requestedBy,
331
- source: "soundcloud",
332
- engine: trackInfo
333
- });
334
- res.push(track);
335
- }
336
- return { playlist: null, tracks: res };
337
- }
338
- case types_1.QueryType.SPOTIFY_SONG: {
339
- const spotifyData = await (0, spotify_url_info_1.default)(await Util_1.Util.getFetch())
340
- .getData(query)
341
- .catch(Util_1.Util.noop);
342
- if (!spotifyData)
343
- return { playlist: null, tracks: [] };
344
- const spotifyTrack = new Track_1.default(this, {
345
- title: spotifyData.name,
346
- description: spotifyData.description ?? "",
347
- author: spotifyData.artists[0]?.name ?? "Unknown Artist",
348
- url: spotifyData.external_urls?.spotify ?? query,
349
- thumbnail: spotifyData.album?.images[0]?.url ?? spotifyData.preview_url?.length
350
- ? `https://i.scdn.co/image/${spotifyData.preview_url?.split("?cid=")[1]}`
351
- : "https://www.scdn.co/i/_global/twitter_card-default.jpg",
352
- duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(spotifyData.duration_ms)),
353
- views: 0,
354
- requestedBy: options.requestedBy,
355
- source: "spotify"
356
- });
357
- return { playlist: null, tracks: [spotifyTrack] };
358
- }
359
- case types_1.QueryType.SPOTIFY_PLAYLIST:
360
- case types_1.QueryType.SPOTIFY_ALBUM: {
361
- const spotifyPlaylist = await (0, spotify_url_info_1.default)(await Util_1.Util.getFetch())
362
- .getData(query)
363
- .catch(Util_1.Util.noop);
364
- if (!spotifyPlaylist)
365
- return { playlist: null, tracks: [] };
366
- const playlist = new Playlist_1.Playlist(this, {
367
- title: spotifyPlaylist.name ?? spotifyPlaylist.title,
368
- description: spotifyPlaylist.description ?? "",
369
- thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
370
- type: spotifyPlaylist.type,
371
- source: "spotify",
372
- author: spotifyPlaylist.type !== "playlist"
373
- ? {
374
- name: spotifyPlaylist.artists[0]?.name ?? "Unknown Artist",
375
- url: spotifyPlaylist.artists[0]?.external_urls?.spotify ?? null
376
- }
377
- : {
378
- name: spotifyPlaylist.owner?.display_name ?? spotifyPlaylist.owner?.id ?? "Unknown Artist",
379
- url: spotifyPlaylist.owner?.external_urls?.spotify ?? null
380
- },
381
- tracks: [],
382
- id: spotifyPlaylist.id,
383
- url: spotifyPlaylist.external_urls?.spotify ?? query,
384
- rawPlaylist: spotifyPlaylist
385
- });
386
- if (spotifyPlaylist.type !== "playlist") {
387
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
388
- playlist.tracks = spotifyPlaylist.tracks.items.map((m) => {
389
- const data = new Track_1.default(this, {
390
- title: m.name ?? "",
391
- description: m.description ?? "",
392
- author: m.artists[0]?.name ?? "Unknown Artist",
393
- url: m.external_urls?.spotify ?? query,
394
- thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
395
- duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.duration_ms)),
396
- views: 0,
397
- requestedBy: options.requestedBy,
398
- playlist,
399
- source: "spotify"
400
- });
401
- return data;
402
- });
403
- }
404
- else {
405
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
406
- playlist.tracks = spotifyPlaylist.tracks.items.map((m) => {
407
- const data = new Track_1.default(this, {
408
- title: m.track.name ?? "",
409
- description: m.track.description ?? "",
410
- author: m.track.artists?.[0]?.name ?? "Unknown Artist",
411
- url: m.track.external_urls?.spotify ?? query,
412
- thumbnail: m.track.album?.images?.[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
413
- duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.track.duration_ms)),
414
- views: 0,
415
- requestedBy: options.requestedBy,
416
- playlist,
417
- source: "spotify"
418
- });
419
- return data;
420
- });
421
- }
422
- return { playlist: playlist, tracks: playlist.tracks };
423
- }
424
- case types_1.QueryType.SOUNDCLOUD_PLAYLIST: {
425
- const data = await soundcloud.getPlaylist(query).catch(Util_1.Util.noop);
426
- if (!data)
427
- return { playlist: null, tracks: [] };
428
- const res = new Playlist_1.Playlist(this, {
429
- title: data.title,
430
- description: data.description ?? "",
431
- thumbnail: data.thumbnail ?? "https://soundcloud.com/pwa-icon-192.png",
432
- type: "playlist",
433
- source: "soundcloud",
434
- author: {
435
- name: data.author?.name ?? data.author?.username ?? "Unknown Artist",
436
- url: data.author?.profile
437
- },
438
- tracks: [],
439
- id: `${data.id}`,
440
- url: data.url,
441
- rawPlaylist: data
442
- });
443
- for (const song of data.tracks) {
444
- const track = new Track_1.default(this, {
445
- title: song.title,
446
- description: song.description ?? "",
447
- author: song.author?.username ?? song.author?.name ?? "Unknown Artist",
448
- url: song.url,
449
- thumbnail: song.thumbnail,
450
- duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(song.duration)),
451
- views: song.playCount ?? 0,
452
- requestedBy: options.requestedBy,
453
- playlist: res,
454
- source: "soundcloud",
455
- engine: song
456
- });
457
- res.tracks.push(track);
458
- }
459
- return { playlist: res, tracks: res.tracks };
460
- }
461
- case types_1.QueryType.YOUTUBE_PLAYLIST: {
462
- const ytpl = await youtube_sr_1.default.getPlaylist(query).catch(Util_1.Util.noop);
463
- if (!ytpl)
464
- return { playlist: null, tracks: [] };
465
- await ytpl.fetch().catch(Util_1.Util.noop);
466
- const playlist = new Playlist_1.Playlist(this, {
467
- title: ytpl.title,
468
- thumbnail: ytpl.thumbnail,
469
- description: "",
470
- type: "playlist",
471
- source: "youtube",
472
- author: {
473
- name: ytpl.channel.name,
474
- url: ytpl.channel.url
475
- },
476
- tracks: [],
477
- id: ytpl.id,
478
- url: ytpl.url,
479
- rawPlaylist: ytpl
480
- });
481
- playlist.tracks = ytpl.videos.map((video) => new Track_1.default(this, {
482
- title: video.title,
483
- description: video.description,
484
- author: video.channel?.name,
485
- url: video.url,
486
- requestedBy: options.requestedBy,
487
- thumbnail: video.thumbnail.url,
488
- views: video.views,
489
- duration: video.durationFormatted,
490
- raw: video,
491
- playlist: playlist,
492
- source: "youtube"
493
- }));
494
- return { playlist: playlist, tracks: playlist.tracks };
495
- }
496
- default:
497
- return { playlist: null, tracks: [] };
498
- }
499
- }
500
- /**
501
- * Registers extractor
502
- * @param {string} extractorName The extractor name
503
- * @param {ExtractorModel|any} extractor The extractor object
504
- * @param {boolean} [force=false] Overwrite existing extractor with this name (if available)
505
- * @returns {ExtractorModel}
506
- */
507
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
508
- use(extractorName, extractor, force = false) {
509
- if (!extractorName)
510
- throw new PlayerError_1.PlayerError("Cannot use unknown extractor!", PlayerError_1.ErrorStatusCode.UNKNOWN_EXTRACTOR);
511
- if (this.extractors.has(extractorName) && !force)
512
- return this.extractors.get(extractorName);
513
- if (extractor instanceof ExtractorModel_1.ExtractorModel) {
514
- this.extractors.set(extractorName, extractor);
515
- return extractor;
516
- }
517
- for (const method of ["validate", "getInfo"]) {
518
- if (typeof extractor[method] !== "function")
519
- throw new PlayerError_1.PlayerError("Invalid extractor data!", PlayerError_1.ErrorStatusCode.INVALID_EXTRACTOR);
520
- }
521
- const model = new ExtractorModel_1.ExtractorModel(extractorName, extractor);
522
- this.extractors.set(model.name, model);
523
- return model;
524
- }
525
- /**
526
- * Removes registered extractor
527
- * @param {string} extractorName The extractor name
528
- * @returns {ExtractorModel}
529
- */
530
- unuse(extractorName) {
531
- if (!this.extractors.has(extractorName))
532
- throw new PlayerError_1.PlayerError(`Cannot find extractor "${extractorName}"`, PlayerError_1.ErrorStatusCode.UNKNOWN_EXTRACTOR);
533
- const prev = this.extractors.get(extractorName);
534
- this.extractors.delete(extractorName);
535
- return prev;
536
- }
537
- /**
538
- * Generates a report of the dependencies used by the `@discordjs/voice` module. Useful for debugging.
539
- * @returns {string}
540
- */
541
- scanDeps() {
542
- const line = "-".repeat(50);
543
- const depsReport = (0, voice_1.generateDependencyReport)();
544
- const extractorReport = this.extractors
545
- .map((m) => {
546
- return `${m.name} :: ${m.version || "0.1.0"}`;
547
- })
548
- .join("\n");
549
- return `${depsReport}\n${line}\nLoaded Extractors:\n${extractorReport || "None"}`;
550
- }
551
- emit(eventName, ...args) {
552
- if (this.requiredEvents.includes(eventName) && !super.eventNames().includes(eventName)) {
553
- // eslint-disable-next-line no-console
554
- console.error(...args);
555
- process.emitWarning(`[DiscordPlayerWarning] Unhandled "${eventName}" event! Events ${this.requiredEvents.map((m) => `"${m}"`).join(", ")} must have event listeners!`);
556
- return false;
557
- }
558
- else {
559
- return super.emit(eventName, ...args);
560
- }
561
- }
562
- /**
563
- * Resolves queue
564
- * @param {GuildResolvable|Queue} queueLike Queue like object
565
- * @returns {Queue}
566
- */
567
- resolveQueue(queueLike) {
568
- return this.getQueue(queueLike instanceof Queue_1.Queue ? queueLike.guild : queueLike);
569
- }
570
- *[Symbol.iterator]() {
571
- yield* Array.from(this.queues.values());
572
- }
573
- /**
574
- * Creates `Playlist` instance
575
- * @param data The data to initialize a playlist
576
- */
577
- createPlaylist(data) {
578
- return new Playlist_1.Playlist(this, data);
579
- }
580
- }
581
- exports.Player = Player;
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Player = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const discord_js_1 = require("discord.js");
6
+ const tiny_typed_emitter_1 = require("tiny-typed-emitter");
7
+ const Queue_1 = require("./Structures/Queue");
8
+ const VoiceUtils_1 = require("./VoiceInterface/VoiceUtils");
9
+ const types_1 = require("./types/types");
10
+ const Track_1 = tslib_1.__importDefault(require("./Structures/Track"));
11
+ const QueryResolver_1 = require("./utils/QueryResolver");
12
+ const youtube_sr_1 = tslib_1.__importDefault(require("youtube-sr"));
13
+ const Util_1 = require("./utils/Util");
14
+ const spotify_url_info_1 = tslib_1.__importDefault(require("spotify-url-info"));
15
+ const PlayerError_1 = require("./Structures/PlayerError");
16
+ const ytdl_core_1 = require("ytdl-core");
17
+ const soundcloud_scraper_1 = require("soundcloud-scraper");
18
+ const Playlist_1 = require("./Structures/Playlist");
19
+ const ExtractorModel_1 = require("./Structures/ExtractorModel");
20
+ const voice_1 = require("@discordjs/voice");
21
+ const soundcloud = new soundcloud_scraper_1.Client();
22
+ class Player extends tiny_typed_emitter_1.TypedEmitter {
23
+ /**
24
+ * Creates new Discord Player
25
+ * @param {Client} client The Discord Client
26
+ * @param {PlayerInitOptions} [options] The player init options
27
+ */
28
+ constructor(client, options = {}) {
29
+ super();
30
+ this.options = {
31
+ autoRegisterExtractor: true,
32
+ ytdlOptions: {
33
+ highWaterMark: 1 << 25
34
+ },
35
+ connectionTimeout: 20000
36
+ };
37
+ this.queues = new discord_js_1.Collection();
38
+ this.voiceUtils = new VoiceUtils_1.VoiceUtils();
39
+ this.extractors = new discord_js_1.Collection();
40
+ this.requiredEvents = ["error", "connectionError"];
41
+ /**
42
+ * The discord.js client
43
+ * @type {Client}
44
+ */
45
+ this.client = client;
46
+ if (this.client?.options?.intents && !new discord_js_1.IntentsBitField(this.client?.options?.intents).has(discord_js_1.IntentsBitField.Flags.GuildVoiceStates)) {
47
+ throw new PlayerError_1.PlayerError('client is missing "GuildVoiceStates" intent');
48
+ }
49
+ /**
50
+ * The extractors collection
51
+ * @type {ExtractorModel}
52
+ */
53
+ this.options = Object.assign(this.options, options);
54
+ this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this));
55
+ if (this.options?.autoRegisterExtractor) {
56
+ let nv; // eslint-disable-line @typescript-eslint/no-explicit-any
57
+ if ((nv = Util_1.Util.require("@discord-player/extractor"))) {
58
+ ["Attachment", "Facebook", "Reverbnation", "Vimeo"].forEach((ext) => void this.use(ext, nv[ext]));
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Handles voice state update
64
+ * @param {VoiceState} oldState The old voice state
65
+ * @param {VoiceState} newState The new voice state
66
+ * @returns {void}
67
+ * @private
68
+ */
69
+ _handleVoiceState(oldState, newState) {
70
+ const queue = this.getQueue(oldState.guild.id);
71
+ if (!queue || !queue.connection)
72
+ return;
73
+ if (oldState.channelId && !newState.channelId && newState.member.id === newState.guild.members.me.id) {
74
+ try {
75
+ queue.destroy();
76
+ }
77
+ catch {
78
+ /* noop */
79
+ }
80
+ return void this.emit("botDisconnect", queue);
81
+ }
82
+ if (!oldState.channelId && newState.channelId && newState.member.id === newState.guild.members.me.id) {
83
+ if (!oldState.serverMute && newState.serverMute) {
84
+ // state.serverMute can be null
85
+ queue.setPaused(!!newState.serverMute);
86
+ }
87
+ else if (!oldState.suppress && newState.suppress) {
88
+ // state.suppress can be null
89
+ queue.setPaused(!!newState.suppress);
90
+ if (newState.suppress) {
91
+ newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util_1.Util.noop);
92
+ }
93
+ }
94
+ }
95
+ if (oldState.channelId === newState.channelId && newState.member.id === newState.guild.members.me.id) {
96
+ if (!oldState.serverMute && newState.serverMute) {
97
+ // state.serverMute can be null
98
+ queue.setPaused(!!newState.serverMute);
99
+ }
100
+ else if (!oldState.suppress && newState.suppress) {
101
+ // state.suppress can be null
102
+ queue.setPaused(!!newState.suppress);
103
+ if (newState.suppress) {
104
+ newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util_1.Util.noop);
105
+ }
106
+ }
107
+ }
108
+ if (queue.connection && !newState.channelId && oldState.channelId === queue.connection.channel.id) {
109
+ if (!Util_1.Util.isVoiceEmpty(queue.connection.channel))
110
+ return;
111
+ const timeout = setTimeout(() => {
112
+ if (!Util_1.Util.isVoiceEmpty(queue.connection.channel))
113
+ return;
114
+ if (!this.queues.has(queue.guild.id))
115
+ return;
116
+ if (queue.options.leaveOnEmpty)
117
+ queue.destroy(true);
118
+ this.emit("channelEmpty", queue);
119
+ }, queue.options.leaveOnEmptyCooldown || 0).unref();
120
+ queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
121
+ }
122
+ if (queue.connection && newState.channelId && newState.channelId === queue.connection.channel.id) {
123
+ const emptyTimeout = queue._cooldownsTimeout.get(`empty_${oldState.guild.id}`);
124
+ const channelEmpty = Util_1.Util.isVoiceEmpty(queue.connection.channel);
125
+ if (!channelEmpty && emptyTimeout) {
126
+ clearTimeout(emptyTimeout);
127
+ queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
128
+ }
129
+ }
130
+ if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId && newState.member.id === newState.guild.members.me.id) {
131
+ if (queue.connection && newState.member.id === newState.guild.members.me.id)
132
+ queue.connection.channel = newState.channel;
133
+ const emptyTimeout = queue._cooldownsTimeout.get(`empty_${oldState.guild.id}`);
134
+ const channelEmpty = Util_1.Util.isVoiceEmpty(queue.connection.channel);
135
+ if (!channelEmpty && emptyTimeout) {
136
+ clearTimeout(emptyTimeout);
137
+ queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
138
+ }
139
+ else {
140
+ const timeout = setTimeout(() => {
141
+ if (queue.connection && !Util_1.Util.isVoiceEmpty(queue.connection.channel))
142
+ return;
143
+ if (!this.queues.has(queue.guild.id))
144
+ return;
145
+ if (queue.options.leaveOnEmpty)
146
+ queue.destroy(true);
147
+ this.emit("channelEmpty", queue);
148
+ }, queue.options.leaveOnEmptyCooldown || 0).unref();
149
+ queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
150
+ }
151
+ }
152
+ }
153
+ /**
154
+ * Creates a queue for a guild if not available, else returns existing queue
155
+ * @param {GuildResolvable} guild The guild
156
+ * @param {PlayerOptions} queueInitOptions Queue init options
157
+ * @returns {Queue}
158
+ */
159
+ createQueue(guild, queueInitOptions = {}) {
160
+ guild = this.client.guilds.resolve(guild);
161
+ if (!guild)
162
+ throw new PlayerError_1.PlayerError("Unknown Guild", PlayerError_1.ErrorStatusCode.UNKNOWN_GUILD);
163
+ if (this.queues.has(guild.id))
164
+ return this.queues.get(guild.id);
165
+ const _meta = queueInitOptions.metadata;
166
+ delete queueInitOptions["metadata"];
167
+ queueInitOptions.volumeSmoothness ?? (queueInitOptions.volumeSmoothness = 0.08);
168
+ queueInitOptions.ytdlOptions ?? (queueInitOptions.ytdlOptions = this.options.ytdlOptions);
169
+ const queue = new Queue_1.Queue(this, guild, queueInitOptions);
170
+ queue.metadata = _meta;
171
+ this.queues.set(guild.id, queue);
172
+ return queue;
173
+ }
174
+ /**
175
+ * Returns the queue if available
176
+ * @param {GuildResolvable} guild The guild id
177
+ * @returns {Queue | undefined}
178
+ */
179
+ getQueue(guild) {
180
+ guild = this.client.guilds.resolve(guild);
181
+ if (!guild)
182
+ throw new PlayerError_1.PlayerError("Unknown Guild", PlayerError_1.ErrorStatusCode.UNKNOWN_GUILD);
183
+ return this.queues.get(guild.id);
184
+ }
185
+ /**
186
+ * Deletes a queue and returns deleted queue object
187
+ * @param {GuildResolvable} guild The guild id to remove
188
+ * @returns {Queue}
189
+ */
190
+ deleteQueue(guild) {
191
+ guild = this.client.guilds.resolve(guild);
192
+ if (!guild)
193
+ throw new PlayerError_1.PlayerError("Unknown Guild", PlayerError_1.ErrorStatusCode.UNKNOWN_GUILD);
194
+ const prev = this.getQueue(guild);
195
+ try {
196
+ prev.destroy();
197
+ }
198
+ catch { } // eslint-disable-line no-empty
199
+ this.queues.delete(guild.id);
200
+ return prev;
201
+ }
202
+ /**
203
+ * @typedef {object} PlayerSearchResult
204
+ * @property {Playlist} [playlist] The playlist (if any)
205
+ * @property {Track[]} tracks The tracks
206
+ */
207
+ /**
208
+ * Search tracks
209
+ * @param {string|Track} query The search query
210
+ * @param {SearchOptions} options The search options
211
+ * @returns {Promise<PlayerSearchResult>}
212
+ */
213
+ async search(query, options) {
214
+ if (query instanceof Track_1.default)
215
+ return { playlist: query.playlist || null, tracks: [query] };
216
+ if (!options)
217
+ throw new PlayerError_1.PlayerError("DiscordPlayer#search needs search options!", PlayerError_1.ErrorStatusCode.INVALID_ARG_TYPE);
218
+ options.requestedBy = this.client.users.resolve(options.requestedBy);
219
+ if (!("searchEngine" in options))
220
+ options.searchEngine = types_1.QueryType.AUTO;
221
+ if (typeof options.searchEngine === "string" && this.extractors.has(options.searchEngine)) {
222
+ const extractor = this.extractors.get(options.searchEngine);
223
+ if (!extractor.validate(query))
224
+ return { playlist: null, tracks: [] };
225
+ const data = await extractor.handle(query);
226
+ if (data && data.data.length) {
227
+ const playlist = !data.playlist
228
+ ? null
229
+ : new Playlist_1.Playlist(this, {
230
+ ...data.playlist,
231
+ tracks: []
232
+ });
233
+ const tracks = data.data.map((m) => new Track_1.default(this, {
234
+ ...m,
235
+ requestedBy: options.requestedBy,
236
+ duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.duration)),
237
+ playlist: playlist
238
+ }));
239
+ if (playlist)
240
+ playlist.tracks = tracks;
241
+ return { playlist: playlist, tracks: tracks };
242
+ }
243
+ }
244
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
245
+ for (const [_, extractor] of this.extractors) {
246
+ if (options.blockExtractor)
247
+ break;
248
+ if (!extractor.validate(query))
249
+ continue;
250
+ const data = await extractor.handle(query);
251
+ if (data && data.data.length) {
252
+ const playlist = !data.playlist
253
+ ? null
254
+ : new Playlist_1.Playlist(this, {
255
+ ...data.playlist,
256
+ tracks: []
257
+ });
258
+ const tracks = data.data.map((m) => new Track_1.default(this, {
259
+ ...m,
260
+ requestedBy: options.requestedBy,
261
+ duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.duration)),
262
+ playlist: playlist
263
+ }));
264
+ if (playlist)
265
+ playlist.tracks = tracks;
266
+ return { playlist: playlist, tracks: tracks };
267
+ }
268
+ }
269
+ const qt = options.searchEngine === types_1.QueryType.AUTO ? QueryResolver_1.QueryResolver.resolve(query) : options.searchEngine;
270
+ switch (qt) {
271
+ case types_1.QueryType.YOUTUBE_VIDEO: {
272
+ const info = await (0, ytdl_core_1.getInfo)(query, this.options.ytdlOptions).catch(Util_1.Util.noop);
273
+ if (!info)
274
+ return { playlist: null, tracks: [] };
275
+ const track = new Track_1.default(this, {
276
+ title: info.videoDetails.title,
277
+ description: info.videoDetails.description,
278
+ author: info.videoDetails.author?.name,
279
+ url: info.videoDetails.video_url,
280
+ requestedBy: options.requestedBy,
281
+ thumbnail: Util_1.Util.last(info.videoDetails.thumbnails)?.url,
282
+ views: parseInt(info.videoDetails.viewCount.replace(/[^0-9]/g, "")) || 0,
283
+ duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(parseInt(info.videoDetails.lengthSeconds) * 1000)),
284
+ source: "youtube",
285
+ raw: info
286
+ });
287
+ return { playlist: null, tracks: [track] };
288
+ }
289
+ case types_1.QueryType.YOUTUBE_SEARCH: {
290
+ const videos = await youtube_sr_1.default.search(query, {
291
+ type: "video"
292
+ }).catch(Util_1.Util.noop);
293
+ if (!videos)
294
+ return { playlist: null, tracks: [] };
295
+ const tracks = videos.map((m) => {
296
+ m.source = "youtube"; // eslint-disable-line @typescript-eslint/no-explicit-any
297
+ return new Track_1.default(this, {
298
+ title: m.title,
299
+ description: m.description,
300
+ author: m.channel?.name,
301
+ url: m.url,
302
+ requestedBy: options.requestedBy,
303
+ thumbnail: m.thumbnail?.displayThumbnailURL("maxresdefault"),
304
+ views: m.views,
305
+ duration: m.durationFormatted,
306
+ source: "youtube",
307
+ raw: m
308
+ });
309
+ });
310
+ return { playlist: null, tracks };
311
+ }
312
+ case types_1.QueryType.SOUNDCLOUD_TRACK:
313
+ case types_1.QueryType.SOUNDCLOUD_SEARCH: {
314
+ const result = QueryResolver_1.QueryResolver.resolve(query) === types_1.QueryType.SOUNDCLOUD_TRACK ? [{ url: query }] : await soundcloud.search(query, "track").catch(() => []);
315
+ if (!result || !result.length)
316
+ return { playlist: null, tracks: [] };
317
+ const res = [];
318
+ for (const r of result) {
319
+ const trackInfo = await soundcloud.getSongInfo(r.url).catch(Util_1.Util.noop);
320
+ if (!trackInfo)
321
+ continue;
322
+ const track = new Track_1.default(this, {
323
+ title: trackInfo.title,
324
+ url: trackInfo.url,
325
+ duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(trackInfo.duration)),
326
+ description: trackInfo.description,
327
+ thumbnail: trackInfo.thumbnail,
328
+ views: trackInfo.playCount,
329
+ author: trackInfo.author.name,
330
+ requestedBy: options.requestedBy,
331
+ source: "soundcloud",
332
+ engine: trackInfo
333
+ });
334
+ res.push(track);
335
+ }
336
+ return { playlist: null, tracks: res };
337
+ }
338
+ case types_1.QueryType.SPOTIFY_SONG: {
339
+ const spotifyData = await (0, spotify_url_info_1.default)(await Util_1.Util.getFetch())
340
+ .getData(query)
341
+ .catch(Util_1.Util.noop);
342
+ if (!spotifyData)
343
+ return { playlist: null, tracks: [] };
344
+ const spotifyTrack = new Track_1.default(this, {
345
+ title: spotifyData.name,
346
+ description: spotifyData.description ?? "",
347
+ author: spotifyData.artists[0]?.name ?? "Unknown Artist",
348
+ url: spotifyData.external_urls?.spotify ?? query,
349
+ thumbnail: (spotifyData.coverArt?.sources?.[0]?.url ??
350
+ spotifyData.album?.images[0]?.url ??
351
+ (spotifyData.preview_url?.length && `https://i.scdn.co/image/${spotifyData.preview_url?.split("?cid=")[1]}`)) ||
352
+ "https://www.scdn.co/i/_global/twitter_card-default.jpg",
353
+ duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(spotifyData.duration_ms ?? spotifyData.duration ?? spotifyData.maxDuration)),
354
+ views: 0,
355
+ requestedBy: options.requestedBy,
356
+ source: "spotify"
357
+ });
358
+ return { playlist: null, tracks: [spotifyTrack] };
359
+ }
360
+ case types_1.QueryType.SPOTIFY_PLAYLIST:
361
+ case types_1.QueryType.SPOTIFY_ALBUM: {
362
+ const spotifyPlaylist = await (0, spotify_url_info_1.default)(await Util_1.Util.getFetch())
363
+ .getData(query)
364
+ .catch(Util_1.Util.noop);
365
+ if (!spotifyPlaylist)
366
+ return { playlist: null, tracks: [] };
367
+ const playlist = new Playlist_1.Playlist(this, {
368
+ title: spotifyPlaylist.name ?? spotifyPlaylist.title,
369
+ description: spotifyPlaylist.description ?? "",
370
+ thumbnail: spotifyPlaylist.coverArt?.sources?.[0]?.url ?? spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
371
+ type: spotifyPlaylist.type,
372
+ source: "spotify",
373
+ author: spotifyPlaylist.type !== "playlist"
374
+ ? {
375
+ name: spotifyPlaylist.artists[0]?.name ?? "Unknown Artist",
376
+ url: spotifyPlaylist.artists[0]?.external_urls?.spotify ?? null
377
+ }
378
+ : {
379
+ name: spotifyPlaylist.owner?.display_name ?? spotifyPlaylist.owner?.id ?? "Unknown Artist",
380
+ url: spotifyPlaylist.owner?.external_urls?.spotify ?? null
381
+ },
382
+ tracks: [],
383
+ id: spotifyPlaylist.id,
384
+ url: spotifyPlaylist.external_urls?.spotify ?? query,
385
+ rawPlaylist: spotifyPlaylist
386
+ });
387
+ if (spotifyPlaylist.type !== "playlist") {
388
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
389
+ playlist.tracks = spotifyPlaylist.tracks.items.map((m) => {
390
+ const data = new Track_1.default(this, {
391
+ title: m.name ?? "",
392
+ description: m.description ?? "",
393
+ author: m.artists[0]?.name ?? "Unknown Artist",
394
+ url: m.external_urls?.spotify ?? query,
395
+ thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
396
+ duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.duration_ms)),
397
+ views: 0,
398
+ requestedBy: options.requestedBy,
399
+ playlist,
400
+ source: "spotify"
401
+ });
402
+ return data;
403
+ });
404
+ }
405
+ else {
406
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
407
+ playlist.tracks = spotifyPlaylist.tracks.items.map((m) => {
408
+ const data = new Track_1.default(this, {
409
+ title: m.track.name ?? "",
410
+ description: m.track.description ?? "",
411
+ author: m.track.artists?.[0]?.name ?? "Unknown Artist",
412
+ url: m.track.external_urls?.spotify ?? query,
413
+ thumbnail: m.track.album?.images?.[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
414
+ duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.track.duration_ms)),
415
+ views: 0,
416
+ requestedBy: options.requestedBy,
417
+ playlist,
418
+ source: "spotify"
419
+ });
420
+ return data;
421
+ });
422
+ }
423
+ return { playlist: playlist, tracks: playlist.tracks };
424
+ }
425
+ case types_1.QueryType.SOUNDCLOUD_PLAYLIST: {
426
+ const data = await soundcloud.getPlaylist(query).catch(Util_1.Util.noop);
427
+ if (!data)
428
+ return { playlist: null, tracks: [] };
429
+ const res = new Playlist_1.Playlist(this, {
430
+ title: data.title,
431
+ description: data.description ?? "",
432
+ thumbnail: data.thumbnail ?? "https://soundcloud.com/pwa-icon-192.png",
433
+ type: "playlist",
434
+ source: "soundcloud",
435
+ author: {
436
+ name: data.author?.name ?? data.author?.username ?? "Unknown Artist",
437
+ url: data.author?.profile
438
+ },
439
+ tracks: [],
440
+ id: `${data.id}`,
441
+ url: data.url,
442
+ rawPlaylist: data
443
+ });
444
+ for (const song of data.tracks) {
445
+ const track = new Track_1.default(this, {
446
+ title: song.title,
447
+ description: song.description ?? "",
448
+ author: song.author?.username ?? song.author?.name ?? "Unknown Artist",
449
+ url: song.url,
450
+ thumbnail: song.thumbnail,
451
+ duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(song.duration)),
452
+ views: song.playCount ?? 0,
453
+ requestedBy: options.requestedBy,
454
+ playlist: res,
455
+ source: "soundcloud",
456
+ engine: song
457
+ });
458
+ res.tracks.push(track);
459
+ }
460
+ return { playlist: res, tracks: res.tracks };
461
+ }
462
+ case types_1.QueryType.YOUTUBE_PLAYLIST: {
463
+ const ytpl = await youtube_sr_1.default.getPlaylist(query).catch(Util_1.Util.noop);
464
+ if (!ytpl)
465
+ return { playlist: null, tracks: [] };
466
+ await ytpl.fetch().catch(Util_1.Util.noop);
467
+ const playlist = new Playlist_1.Playlist(this, {
468
+ title: ytpl.title,
469
+ thumbnail: ytpl.thumbnail,
470
+ description: "",
471
+ type: "playlist",
472
+ source: "youtube",
473
+ author: {
474
+ name: ytpl.channel.name,
475
+ url: ytpl.channel.url
476
+ },
477
+ tracks: [],
478
+ id: ytpl.id,
479
+ url: ytpl.url,
480
+ rawPlaylist: ytpl
481
+ });
482
+ playlist.tracks = ytpl.videos.map((video) => new Track_1.default(this, {
483
+ title: video.title,
484
+ description: video.description,
485
+ author: video.channel?.name,
486
+ url: video.url,
487
+ requestedBy: options.requestedBy,
488
+ thumbnail: video.thumbnail.url,
489
+ views: video.views,
490
+ duration: video.durationFormatted,
491
+ raw: video,
492
+ playlist: playlist,
493
+ source: "youtube"
494
+ }));
495
+ return { playlist: playlist, tracks: playlist.tracks };
496
+ }
497
+ default:
498
+ return { playlist: null, tracks: [] };
499
+ }
500
+ }
501
+ /**
502
+ * Registers extractor
503
+ * @param {string} extractorName The extractor name
504
+ * @param {ExtractorModel|any} extractor The extractor object
505
+ * @param {boolean} [force=false] Overwrite existing extractor with this name (if available)
506
+ * @returns {ExtractorModel}
507
+ */
508
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
509
+ use(extractorName, extractor, force = false) {
510
+ if (!extractorName)
511
+ throw new PlayerError_1.PlayerError("Cannot use unknown extractor!", PlayerError_1.ErrorStatusCode.UNKNOWN_EXTRACTOR);
512
+ if (this.extractors.has(extractorName) && !force)
513
+ return this.extractors.get(extractorName);
514
+ if (extractor instanceof ExtractorModel_1.ExtractorModel) {
515
+ this.extractors.set(extractorName, extractor);
516
+ return extractor;
517
+ }
518
+ for (const method of ["validate", "getInfo"]) {
519
+ if (typeof extractor[method] !== "function")
520
+ throw new PlayerError_1.PlayerError("Invalid extractor data!", PlayerError_1.ErrorStatusCode.INVALID_EXTRACTOR);
521
+ }
522
+ const model = new ExtractorModel_1.ExtractorModel(extractorName, extractor);
523
+ this.extractors.set(model.name, model);
524
+ return model;
525
+ }
526
+ /**
527
+ * Removes registered extractor
528
+ * @param {string} extractorName The extractor name
529
+ * @returns {ExtractorModel}
530
+ */
531
+ unuse(extractorName) {
532
+ if (!this.extractors.has(extractorName))
533
+ throw new PlayerError_1.PlayerError(`Cannot find extractor "${extractorName}"`, PlayerError_1.ErrorStatusCode.UNKNOWN_EXTRACTOR);
534
+ const prev = this.extractors.get(extractorName);
535
+ this.extractors.delete(extractorName);
536
+ return prev;
537
+ }
538
+ /**
539
+ * Generates a report of the dependencies used by the `@discordjs/voice` module. Useful for debugging.
540
+ * @returns {string}
541
+ */
542
+ scanDeps() {
543
+ const line = "-".repeat(50);
544
+ const depsReport = (0, voice_1.generateDependencyReport)();
545
+ const extractorReport = this.extractors
546
+ .map((m) => {
547
+ return `${m.name} :: ${m.version || "0.1.0"}`;
548
+ })
549
+ .join("\n");
550
+ return `${depsReport}\n${line}\nLoaded Extractors:\n${extractorReport || "None"}`;
551
+ }
552
+ emit(eventName, ...args) {
553
+ if (this.requiredEvents.includes(eventName) && !super.eventNames().includes(eventName)) {
554
+ // eslint-disable-next-line no-console
555
+ console.error(...args);
556
+ process.emitWarning(`[DiscordPlayerWarning] Unhandled "${eventName}" event! Events ${this.requiredEvents.map((m) => `"${m}"`).join(", ")} must have event listeners!`);
557
+ return false;
558
+ }
559
+ else {
560
+ return super.emit(eventName, ...args);
561
+ }
562
+ }
563
+ /**
564
+ * Resolves queue
565
+ * @param {GuildResolvable|Queue} queueLike Queue like object
566
+ * @returns {Queue}
567
+ */
568
+ resolveQueue(queueLike) {
569
+ return this.getQueue(queueLike instanceof Queue_1.Queue ? queueLike.guild : queueLike);
570
+ }
571
+ *[Symbol.iterator]() {
572
+ yield* Array.from(this.queues.values());
573
+ }
574
+ /**
575
+ * Creates `Playlist` instance
576
+ * @param data The data to initialize a playlist
577
+ */
578
+ createPlaylist(data) {
579
+ return new Playlist_1.Playlist(this, data);
580
+ }
581
+ }
582
+ exports.Player = Player;