From 1e45ebc54ca920a045482f1e45b3e2a65d8b65e5 Mon Sep 17 00:00:00 2001 From: Snowflake107 Date: Fri, 11 Jun 2021 16:17:22 +0545 Subject: [PATCH] feat(Structures): basic setup --- src/Player.ts | 29 +++++- src/Structures/ExtractorModel.ts | 70 +++++++++++++- src/Structures/Queue.ts | 56 ++++++++++- src/Structures/Track.ts | 156 ++++++++++++++++++++++++++++++- src/types/types.ts | 64 +++++++++++++ 5 files changed, 371 insertions(+), 4 deletions(-) diff --git a/src/Player.ts b/src/Player.ts index cb0ff5c..917bcbf 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -1 +1,28 @@ -export {}; +import { Client, Collection, Guild, Snowflake } from "discord.js"; +import { TypedEmitter as EventEmitter } from "tiny-typed-emitter"; +import { Queue } from "./Structures/Queue"; +import { PlayerOptions } from "./types/types"; + +class DiscordPlayer extends EventEmitter { + public readonly client: Client; + public readonly queues = new Collection(); + + constructor(client: Client) { + super(); + this.client = client; + } + + createQueue(guild: Guild, queueInitOptions?: PlayerOptions) { + if (this.queues.has(guild.id)) return this.queues.get(guild.id); + const queue = new Queue(this, guild, queueInitOptions); + this.queues.set(guild.id, queue); + + return queue; + } + + getQueue(guild: Snowflake) { + return this.queues.get(guild); + } +} + +export { DiscordPlayer as Player }; diff --git a/src/Structures/ExtractorModel.ts b/src/Structures/ExtractorModel.ts index cb0ff5c..dbbdae7 100644 --- a/src/Structures/ExtractorModel.ts +++ b/src/Structures/ExtractorModel.ts @@ -1 +1,69 @@ -export {}; +import { ExtractorModelData } from "../types/types"; + +class ExtractorModel { + name: string; + private _raw: any; + + /** + * Model for raw Discord Player extractors + * @param {String} extractorName Name of the extractor + * @param {Object} data Extractor object + */ + constructor(extractorName: string, data: any) { + /** + * The extractor name + * @type {String} + */ + this.name = extractorName; + + Object.defineProperty(this, "_raw", { value: data, configurable: false, writable: false, enumerable: false }); + } + + /** + * Method to handle requests from `Player.play()` + * @param {String} query Query to handle + * @returns {Promise} + */ + async handle(query: string): Promise { + const data = await this._raw.getInfo(query); + if (!data) return null; + + return { + title: data.title, + duration: data.duration, + thumbnail: data.thumbnail, + engine: data.engine, + views: data.views, + author: data.author, + description: data.description, + url: data.url + }; + } + + /** + * Method used by Discord Player to validate query with this extractor + * @param {String} query The query to validate + * @returns {Boolean} + */ + validate(query: string): boolean { + return Boolean(this._raw.validate(query)); + } + + /** + * The extractor version + * @type {String} + */ + get version(): string { + return this._raw.version ?? "0.0.0"; + } + + /** + * If player should mark this extractor as important + * @type {Boolean} + */ + get important(): boolean { + return Boolean(this._raw.important); + } +} + +export { ExtractorModel }; diff --git a/src/Structures/Queue.ts b/src/Structures/Queue.ts index cb0ff5c..abe7073 100644 --- a/src/Structures/Queue.ts +++ b/src/Structures/Queue.ts @@ -1 +1,55 @@ -export {}; +import { Guild, StageChannel, VoiceChannel } from "discord.js"; +import { Player } from "../Player"; +import { VoiceUtils } from "../VoiceInterface/VoiceUtils"; +import { VoiceSubscription } from "../VoiceInterface/VoiceSubscription"; +import Track from "./Track"; +import { PlayerOptions } from "../types/types"; + +class Queue { + public readonly guild: Guild; + public readonly player: Player; + public voiceConnection: VoiceSubscription; + public tracks: Track[] = []; + public options: PlayerOptions; + + constructor(player: Player, guild: Guild, options: PlayerOptions = {}) { + this.player = player; + this.guild = guild; + this.options = {}; + + Object.assign( + this.options, + { + leaveOnEnd: true, + leaveOnEndCooldown: 1000, + leaveOnStop: true, + leaveOnEmpty: true, + leaveOnEmptyCooldown: 1000, + autoSelfDeaf: true, + enableLive: false, + ytdlDownloadOptions: {}, + useSafeSearch: false, + disableAutoRegister: false, + fetchBeforeQueued: false + } as PlayerOptions, + options + ); + } + + async joinVoiceChannel(channel: StageChannel | VoiceChannel) { + if (!["stage", "voice"].includes(channel.type)) + throw new TypeError(`Channel type must be voice or stage, got ${channel.type}!`); + const connection = await VoiceUtils.connect(channel); + this.voiceConnection = connection; + + return this; + } + + destroy() { + this.voiceConnection.stop(); + this.voiceConnection.disconnect(); + this.player.queues.delete(this.guild.id); + } +} + +export { Queue }; diff --git a/src/Structures/Track.ts b/src/Structures/Track.ts index cb0ff5c..ee5e79e 100644 --- a/src/Structures/Track.ts +++ b/src/Structures/Track.ts @@ -1 +1,155 @@ -export {}; +import { User } from "discord.js"; +import { Player } from "../Player"; +import { RawTrackData } from "../types/types"; +import { Queue } from "./Queue"; + +class Track { + public player!: Player; + public title!: string; + public description!: string; + public author!: string; + public url!: string; + public thumbnail!: string; + public duration!: string; + public views!: number; + public requestedBy!: User; + public fromPlaylist!: boolean; + public raw!: RawTrackData; + + /** + * Track constructor + * @param {Player} player The player that instantiated this Track + * @param {RawTrackData} data Track data + */ + constructor(player: Player, data: RawTrackData) { + /** + * The player that instantiated this Track + * @name Track#player + * @type {Player} + * @readonly + */ + Object.defineProperty(this, "player", { value: player, enumerable: false }); + + /** + * Title of this track + * @name Track#title + * @type {String} + */ + + /** + * Description of this track + * @name Track#description + * @type {String} + */ + + /** + * Author of this track + * @name Track#author + * @type {String} + */ + + /** + * URL of this track + * @name Track#url + * @type {String} + */ + + /** + * Thumbnail of this track + * @name Track#thumbnail + * @type {String} + */ + + /** + * Duration of this track + * @name Track#duration + * @type {String} + */ + + /** + * Views count of this track + * @name Track#views + * @type {Number} + */ + + /** + * Person who requested this track + * @name Track#requestedBy + * @type {DiscordUser} + */ + + /** + * If this track belongs to playlist + * @name Track#fromPlaylist + * @type {Boolean} + */ + + /** + * Raw track data + * @name Track#raw + * @type {RawTrackData} + */ + + void this._patch(data); + } + + private _patch(data: RawTrackData) { + this.title = data.title ?? ""; + this.author = data.author ?? ""; + this.url = data.url ?? ""; + this.thumbnail = data.thumbnail ?? ""; + this.duration = data.duration ?? ""; + this.views = data.views ?? 0; + this.requestedBy = data.requestedBy; + this.fromPlaylist = Boolean(data.fromPlaylist); + + // raw + Object.defineProperty(this, "raw", { get: () => data, enumerable: false }); + } + + /** + * The queue in which this track is located + * @type {Queue} + */ + get queue(): Queue { + return this.player.queues.find((q) => q.tracks.includes(this)); + } + + /** + * The track duration in millisecond + * @type {Number} + */ + get durationMS(): number { + const times = (n: number, t: number) => { + let tn = 1; + for (let i = 0; i < t; i++) tn *= n; + return t <= 0 ? 1000 : tn * 1000; + }; + + return this.duration + .split(":") + .reverse() + .map((m, i) => parseInt(m) * times(60, i)) + .reduce((a, c) => a + c, 0); + } + + /** + * Returns source of this track + * @type {TrackSource} + */ + get source() { + return this.raw.source ?? "arbitrary"; + } + + /** + * String representation of this track + * @returns {String} + */ + toString(): string { + return `${this.title} by ${this.author}`; + } +} + +export default Track; + +export { Track }; diff --git a/src/types/types.ts b/src/types/types.ts index fc657f8..bb00c05 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,3 +1,7 @@ +import { User } from "discord.js"; +import { downloadOptions } from "ytdl-core"; +import { Readable, Duplex } from "stream"; + export type FiltersName = keyof QueueFilters; export type QueueFilters = { @@ -30,3 +34,63 @@ export type QueueFilters = { chorus3d?: boolean; fadein?: boolean; }; + +export type TrackSource = "soundcloud" | "youtube" | "spotify" | "arbitrary"; + +export interface RawTrackData { + title: string; + description: string; + author: string; + url: string; + thumbnail: string; + duration: string; + views: number; + requestedBy: User; + fromPlaylist: boolean; + source?: TrackSource; + engine?: any; + live?: boolean; +} + +export interface TimeData { + days: number; + hours: number; + minutes: number; + seconds: number; +} + +export interface PlayerProgressbarOptions { + timecodes?: boolean; + queue?: boolean; + length?: number; + line?: string; + indicator?: string; +} + +export interface PlayerOptions { + leaveOnEnd?: boolean; + leaveOnEndCooldown?: number; + leaveOnStop?: boolean; + leaveOnEmpty?: boolean; + leaveOnEmptyCooldown?: number; + autoSelfDeaf?: boolean; + enableLive?: boolean; + ytdlDownloadOptions?: downloadOptions; + useSafeSearch?: boolean; + disableAutoRegister?: boolean; + fetchBeforeQueued?: boolean; +} + +export interface ExtractorModelData { + title: string; + duration: number; + thumbnail: string; + engine: string | Readable | Duplex; + views: number; + author: string; + description: string; + url: string; + version?: string; + important?: boolean; + source?: TrackSource; +}