This commit is contained in:
Snowflake107 2021-05-10 10:43:37 +05:45
parent c1dacb7843
commit b8eec6dbd4
7 changed files with 226 additions and 26 deletions

View file

@ -65,7 +65,7 @@
"@discordjs/opus": "^0.5.0",
"@types/node": "^14.14.41",
"@types/ws": "^7.4.1",
"discord.js": "^12.5.3",
"discord.js": "discordjs/discord.js",
"discord.js-docgen": "discordjs/docgen#ts-patch",
"jsdoc-babel": "^0.5.0",
"prettier": "^2.2.1",

View file

@ -1,13 +1,19 @@
import { EventEmitter } from 'events';
import { Client, Collection, Snowflake, Message } from 'discord.js';
import { Client, Collection, Snowflake, Message, Collector } from 'discord.js';
import Util from './utils/Util';
import Queue from './Structures/Queue';
import Track from './Structures/Track';
import PlayerError from './utils/PlayerError';
import { ExtractorModel } from './Structures/ExtractorModel';
import ytdl from 'discord-ytdl-core';
import { PlayerEvents, PlayerErrorEventCodes } from './utils/Constants';
export class Player extends EventEmitter {
public client: Client;
public queues = new Collection<Snowflake, Queue>();
public Extractors = new Collection<string, ExtractorModel>();
private _cooldownsTimeout = new Collection<string, NodeJS.Timeout>();
private _resultsCollectors = new Collection<string, Collector<Snowflake, Message>>();
constructor(client: Client) {
super();
@ -21,18 +27,171 @@ export class Player extends EventEmitter {
}
public createQueue(message: Message) {
if (this.queues.has(message.guild.id)) return this.queues.get(message.guild.id);
return new Promise<Queue>((resolve) => {
if (this.queues.has(message.guild.id)) return this.queues.get(message.guild.id);
const channel = message.member.voice?.channel;
if (!channel) return void this.emit(
PlayerEvents.ERROR,
new PlayerError('Voice connection is not available in this server!', PlayerErrorEventCodes.NOT_CONNECTED, message)
);
const queue = new Queue(this, message);
void this.queues.set(message.guild.id, queue);
return queue;
const queue = new Queue(this, message.guild);
void this.queues.set(message.guild.id, queue);
channel
.join()
.then((connection) => {
this.emit(PlayerEvents.CONNECTION_CREATE, message, connection);
queue.voiceConnection = connection;
if (queue.options.setSelfDeaf) connection.voice.setSelfDeaf(true);
this.emit(PlayerEvents.QUEUE_CREATE, message, queue);
resolve(queue);
})
.catch((err) => {
this.queues.delete(message.guild.id);
this.emit(
PlayerEvents.ERROR,
new PlayerError(err.message ?? err, PlayerErrorEventCodes.UNABLE_TO_JOIN, message)
);
});
return queue;
})
}
public getQueue(message: Message) {
return this.queues.get(message.guild.id) ?? null;
}
async play(message: Message, query: string | Track, firstResult?: boolean): Promise<void> {
if (!message) throw new PlayerError('Play function needs message');
if (!query) throw new PlayerError('Play function needs search query as a string or Player.Track object');
if (this._cooldownsTimeout.has(`end_${message.guild.id}`)) {
clearTimeout(this._cooldownsTimeout.get(`end_${message.guild.id}`));
this._cooldownsTimeout.delete(`end_${message.guild.id}`);
}
if (typeof query === 'string') query = query.replace(/<(.+)>/g, '$1');
let track;
const queue = this.getQueue(message);
if (query instanceof Track) track = query;
else {
if (ytdl.validateURL(query)) {
const info = await ytdl.getBasicInfo(query).catch(() => { });
if (!info) return void this.emit(PlayerEvents.NO_RESULTS, message, query);
if (info.videoDetails.isLiveContent && !queue.options.enableLive)
return void this.emit(
PlayerEvents.ERROR,
new PlayerError('Live video is not enabled!', PlayerErrorEventCodes.LIVE_VIDEO, message)
);
const lastThumbnail = info.videoDetails.thumbnails[info.videoDetails.thumbnails.length - 1];
track = new Track(this, {
title: info.videoDetails.title,
description: info.videoDetails.description,
author: info.videoDetails.author.name,
url: info.videoDetails.video_url,
thumbnail: lastThumbnail.url,
duration: Util.buildTimeCode(Util.parseMS(parseInt(info.videoDetails.lengthSeconds) * 1000)),
views: parseInt(info.videoDetails.viewCount),
requestedBy: message.author,
fromPlaylist: false,
source: 'youtube',
live: Boolean(info.videoDetails.isLiveContent)
});
} else {
for (const [_, extractor] of this.Extractors) {
if (extractor.validate(query)) {
const data = await extractor.handle(query);
if (data) {
track = new Track(this, {
title: data.title,
description: data.description,
duration: Util.buildTimeCode(Util.parseMS(data.duration)),
thumbnail: data.thumbnail,
author: data.author,
views: data.views,
engine: data.engine,
source: 'arbitrary',
fromPlaylist: false,
requestedBy: message.author,
url: data.url
});
if (extractor.important) break;
}
}
}
if (!track) track = await this.searchTracks(message, query, firstResult);
}
}
if (track) {
if (queue) {
const q = queue.addTrack(track);
this.emit(PlayerEvents.TRACK_ADD, message, q, q.tracks[q.tracks.length - 1]);
} else {
const q = queue.addTrack(track);
if (q) this.emit(PlayerEvents.TRACK_START, message, q.tracks[0], q);
// todo: start playing
}
}
}
private searchTracks(message: Message, query: string, firstResult?: boolean): Promise<Track> {
return new Promise(async (resolve) => {
let tracks: Track[] = [];
const queryType = Util.getQueryType(query);
switch (queryType) {
default:
tracks = await Util.ytSearch(query, { user: message.author, player: this });
}
if (tracks.length < 1) return void this.emit(PlayerEvents.NO_RESULTS, message, query);
if (firstResult || tracks.length === 1) return resolve(tracks[0]);
const collectorString = `${message.author.id}-${message.channel.id}`;
const currentCollector = this._resultsCollectors.get(collectorString);
if (currentCollector) currentCollector.stop();
const collector = message.channel.createMessageCollector((m) => m.author.id === message.author.id, {
time: 60000
});
this._resultsCollectors.set(collectorString, collector);
this.emit(PlayerEvents.SEARCH_RESULTS, message, query, tracks, collector);
collector.on('collect', ({ content }) => {
if (content === 'cancel') {
collector.stop();
return this.emit(PlayerEvents.SEARCH_CANCEL, message, query, tracks);
}
if (!isNaN(content) && parseInt(content) >= 1 && parseInt(content) <= tracks.length) {
const index = parseInt(content, 10);
const track = tracks[index - 1];
collector.stop();
resolve(track);
} else {
this.emit(PlayerEvents.SEARCH_INVALID_RESPONSE, message, query, tracks, content, collector);
}
});
collector.on('end', (_, reason) => {
if (reason === 'time') {
this.emit(PlayerEvents.SEARCH_CANCEL, message, query, tracks);
}
});
});
}
}
export default Player;

View file

@ -1,10 +1,41 @@
import { Guild, Message, VoiceConnection } from 'discord.js';
import { Player } from '../Player';
import { PlayerOptions } from '../types/types';
import Track from './Track';
import { PlayerError } from '../utils/PlayerError';
export class Queue {
public readonly player: Player;
player: Player;
guild: Guild;
firstMessage: Message;
options: PlayerOptions = {};
tracks: Track[] = [];
voiceConnection: VoiceConnection = null;
constructor(player: Player, data: any) {
constructor(player: Player, guild: Guild) {
Object.defineProperty(this, 'player', { value: player, enumerable: false });
this.guild = guild;
}
get playing() {
return this.tracks[0];
}
async play(message: Message, query: string | Track, firstResult?: boolean) {
return await this.player.play(message, query, firstResult);
}
addTrack(track: Track) {
if (!track || !(track instanceof Track)) throw new PlayerError('No track specified to add to the queue');
this.tracks.push(track);
return this;
}
addTracks(tracks: Track[]) {
this.tracks.push(...tracks);
return this;
}
}

View file

@ -1,7 +1,9 @@
import { Message } from 'discord.js';
import { Player } from '../Player';
export class Track {
public readonly player: Player;
readonly player: Player;
readonly message: Message;
constructor(player: Player, data: any) {
Object.defineProperty(this, 'player', { value: player, enumerable: false });

View file

@ -20,6 +20,7 @@ export enum PlayerEvents {
};
export enum PlayerErrorEventCodes {
DEFAULT = 'PlayerError',
LIVE_VIDEO = 'LiveVideo',
NOT_CONNECTED = 'NotConnected',
UNABLE_TO_JOIN = 'UnableToJoin',

View file

@ -1,10 +1,19 @@
import { Message } from "discord.js";
export default class PlayerError extends Error {
constructor(msg: string, name?: string) {
discordMessage: Message;
constructor(msg: string, name?: string, message?: Message) {
super();
this.name = name ?? 'PlayerError';
this.message = msg;
this.discordMessage = message;
Error.captureStackTrace(this);
}
get code() {
return this.name;
}
}
export { PlayerError };

View file

@ -1802,19 +1802,17 @@ discord.js-docgen@discordjs/docgen#ts-patch:
tsubaki "^1.3.2"
yargs "^14.0.0"
discord.js@^12.5.3:
version "12.5.3"
resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-12.5.3.tgz#56820d473c24320871df9ea0bbc6b462f21cf85c"
integrity sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==
discord.js@discordjs/discord.js:
version "12.5.0"
resolved "https://codeload.github.com/discordjs/discord.js/tar.gz/0e40f9b86826ba50aa3840807fb86e1bce6b1c3d"
dependencies:
"@discordjs/collection" "^0.1.6"
"@discordjs/form-data" "^3.0.1"
abort-controller "^3.0.0"
node-fetch "^2.6.1"
prism-media "^1.2.9"
setimmediate "^1.0.5"
prism-media "^1.2.2"
tweetnacl "^1.0.3"
ws "^7.4.4"
ws "^7.3.1"
dmd@^4.0.5:
version "4.0.6"
@ -3476,7 +3474,7 @@ prettier@^2.2.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
prism-media@^1.2.7, prism-media@^1.2.9:
prism-media@^1.2.2, prism-media@^1.2.7:
version "1.2.9"
resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.2.9.tgz#8d4f97b36efdfc82483eb8d3db64020767866f36"
integrity sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q==
@ -3834,11 +3832,6 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -4572,6 +4565,11 @@ write@1.0.3:
dependencies:
mkdirp "^0.5.1"
ws@^7.3.1:
version "7.4.5"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1"
integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==
ws@^7.4.4:
version "7.4.4"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"