discord-player 5.3.2-dev.3 → 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,607 +1,607 @@
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;
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;