2022-06-15 18:10:26 +05:00
|
|
|
import { Client, Collection, GuildResolvable, Snowflake, User, VoiceState, IntentsBitField } from "discord.js";
|
2021-06-11 15:32:22 +05:00
|
|
|
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
|
|
|
|
import { Queue } from "./Structures/Queue";
|
2021-06-11 16:50:43 +05:00
|
|
|
import { VoiceUtils } from "./VoiceInterface/VoiceUtils";
|
2022-07-21 17:08:11 +05:00
|
|
|
import { PlayerEvents, PlayerOptions, QueryType, SearchOptions, PlayerInitOptions, PlayerSearchResult, PlaylistInitData } from "./types/types";
|
2021-06-11 23:19:52 +05:00
|
|
|
import Track from "./Structures/Track";
|
2022-10-31 13:15:32 +05:00
|
|
|
import play, { SoundCloudPlaylist, YouTubePlayList } from "play-dl";
|
2022-09-13 11:40:21 +05:00
|
|
|
import Spotify from "spotify-url-info";
|
2021-06-11 23:19:52 +05:00
|
|
|
import { QueryResolver } from "./utils/QueryResolver";
|
2021-06-13 17:03:20 +05:00
|
|
|
import { Util } from "./utils/Util";
|
2021-08-07 19:25:21 +05:00
|
|
|
import { PlayerError, ErrorStatusCode } from "./Structures/PlayerError";
|
2021-06-13 19:31:27 +05:00
|
|
|
import { Playlist } from "./Structures/Playlist";
|
2021-06-14 18:50:36 +05:00
|
|
|
import { ExtractorModel } from "./Structures/ExtractorModel";
|
2021-06-19 18:12:58 +05:00
|
|
|
import { generateDependencyReport } from "@discordjs/voice";
|
2021-06-13 17:03:20 +05:00
|
|
|
|
2021-06-20 19:26:16 +05:00
|
|
|
class Player extends EventEmitter<PlayerEvents> {
|
2021-06-11 15:32:22 +05:00
|
|
|
public readonly client: Client;
|
2021-07-05 11:28:06 +05:00
|
|
|
public readonly options: PlayerInitOptions = {
|
2021-06-14 19:32:37 +05:00
|
|
|
autoRegisterExtractor: true,
|
2021-08-07 20:34:31 +05:00
|
|
|
connectionTimeout: 20000
|
2021-06-14 18:50:36 +05:00
|
|
|
};
|
2021-06-11 15:32:22 +05:00
|
|
|
public readonly queues = new Collection<Snowflake, Queue>();
|
2021-06-11 16:50:43 +05:00
|
|
|
public readonly voiceUtils = new VoiceUtils();
|
2021-06-14 18:50:36 +05:00
|
|
|
public readonly extractors = new Collection<string, ExtractorModel>();
|
2021-11-28 13:25:27 +05:00
|
|
|
public requiredEvents = ["error", "connectionError"] as string[];
|
2021-06-11 15:32:22 +05:00
|
|
|
|
2021-06-13 15:40:41 +05:00
|
|
|
/**
|
|
|
|
* Creates new Discord Player
|
2021-06-17 18:26:30 +05:00
|
|
|
* @param {Client} client The Discord Client
|
2022-07-19 20:07:02 +05:00
|
|
|
* @param {PlayerInitOptions} [options] The player init options
|
2021-06-13 15:40:41 +05:00
|
|
|
*/
|
2021-07-05 11:28:06 +05:00
|
|
|
constructor(client: Client, options: PlayerInitOptions = {}) {
|
2021-06-11 15:32:22 +05:00
|
|
|
super();
|
2021-06-13 15:40:41 +05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The discord.js client
|
2021-06-17 18:26:30 +05:00
|
|
|
* @type {Client}
|
2021-06-13 15:40:41 +05:00
|
|
|
*/
|
2021-06-11 15:32:22 +05:00
|
|
|
this.client = client;
|
2021-06-13 21:47:04 +05:00
|
|
|
|
2022-06-15 18:10:26 +05:00
|
|
|
if (this.client?.options?.intents && !new IntentsBitField(this.client?.options?.intents).has(IntentsBitField.Flags.GuildVoiceStates)) {
|
|
|
|
throw new PlayerError('client is missing "GuildVoiceStates" intent');
|
2021-08-09 17:58:18 +05:00
|
|
|
}
|
|
|
|
|
2021-06-14 18:50:36 +05:00
|
|
|
/**
|
|
|
|
* The extractors collection
|
|
|
|
* @type {ExtractorModel}
|
|
|
|
*/
|
|
|
|
this.options = Object.assign(this.options, options);
|
|
|
|
|
2021-06-13 21:47:04 +05:00
|
|
|
this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this));
|
2021-06-14 18:50:36 +05:00
|
|
|
|
|
|
|
if (this.options?.autoRegisterExtractor) {
|
2021-06-22 15:24:05 +05:00
|
|
|
let nv: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
2021-06-14 18:50:36 +05:00
|
|
|
|
|
|
|
if ((nv = Util.require("@discord-player/extractor"))) {
|
|
|
|
["Attachment", "Facebook", "Reverbnation", "Vimeo"].forEach((ext) => void this.use(ext, nv[ext]));
|
|
|
|
}
|
|
|
|
}
|
2021-06-13 21:47:04 +05:00
|
|
|
}
|
|
|
|
|
2021-06-20 19:22:09 +05:00
|
|
|
/**
|
|
|
|
* Handles voice state update
|
|
|
|
* @param {VoiceState} oldState The old voice state
|
|
|
|
* @param {VoiceState} newState The new voice state
|
|
|
|
* @returns {void}
|
|
|
|
* @private
|
|
|
|
*/
|
2021-06-13 21:47:04 +05:00
|
|
|
private _handleVoiceState(oldState: VoiceState, newState: VoiceState): void {
|
|
|
|
const queue = this.getQueue(oldState.guild.id);
|
2022-07-19 18:58:01 +05:00
|
|
|
if (!queue || !queue.connection) return;
|
2021-06-13 21:47:04 +05:00
|
|
|
|
2022-07-19 18:58:01 +05:00
|
|
|
if (oldState.channelId && !newState.channelId && newState.member.id === newState.guild.members.me.id) {
|
|
|
|
try {
|
|
|
|
queue.destroy();
|
|
|
|
} catch {
|
|
|
|
/* noop */
|
2021-06-26 00:08:34 +05:00
|
|
|
}
|
2022-07-19 18:58:01 +05:00
|
|
|
return void this.emit("botDisconnect", queue);
|
|
|
|
}
|
2021-06-23 15:34:53 +05:00
|
|
|
|
2022-07-19 18:58:01 +05:00
|
|
|
if (!oldState.channelId && newState.channelId && newState.member.id === newState.guild.members.me.id) {
|
2022-07-21 17:04:03 +05:00
|
|
|
if (!oldState.serverMute && newState.serverMute) {
|
2022-07-21 17:11:44 +05:00
|
|
|
// state.serverMute can be null
|
|
|
|
queue.setPaused(!!newState.serverMute);
|
2022-07-21 17:04:03 +05:00
|
|
|
} else if (!oldState.suppress && newState.suppress) {
|
2022-07-21 17:11:44 +05:00
|
|
|
// state.suppress can be null
|
|
|
|
queue.setPaused(!!newState.suppress);
|
2022-07-21 17:04:03 +05:00
|
|
|
if (newState.suppress) {
|
|
|
|
newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util.noop);
|
|
|
|
}
|
2021-06-26 00:08:34 +05:00
|
|
|
}
|
2022-07-19 18:58:01 +05:00
|
|
|
}
|
2021-06-23 15:34:53 +05:00
|
|
|
|
2022-07-19 18:58:01 +05:00
|
|
|
if (oldState.channelId === newState.channelId && newState.member.id === newState.guild.members.me.id) {
|
2022-07-21 17:04:03 +05:00
|
|
|
if (!oldState.serverMute && newState.serverMute) {
|
2022-07-21 17:11:44 +05:00
|
|
|
// state.serverMute can be null
|
|
|
|
queue.setPaused(!!newState.serverMute);
|
2022-07-21 17:04:03 +05:00
|
|
|
} else if (!oldState.suppress && newState.suppress) {
|
2022-07-21 17:11:44 +05:00
|
|
|
// state.suppress can be null
|
|
|
|
queue.setPaused(!!newState.suppress);
|
2022-07-21 17:04:03 +05:00
|
|
|
if (newState.suppress) {
|
|
|
|
newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util.noop);
|
|
|
|
}
|
2021-11-05 11:34:19 +05:00
|
|
|
}
|
2022-07-19 18:58:01 +05:00
|
|
|
}
|
2021-06-13 21:47:04 +05:00
|
|
|
|
2022-07-19 18:58:01 +05:00
|
|
|
if (queue.connection && !newState.channelId && oldState.channelId === queue.connection.channel.id) {
|
|
|
|
if (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
if (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
|
|
|
if (!this.queues.has(queue.guild.id)) return;
|
|
|
|
if (queue.options.leaveOnEmpty) queue.destroy(true);
|
|
|
|
this.emit("channelEmpty", queue);
|
|
|
|
}, queue.options.leaveOnEmptyCooldown || 0).unref();
|
|
|
|
queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
|
|
|
|
}
|
2021-06-13 21:47:04 +05:00
|
|
|
|
2022-07-19 18:58:01 +05:00
|
|
|
if (queue.connection && newState.channelId && newState.channelId === queue.connection.channel.id) {
|
|
|
|
const emptyTimeout = queue._cooldownsTimeout.get(`empty_${oldState.guild.id}`);
|
|
|
|
const channelEmpty = Util.isVoiceEmpty(queue.connection.channel);
|
|
|
|
if (!channelEmpty && emptyTimeout) {
|
|
|
|
clearTimeout(emptyTimeout);
|
|
|
|
queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
|
|
|
|
}
|
|
|
|
}
|
2021-06-13 21:47:04 +05:00
|
|
|
|
2022-07-19 18:58:01 +05:00
|
|
|
if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId && newState.member.id === newState.guild.members.me.id) {
|
|
|
|
if (queue.connection && newState.member.id === newState.guild.members.me.id) queue.connection.channel = newState.channel;
|
|
|
|
const emptyTimeout = queue._cooldownsTimeout.get(`empty_${oldState.guild.id}`);
|
|
|
|
const channelEmpty = Util.isVoiceEmpty(queue.connection.channel);
|
|
|
|
if (!channelEmpty && emptyTimeout) {
|
|
|
|
clearTimeout(emptyTimeout);
|
|
|
|
queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
|
2021-11-05 11:34:19 +05:00
|
|
|
} else {
|
2022-07-19 18:58:01 +05:00
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
if (queue.connection && !Util.isVoiceEmpty(queue.connection.channel)) return;
|
|
|
|
if (!this.queues.has(queue.guild.id)) return;
|
|
|
|
if (queue.options.leaveOnEmpty) queue.destroy(true);
|
|
|
|
this.emit("channelEmpty", queue);
|
|
|
|
}, queue.options.leaveOnEmptyCooldown || 0).unref();
|
|
|
|
queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
|
2021-10-16 03:09:12 +05:00
|
|
|
}
|
2021-06-13 21:47:04 +05:00
|
|
|
}
|
2021-06-11 15:32:22 +05:00
|
|
|
}
|
|
|
|
|
2021-06-13 15:40:41 +05:00
|
|
|
/**
|
|
|
|
* Creates a queue for a guild if not available, else returns existing queue
|
2021-06-17 18:22:03 +05:00
|
|
|
* @param {GuildResolvable} guild The guild
|
2021-06-13 15:40:41 +05:00
|
|
|
* @param {PlayerOptions} queueInitOptions Queue init options
|
|
|
|
* @returns {Queue}
|
|
|
|
*/
|
2021-06-24 09:17:53 +05:00
|
|
|
createQueue<T = unknown>(guild: GuildResolvable, queueInitOptions: PlayerOptions & { metadata?: T } = {}): Queue<T> {
|
2021-06-17 18:22:03 +05:00
|
|
|
guild = this.client.guilds.resolve(guild);
|
2021-08-07 19:25:21 +05:00
|
|
|
if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD);
|
2021-06-12 11:37:41 +05:00
|
|
|
if (this.queues.has(guild.id)) return this.queues.get(guild.id) as Queue<T>;
|
|
|
|
|
2021-06-13 15:40:41 +05:00
|
|
|
const _meta = queueInitOptions.metadata;
|
|
|
|
delete queueInitOptions["metadata"];
|
2022-01-31 16:44:00 +05:00
|
|
|
queueInitOptions.volumeSmoothness ??= 0.08;
|
2021-06-11 15:32:22 +05:00
|
|
|
const queue = new Queue(this, guild, queueInitOptions);
|
2021-06-13 15:40:41 +05:00
|
|
|
queue.metadata = _meta;
|
2021-06-11 15:32:22 +05:00
|
|
|
this.queues.set(guild.id, queue);
|
|
|
|
|
2021-06-12 11:37:41 +05:00
|
|
|
return queue as Queue<T>;
|
2021-06-11 15:32:22 +05:00
|
|
|
}
|
|
|
|
|
2021-06-13 15:40:41 +05:00
|
|
|
/**
|
|
|
|
* Returns the queue if available
|
2021-06-17 18:22:03 +05:00
|
|
|
* @param {GuildResolvable} guild The guild id
|
2021-06-13 15:40:41 +05:00
|
|
|
* @returns {Queue}
|
|
|
|
*/
|
2021-06-17 18:22:03 +05:00
|
|
|
getQueue<T = unknown>(guild: GuildResolvable) {
|
|
|
|
guild = this.client.guilds.resolve(guild);
|
2021-08-07 19:25:21 +05:00
|
|
|
if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD);
|
2021-06-17 18:22:03 +05:00
|
|
|
return this.queues.get(guild.id) as Queue<T>;
|
2021-06-11 15:32:22 +05:00
|
|
|
}
|
2021-06-11 19:57:49 +05:00
|
|
|
|
2021-06-13 15:40:41 +05:00
|
|
|
/**
|
|
|
|
* Deletes a queue and returns deleted queue object
|
2021-06-17 18:22:03 +05:00
|
|
|
* @param {GuildResolvable} guild The guild id to remove
|
2021-06-13 15:40:41 +05:00
|
|
|
* @returns {Queue}
|
|
|
|
*/
|
2021-06-17 18:22:03 +05:00
|
|
|
deleteQueue<T = unknown>(guild: GuildResolvable) {
|
|
|
|
guild = this.client.guilds.resolve(guild);
|
2021-08-07 19:25:21 +05:00
|
|
|
if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD);
|
2021-06-13 13:06:19 +05:00
|
|
|
const prev = this.getQueue<T>(guild);
|
2021-06-13 13:11:54 +05:00
|
|
|
|
|
|
|
try {
|
|
|
|
prev.destroy();
|
2021-06-23 00:11:50 +05:00
|
|
|
} catch {} // eslint-disable-line no-empty
|
2021-06-17 18:22:03 +05:00
|
|
|
this.queues.delete(guild.id);
|
2021-06-13 13:11:54 +05:00
|
|
|
|
2021-06-13 13:06:19 +05:00
|
|
|
return prev;
|
|
|
|
}
|
|
|
|
|
2021-06-21 10:31:28 +05:00
|
|
|
/**
|
2021-11-05 17:53:48 +05:00
|
|
|
* @typedef {object} PlayerSearchResult
|
2021-06-21 10:31:28 +05:00
|
|
|
* @property {Playlist} [playlist] The playlist (if any)
|
|
|
|
* @property {Track[]} tracks The tracks
|
|
|
|
*/
|
2021-06-11 23:19:52 +05:00
|
|
|
/**
|
|
|
|
* Search tracks
|
|
|
|
* @param {string|Track} query The search query
|
2021-06-21 10:31:28 +05:00
|
|
|
* @param {SearchOptions} options The search options
|
2021-11-05 17:49:17 +05:00
|
|
|
* @returns {Promise<PlayerSearchResult>}
|
2021-06-11 23:19:52 +05:00
|
|
|
*/
|
2021-11-05 17:49:17 +05:00
|
|
|
async search(query: string | Track, options: SearchOptions): Promise<PlayerSearchResult> {
|
|
|
|
if (query instanceof Track) return { playlist: query.playlist || null, tracks: [query] };
|
2021-08-07 19:25:21 +05:00
|
|
|
if (!options) throw new PlayerError("DiscordPlayer#search needs search options!", ErrorStatusCode.INVALID_ARG_TYPE);
|
2021-06-17 19:01:07 +05:00
|
|
|
options.requestedBy = this.client.users.resolve(options.requestedBy);
|
2021-06-13 17:03:20 +05:00
|
|
|
if (!("searchEngine" in options)) options.searchEngine = QueryType.AUTO;
|
2021-11-05 17:53:48 +05:00
|
|
|
if (typeof options.searchEngine === "string" && this.extractors.has(options.searchEngine)) {
|
2021-11-05 17:49:17 +05:00
|
|
|
const extractor = this.extractors.get(options.searchEngine);
|
|
|
|
if (!extractor.validate(query)) return { playlist: null, tracks: [] };
|
|
|
|
const data = await extractor.handle(query);
|
|
|
|
if (data && data.data.length) {
|
|
|
|
const playlist = !data.playlist
|
|
|
|
? null
|
|
|
|
: new Playlist(this, {
|
|
|
|
...data.playlist,
|
|
|
|
tracks: []
|
|
|
|
});
|
|
|
|
|
|
|
|
const tracks = data.data.map(
|
|
|
|
(m) =>
|
|
|
|
new Track(this, {
|
|
|
|
...m,
|
|
|
|
requestedBy: options.requestedBy as User,
|
|
|
|
duration: Util.buildTimeCode(Util.parseMS(m.duration)),
|
|
|
|
playlist: playlist
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
if (playlist) playlist.tracks = tracks;
|
|
|
|
|
|
|
|
return { playlist: playlist, tracks: tracks };
|
|
|
|
}
|
|
|
|
}
|
2021-06-11 23:19:52 +05:00
|
|
|
|
2021-06-22 15:24:05 +05:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2021-06-14 18:50:36 +05:00
|
|
|
for (const [_, extractor] of this.extractors) {
|
2021-06-24 10:20:12 +05:00
|
|
|
if (options.blockExtractor) break;
|
2021-06-14 18:50:36 +05:00
|
|
|
if (!extractor.validate(query)) continue;
|
|
|
|
const data = await extractor.handle(query);
|
|
|
|
if (data && data.data.length) {
|
|
|
|
const playlist = !data.playlist
|
|
|
|
? null
|
|
|
|
: new Playlist(this, {
|
2021-06-23 00:11:50 +05:00
|
|
|
...data.playlist,
|
|
|
|
tracks: []
|
|
|
|
});
|
2021-06-14 18:50:36 +05:00
|
|
|
|
|
|
|
const tracks = data.data.map(
|
|
|
|
(m) =>
|
|
|
|
new Track(this, {
|
|
|
|
...m,
|
2021-06-17 19:01:07 +05:00
|
|
|
requestedBy: options.requestedBy as User,
|
2021-06-14 18:50:36 +05:00
|
|
|
duration: Util.buildTimeCode(Util.parseMS(m.duration)),
|
|
|
|
playlist: playlist
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
if (playlist) playlist.tracks = tracks;
|
|
|
|
|
|
|
|
return { playlist: playlist, tracks: tracks };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-13 11:40:21 +05:00
|
|
|
const qt = options.searchEngine === QueryType.AUTO ? await QueryResolver.resolve(query) : options.searchEngine;
|
2021-06-11 23:19:52 +05:00
|
|
|
switch (qt) {
|
2021-08-08 11:28:15 +05:00
|
|
|
case QueryType.YOUTUBE_VIDEO: {
|
2022-09-13 11:40:21 +05:00
|
|
|
const info = await play.video_info(query).catch(Util.noop);
|
2021-08-08 11:28:15 +05:00
|
|
|
if (!info) return { playlist: null, tracks: [] };
|
|
|
|
|
|
|
|
const track = new Track(this, {
|
2022-09-13 11:40:21 +05:00
|
|
|
title: info.video_details.title,
|
|
|
|
description: info.video_details.description,
|
|
|
|
author: info.video_details.channel?.name,
|
|
|
|
url: info.video_details.url,
|
2021-08-08 11:28:15 +05:00
|
|
|
requestedBy: options.requestedBy as User,
|
2022-09-13 11:40:21 +05:00
|
|
|
thumbnail: Util.last(info.video_details.thumbnails)?.url,
|
|
|
|
views: info.video_details.views || 0,
|
|
|
|
duration: Util.buildTimeCode(Util.parseMS(info.video_details.durationInSec * 1000)),
|
2021-08-08 11:28:15 +05:00
|
|
|
source: "youtube",
|
|
|
|
raw: info
|
|
|
|
});
|
|
|
|
|
|
|
|
return { playlist: null, tracks: [track] };
|
|
|
|
}
|
2021-06-11 23:39:21 +05:00
|
|
|
case QueryType.YOUTUBE_SEARCH: {
|
2022-09-13 11:59:19 +05:00
|
|
|
const videos = await play
|
|
|
|
.search(query, {
|
|
|
|
limit: 10,
|
|
|
|
source: { youtube: "video" }
|
|
|
|
})
|
|
|
|
.catch(Util.noop);
|
2021-06-14 11:44:32 +05:00
|
|
|
if (!videos) return { playlist: null, tracks: [] };
|
2021-06-11 23:19:52 +05:00
|
|
|
|
2022-09-13 11:59:19 +05:00
|
|
|
const tracks = videos.map((m) => {
|
2021-06-22 15:24:05 +05:00
|
|
|
(m as any).source = "youtube"; // eslint-disable-line @typescript-eslint/no-explicit-any
|
2021-06-12 00:18:53 +05:00
|
|
|
return new Track(this, {
|
|
|
|
title: m.title,
|
|
|
|
description: m.description,
|
|
|
|
author: m.channel?.name,
|
|
|
|
url: m.url,
|
2021-06-17 19:01:07 +05:00
|
|
|
requestedBy: options.requestedBy as User,
|
2022-09-13 11:40:21 +05:00
|
|
|
thumbnail: Util.last(m.thumbnails).url,
|
2021-06-12 00:18:53 +05:00
|
|
|
views: m.views,
|
2022-09-13 11:40:21 +05:00
|
|
|
duration: m.durationRaw,
|
2021-08-08 11:28:15 +05:00
|
|
|
source: "youtube",
|
2021-06-12 00:18:53 +05:00
|
|
|
raw: m
|
2021-06-11 23:39:21 +05:00
|
|
|
});
|
2021-06-12 00:18:53 +05:00
|
|
|
});
|
2021-06-13 19:31:27 +05:00
|
|
|
|
2022-09-13 11:40:21 +05:00
|
|
|
return { playlist: null, tracks, searched: true };
|
2021-06-11 23:19:52 +05:00
|
|
|
}
|
2021-06-13 17:03:20 +05:00
|
|
|
case QueryType.SOUNDCLOUD_TRACK:
|
|
|
|
case QueryType.SOUNDCLOUD_SEARCH: {
|
2022-09-13 11:59:19 +05:00
|
|
|
const result =
|
|
|
|
(await QueryResolver.resolve(query)) === QueryType.SOUNDCLOUD_TRACK
|
|
|
|
? [{ url: query }]
|
|
|
|
: await play
|
|
|
|
.search(query, {
|
|
|
|
limit: 5,
|
|
|
|
source: { soundcloud: "tracks" }
|
|
|
|
})
|
|
|
|
.catch(() => []);
|
2021-06-14 11:44:32 +05:00
|
|
|
if (!result || !result.length) return { playlist: null, tracks: [] };
|
2021-06-13 17:03:20 +05:00
|
|
|
const res: Track[] = [];
|
|
|
|
|
|
|
|
for (const r of result) {
|
2022-09-13 11:40:21 +05:00
|
|
|
const trackInfo = await play.soundcloud(r.url).catch(Util.noop);
|
2021-06-13 17:03:20 +05:00
|
|
|
if (!trackInfo) continue;
|
|
|
|
|
|
|
|
const track = new Track(this, {
|
2022-09-13 11:40:21 +05:00
|
|
|
title: trackInfo.name,
|
2021-06-13 17:03:20 +05:00
|
|
|
url: trackInfo.url,
|
2022-09-13 11:40:21 +05:00
|
|
|
duration: Util.buildTimeCode(Util.parseMS(trackInfo.durationInMs)),
|
|
|
|
description: "",
|
|
|
|
thumbnail: trackInfo.user.thumbnail,
|
|
|
|
views: 0,
|
|
|
|
author: trackInfo.user.name,
|
2021-06-13 17:03:20 +05:00
|
|
|
requestedBy: options.requestedBy,
|
|
|
|
source: "soundcloud",
|
|
|
|
engine: trackInfo
|
|
|
|
});
|
|
|
|
|
|
|
|
res.push(track);
|
|
|
|
}
|
|
|
|
|
2021-06-14 11:44:32 +05:00
|
|
|
return { playlist: null, tracks: res };
|
2021-06-13 17:03:20 +05:00
|
|
|
}
|
2021-06-13 18:09:25 +05:00
|
|
|
case QueryType.SPOTIFY_SONG: {
|
2022-07-19 18:58:01 +05:00
|
|
|
const spotifyData = await Spotify(await Util.getFetch())
|
|
|
|
.getData(query)
|
|
|
|
.catch(Util.noop);
|
2021-06-14 11:44:32 +05:00
|
|
|
if (!spotifyData) return { playlist: null, tracks: [] };
|
2021-06-13 18:09:25 +05:00
|
|
|
const spotifyTrack = new Track(this, {
|
|
|
|
title: spotifyData.name,
|
|
|
|
description: spotifyData.description ?? "",
|
|
|
|
author: spotifyData.artists[0]?.name ?? "Unknown Artist",
|
|
|
|
url: spotifyData.external_urls?.spotify ?? query,
|
|
|
|
thumbnail:
|
|
|
|
spotifyData.album?.images[0]?.url ?? spotifyData.preview_url?.length
|
|
|
|
? `https://i.scdn.co/image/${spotifyData.preview_url?.split("?cid=")[1]}`
|
|
|
|
: "https://www.scdn.co/i/_global/twitter_card-default.jpg",
|
|
|
|
duration: Util.buildTimeCode(Util.parseMS(spotifyData.duration_ms)),
|
|
|
|
views: 0,
|
|
|
|
requestedBy: options.requestedBy,
|
|
|
|
source: "spotify"
|
|
|
|
});
|
|
|
|
|
2021-06-14 11:44:32 +05:00
|
|
|
return { playlist: null, tracks: [spotifyTrack] };
|
2021-06-13 19:31:27 +05:00
|
|
|
}
|
|
|
|
case QueryType.SPOTIFY_PLAYLIST:
|
|
|
|
case QueryType.SPOTIFY_ALBUM: {
|
2022-06-15 18:10:26 +05:00
|
|
|
const spotifyPlaylist = await Spotify(await Util.getFetch())
|
|
|
|
.getData(query)
|
|
|
|
.catch(Util.noop);
|
2021-06-14 11:44:32 +05:00
|
|
|
if (!spotifyPlaylist) return { playlist: null, tracks: [] };
|
2021-06-13 19:31:27 +05:00
|
|
|
|
|
|
|
const playlist = new Playlist(this, {
|
|
|
|
title: spotifyPlaylist.name ?? spotifyPlaylist.title,
|
|
|
|
description: spotifyPlaylist.description ?? "",
|
|
|
|
thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
|
|
|
|
type: spotifyPlaylist.type,
|
|
|
|
source: "spotify",
|
|
|
|
author:
|
|
|
|
spotifyPlaylist.type !== "playlist"
|
|
|
|
? {
|
2021-06-23 00:11:50 +05:00
|
|
|
name: spotifyPlaylist.artists[0]?.name ?? "Unknown Artist",
|
|
|
|
url: spotifyPlaylist.artists[0]?.external_urls?.spotify ?? null
|
|
|
|
}
|
2021-06-13 19:31:27 +05:00
|
|
|
: {
|
2021-06-23 00:11:50 +05:00
|
|
|
name: spotifyPlaylist.owner?.display_name ?? spotifyPlaylist.owner?.id ?? "Unknown Artist",
|
|
|
|
url: spotifyPlaylist.owner?.external_urls?.spotify ?? null
|
|
|
|
},
|
2021-06-13 19:31:27 +05:00
|
|
|
tracks: [],
|
|
|
|
id: spotifyPlaylist.id,
|
2021-06-14 11:44:32 +05:00
|
|
|
url: spotifyPlaylist.external_urls?.spotify ?? query,
|
|
|
|
rawPlaylist: spotifyPlaylist
|
2021-06-13 19:31:27 +05:00
|
|
|
});
|
|
|
|
|
|
|
|
if (spotifyPlaylist.type !== "playlist") {
|
2021-06-22 15:24:05 +05:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2021-06-13 19:31:27 +05:00
|
|
|
playlist.tracks = spotifyPlaylist.tracks.items.map((m: any) => {
|
|
|
|
const data = new Track(this, {
|
|
|
|
title: m.name ?? "",
|
|
|
|
description: m.description ?? "",
|
|
|
|
author: m.artists[0]?.name ?? "Unknown Artist",
|
|
|
|
url: m.external_urls?.spotify ?? query,
|
|
|
|
thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
|
|
|
|
duration: Util.buildTimeCode(Util.parseMS(m.duration_ms)),
|
|
|
|
views: 0,
|
2021-06-17 19:01:07 +05:00
|
|
|
requestedBy: options.requestedBy as User,
|
2021-06-13 19:31:27 +05:00
|
|
|
playlist,
|
|
|
|
source: "spotify"
|
|
|
|
});
|
|
|
|
|
|
|
|
return data;
|
|
|
|
}) as Track[];
|
|
|
|
} else {
|
2021-06-22 15:24:05 +05:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2021-06-13 19:31:27 +05:00
|
|
|
playlist.tracks = spotifyPlaylist.tracks.items.map((m: any) => {
|
|
|
|
const data = new Track(this, {
|
|
|
|
title: m.track.name ?? "",
|
|
|
|
description: m.track.description ?? "",
|
2022-07-20 12:10:01 +05:00
|
|
|
author: m.track.artists?.[0]?.name ?? "Unknown Artist",
|
2021-06-13 19:31:27 +05:00
|
|
|
url: m.track.external_urls?.spotify ?? query,
|
2022-07-20 12:10:01 +05:00
|
|
|
thumbnail: m.track.album?.images?.[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
|
2021-06-13 19:31:27 +05:00
|
|
|
duration: Util.buildTimeCode(Util.parseMS(m.track.duration_ms)),
|
|
|
|
views: 0,
|
2021-06-17 19:01:07 +05:00
|
|
|
requestedBy: options.requestedBy as User,
|
2021-06-13 19:31:27 +05:00
|
|
|
playlist,
|
|
|
|
source: "spotify"
|
|
|
|
});
|
|
|
|
|
|
|
|
return data;
|
|
|
|
}) as Track[];
|
|
|
|
}
|
|
|
|
|
2021-06-14 11:44:32 +05:00
|
|
|
return { playlist: playlist, tracks: playlist.tracks };
|
|
|
|
}
|
|
|
|
case QueryType.SOUNDCLOUD_PLAYLIST: {
|
2022-09-13 11:59:19 +05:00
|
|
|
const data = (await play.soundcloud(query).catch(Util.noop)) as unknown as SoundCloudPlaylist;
|
2021-06-14 11:44:32 +05:00
|
|
|
if (!data) return { playlist: null, tracks: [] };
|
|
|
|
|
|
|
|
const res = new Playlist(this, {
|
2022-09-13 11:40:21 +05:00
|
|
|
title: data.name,
|
|
|
|
description: "",
|
|
|
|
thumbnail: "https://soundcloud.com/pwa-icon-192.png",
|
2021-06-14 11:44:32 +05:00
|
|
|
type: "playlist",
|
|
|
|
source: "soundcloud",
|
|
|
|
author: {
|
2022-09-13 11:40:21 +05:00
|
|
|
name: data.user.name ?? "Unknown Owner",
|
|
|
|
url: data.user.url
|
2021-06-14 11:44:32 +05:00
|
|
|
},
|
|
|
|
tracks: [],
|
|
|
|
id: `${data.id}`, // stringified
|
|
|
|
url: data.url,
|
|
|
|
rawPlaylist: data
|
|
|
|
});
|
|
|
|
|
2022-09-13 11:59:19 +05:00
|
|
|
const songs = await data.all_tracks();
|
2022-09-13 11:40:21 +05:00
|
|
|
for (const song of songs) {
|
2021-06-14 11:44:32 +05:00
|
|
|
const track = new Track(this, {
|
2022-09-13 11:40:21 +05:00
|
|
|
title: song.name,
|
|
|
|
description: "",
|
2022-10-31 14:15:30 +05:00
|
|
|
author: song.publisher?.name ?? "Unknown",
|
|
|
|
url: song.permalink,
|
2021-06-14 11:44:32 +05:00
|
|
|
thumbnail: song.thumbnail,
|
2022-09-13 11:40:21 +05:00
|
|
|
duration: Util.buildTimeCode(Util.parseMS(song.durationInMs)),
|
|
|
|
views: 0,
|
2021-06-14 11:44:32 +05:00
|
|
|
requestedBy: options.requestedBy,
|
|
|
|
playlist: res,
|
|
|
|
source: "soundcloud",
|
|
|
|
engine: song
|
|
|
|
});
|
|
|
|
res.tracks.push(track);
|
|
|
|
}
|
|
|
|
|
|
|
|
return { playlist: res, tracks: res.tracks };
|
|
|
|
}
|
|
|
|
case QueryType.YOUTUBE_PLAYLIST: {
|
2022-09-13 11:59:19 +05:00
|
|
|
const ytpl = (await play.playlist_info(query, { incomplete: true }).catch(Util.noop)) as unknown as YouTubePlayList;
|
2021-06-14 11:44:32 +05:00
|
|
|
if (!ytpl) return { playlist: null, tracks: [] };
|
|
|
|
|
2021-06-14 12:28:31 +05:00
|
|
|
const playlist: Playlist = new Playlist(this, {
|
2021-06-14 11:44:32 +05:00
|
|
|
title: ytpl.title,
|
2021-06-14 12:28:31 +05:00
|
|
|
thumbnail: ytpl.thumbnail as unknown as string,
|
2021-06-14 11:44:32 +05:00
|
|
|
description: "",
|
|
|
|
type: "playlist",
|
|
|
|
source: "youtube",
|
|
|
|
author: {
|
|
|
|
name: ytpl.channel.name,
|
|
|
|
url: ytpl.channel.url
|
|
|
|
},
|
|
|
|
tracks: [],
|
|
|
|
id: ytpl.id,
|
|
|
|
url: ytpl.url,
|
|
|
|
rawPlaylist: ytpl
|
|
|
|
});
|
|
|
|
|
2022-09-13 11:59:19 +05:00
|
|
|
const videos = await ytpl.all_videos();
|
|
|
|
playlist.tracks = videos.map(
|
|
|
|
(video) =>
|
|
|
|
new Track(this, {
|
|
|
|
title: video.title,
|
|
|
|
description: video.description,
|
|
|
|
author: video.channel?.name,
|
|
|
|
url: video.url,
|
|
|
|
requestedBy: options.requestedBy as User,
|
|
|
|
thumbnail: Util.last(video.thumbnails).url,
|
|
|
|
views: video.views,
|
|
|
|
duration: video.durationRaw,
|
|
|
|
raw: video,
|
|
|
|
playlist: playlist,
|
|
|
|
source: "youtube"
|
|
|
|
})
|
|
|
|
);
|
2021-06-14 11:48:32 +05:00
|
|
|
|
|
|
|
return { playlist: playlist, tracks: playlist.tracks };
|
2021-06-13 18:09:25 +05:00
|
|
|
}
|
2021-06-11 23:39:21 +05:00
|
|
|
default:
|
2021-06-14 11:44:32 +05:00
|
|
|
return { playlist: null, tracks: [] };
|
2021-06-11 23:19:52 +05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-20 19:22:09 +05:00
|
|
|
/**
|
2021-06-21 10:31:28 +05:00
|
|
|
* Registers extractor
|
2021-06-20 19:22:09 +05:00
|
|
|
* @param {string} extractorName The extractor name
|
|
|
|
* @param {ExtractorModel|any} extractor The extractor object
|
2021-06-20 19:54:02 +05:00
|
|
|
* @param {boolean} [force=false] Overwrite existing extractor with this name (if available)
|
2021-06-20 19:22:09 +05:00
|
|
|
* @returns {ExtractorModel}
|
|
|
|
*/
|
2021-06-22 15:24:05 +05:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2021-06-17 23:17:55 +05:00
|
|
|
use(extractorName: string, extractor: ExtractorModel | any, force = false): ExtractorModel {
|
2021-08-07 19:25:21 +05:00
|
|
|
if (!extractorName) throw new PlayerError("Cannot use unknown extractor!", ErrorStatusCode.UNKNOWN_EXTRACTOR);
|
2021-06-17 23:17:55 +05:00
|
|
|
if (this.extractors.has(extractorName) && !force) return this.extractors.get(extractorName);
|
2021-06-14 18:50:36 +05:00
|
|
|
if (extractor instanceof ExtractorModel) {
|
|
|
|
this.extractors.set(extractorName, extractor);
|
2021-06-17 23:17:55 +05:00
|
|
|
return extractor;
|
2021-06-14 18:50:36 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
for (const method of ["validate", "getInfo"]) {
|
2021-08-07 19:25:21 +05:00
|
|
|
if (typeof extractor[method] !== "function") throw new PlayerError("Invalid extractor data!", ErrorStatusCode.INVALID_EXTRACTOR);
|
2021-06-14 18:50:36 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
const model = new ExtractorModel(extractorName, extractor);
|
|
|
|
this.extractors.set(model.name, model);
|
|
|
|
|
2021-06-17 23:17:55 +05:00
|
|
|
return model;
|
2021-06-14 18:50:36 +05:00
|
|
|
}
|
|
|
|
|
2021-06-20 19:22:09 +05:00
|
|
|
/**
|
|
|
|
* Removes registered extractor
|
|
|
|
* @param {string} extractorName The extractor name
|
|
|
|
* @returns {ExtractorModel}
|
|
|
|
*/
|
2021-06-14 18:50:36 +05:00
|
|
|
unuse(extractorName: string) {
|
2021-08-07 19:25:21 +05:00
|
|
|
if (!this.extractors.has(extractorName)) throw new PlayerError(`Cannot find extractor "${extractorName}"`, ErrorStatusCode.UNKNOWN_EXTRACTOR);
|
2021-06-17 23:17:55 +05:00
|
|
|
const prev = this.extractors.get(extractorName);
|
2021-06-14 18:50:36 +05:00
|
|
|
this.extractors.delete(extractorName);
|
2021-06-17 23:17:55 +05:00
|
|
|
return prev;
|
2021-06-14 18:50:36 +05:00
|
|
|
}
|
|
|
|
|
2021-06-20 19:22:09 +05:00
|
|
|
/**
|
|
|
|
* Generates a report of the dependencies used by the `@discordjs/voice` module. Useful for debugging.
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
2021-06-19 18:12:58 +05:00
|
|
|
scanDeps() {
|
2021-11-28 13:09:07 +05:00
|
|
|
const line = "-".repeat(50);
|
|
|
|
const depsReport = generateDependencyReport();
|
2021-11-28 13:25:27 +05:00
|
|
|
const extractorReport = this.extractors
|
|
|
|
.map((m) => {
|
|
|
|
return `${m.name} :: ${m.version || "0.1.0"}`;
|
|
|
|
})
|
|
|
|
.join("\n");
|
2021-11-28 13:09:07 +05:00
|
|
|
return `${depsReport}\n${line}\nLoaded Extractors:\n${extractorReport || "None"}`;
|
2021-06-19 18:12:58 +05:00
|
|
|
}
|
|
|
|
|
2021-11-28 13:32:46 +05:00
|
|
|
emit<U extends keyof PlayerEvents>(eventName: U, ...args: Parameters<PlayerEvents[U]>): boolean {
|
2021-11-28 13:25:27 +05:00
|
|
|
if (this.requiredEvents.includes(eventName) && !super.eventNames().includes(eventName)) {
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
console.error(...args);
|
2021-11-28 13:32:46 +05:00
|
|
|
process.emitWarning(`[DiscordPlayerWarning] Unhandled "${eventName}" event! Events ${this.requiredEvents.map((m) => `"${m}"`).join(", ")} must have event listeners!`);
|
2021-11-28 13:25:27 +05:00
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
return super.emit(eventName, ...args);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-04 18:35:40 +05:00
|
|
|
/**
|
2021-08-09 19:56:40 +05:00
|
|
|
* Resolves queue
|
2021-07-04 18:35:40 +05:00
|
|
|
* @param {GuildResolvable|Queue} queueLike Queue like object
|
|
|
|
* @returns {Queue}
|
|
|
|
*/
|
|
|
|
resolveQueue<T>(queueLike: GuildResolvable | Queue): Queue<T> {
|
|
|
|
return this.getQueue(queueLike instanceof Queue ? queueLike.guild : queueLike);
|
|
|
|
}
|
|
|
|
|
2021-06-11 19:57:49 +05:00
|
|
|
*[Symbol.iterator]() {
|
|
|
|
yield* Array.from(this.queues.values());
|
|
|
|
}
|
2022-07-21 17:08:11 +05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates `Playlist` instance
|
|
|
|
* @param data The data to initialize a playlist
|
|
|
|
*/
|
|
|
|
createPlaylist(data: PlaylistInitData) {
|
|
|
|
return new Playlist(this, data);
|
|
|
|
}
|
2021-06-11 15:32:22 +05:00
|
|
|
}
|
|
|
|
|
2021-08-05 14:05:42 +05:00
|
|
|
export { Player };
|