discord-player 5.3.2-dev.2 → 5.3.2

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