rough
This commit is contained in:
parent
c1dacb7843
commit
b8eec6dbd4
7 changed files with 226 additions and 26 deletions
|
@ -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",
|
||||
|
|
169
src/Player.ts
169
src/Player.ts
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -20,6 +20,7 @@ export enum PlayerEvents {
|
|||
};
|
||||
|
||||
export enum PlayerErrorEventCodes {
|
||||
DEFAULT = 'PlayerError',
|
||||
LIVE_VIDEO = 'LiveVideo',
|
||||
NOT_CONNECTED = 'NotConnected',
|
||||
UNABLE_TO_JOIN = 'UnableToJoin',
|
||||
|
|
|
@ -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 };
|
||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue