diff --git a/src/Player.ts b/src/Player.ts index a9da7fb..cfebf98 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -1,8 +1,8 @@ -import { Client, Collection, Guild, Snowflake, User, VoiceState } from "discord.js"; +import { Client, Collection, Guild, Snowflake, VoiceState } from "discord.js"; import { TypedEmitter as EventEmitter } from "tiny-typed-emitter"; import { Queue } from "./Structures/Queue"; import { VoiceUtils } from "./VoiceInterface/VoiceUtils"; -import { PlayerEvents, PlayerOptions, QueryType, SearchOptions } from "./types/types"; +import { PlayerEvents, PlayerOptions, QueryType, SearchOptions, DiscordPlayerInitOptions } from "./types/types"; import Track from "./Structures/Track"; import { QueryResolver } from "./utils/QueryResolver"; import YouTube from "youtube-sr"; @@ -11,19 +11,24 @@ import Spotify from "spotify-url-info"; // @ts-ignore import { Client as SoundCloud } from "soundcloud-scraper"; import { Playlist } from "./Structures/Playlist"; +import { ExtractorModel } from "./Structures/ExtractorModel"; const soundcloud = new SoundCloud(); class DiscordPlayer extends EventEmitter { public readonly client: Client; + public readonly options: DiscordPlayerInitOptions = { + autoRegisterExtractor: true + }; public readonly queues = new Collection(); public readonly voiceUtils = new VoiceUtils(); + public readonly extractors = new Collection(); /** * Creates new Discord Player * @param {Discord.Client} client The Discord Client */ - constructor(client: Client) { + constructor(client: Client, options: DiscordPlayerInitOptions = {}) { super(); /** @@ -32,7 +37,21 @@ class DiscordPlayer extends EventEmitter { */ this.client = client; + /** + * The extractors collection + * @type {ExtractorModel} + */ + this.options = Object.assign(this.options, options); + this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this)); + + if (this.options?.autoRegisterExtractor) { + let nv: any; + + if ((nv = Util.require("@discord-player/extractor"))) { + ["Attachment", "Facebook", "Reverbnation", "Vimeo"].forEach((ext) => void this.use(ext, nv[ext])); + } + } } private _handleVoiceState(oldState: VoiceState, newState: VoiceState): void { @@ -120,7 +139,33 @@ class DiscordPlayer extends EventEmitter { if (!options) throw new Error("DiscordPlayer#search needs search options!"); if (!("searchEngine" in options)) options.searchEngine = QueryType.AUTO; - // @todo: add extractors + for (const [_, extractor] of this.extractors) { + if (!extractor.validate(query)) continue; + 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, + duration: Util.buildTimeCode(Util.parseMS(m.duration)), + playlist: playlist + }) + ); + + if (playlist) playlist.tracks = tracks; + + return { playlist: playlist, tracks: tracks }; + } + } + const qt = options.searchEngine === QueryType.AUTO ? QueryResolver.resolve(query) : options.searchEngine; switch (qt) { case QueryType.YOUTUBE_SEARCH: { @@ -345,6 +390,29 @@ class DiscordPlayer extends EventEmitter { } } + use(extractorName: string, extractor: ExtractorModel | any, force = false) { + if (!extractorName) throw new Error("Cannot use unknown extractor!"); + if (this.extractors.has(extractorName) && !force) return this; + if (extractor instanceof ExtractorModel) { + this.extractors.set(extractorName, extractor); + return this; + } + + for (const method of ["validate", "getInfo"]) { + if (typeof extractor[method] !== "function") throw new Error("Invalid extractor data!"); + } + + const model = new ExtractorModel(extractorName, extractor); + this.extractors.set(model.name, model); + + return this; + } + + unuse(extractorName: string) { + if (!this.extractors.has(extractorName)) throw new Error(`Cannot find extractor "${extractorName}"`); + this.extractors.delete(extractorName); + } + *[Symbol.iterator]() { yield* Array.from(this.queues.values()); } diff --git a/src/Structures/ExtractorModel.ts b/src/Structures/ExtractorModel.ts index 183747f..9b0ad5e 100644 --- a/src/Structures/ExtractorModel.ts +++ b/src/Structures/ExtractorModel.ts @@ -29,14 +29,18 @@ class ExtractorModel { 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 + playlist: data.playlist ?? null, + data: + data.info?.map((m: any) => ({ + title: m.title, + duration: m.duration, + thumbnail: m.thumbnail, + engine: m.engine, + views: m.views, + author: m.author, + description: m.description, + url: m.url + })) ?? [] }; } @@ -56,14 +60,6 @@ class ExtractorModel { 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/VoiceInterface/BasicStreamDispatcher.ts b/src/VoiceInterface/BasicStreamDispatcher.ts index 0d4e3aa..cabd202 100644 --- a/src/VoiceInterface/BasicStreamDispatcher.ts +++ b/src/VoiceInterface/BasicStreamDispatcher.ts @@ -14,7 +14,6 @@ import { StageChannel, VoiceChannel } from "discord.js"; import { Duplex, Readable } from "stream"; import { TypedEmitter as EventEmitter } from "tiny-typed-emitter"; import Track from "../Structures/Track"; -import PlayerError from "../utils/PlayerError"; export interface VoiceEvents { error: (error: AudioPlayerError) => any; @@ -130,7 +129,7 @@ class BasicStreamDispatcher extends EventEmitter { * @param {AudioResource} resource The audio resource to play */ async playStream(resource: AudioResource = this.audioResource) { - if (!resource) throw new PlayerError("Audio resource is not available!"); + if (!resource) throw new Error("Audio resource is not available!"); if (!this.audioResource) this.audioResource = resource; if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000); this.audioPlayer.play(resource); diff --git a/src/index.ts b/src/index.ts index cdbeb79..1d5c948 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,5 @@ export { Playlist } from "./Structures/Playlist"; export { Player } from "./Player"; export { Queue } from "./Structures/Queue"; export { Track } from "./Structures/Track"; -export { PlayerError } from "./utils/PlayerError"; export { VoiceUtils } from "./VoiceInterface/VoiceUtils"; export { VoiceEvents, StreamDispatcher } from "./VoiceInterface/BasicStreamDispatcher"; diff --git a/src/types/types.ts b/src/types/types.ts index 7a67ce9..eb5248f 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -88,17 +88,33 @@ export interface PlayerOptions { } 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; + playlist?: { + title: string; + description: string; + thumbnail: string; + type: "album" | "playlist"; + source: TrackSource; + author: { + name: string; + url: string; + }; + id: string; + url: string; + rawPlaylist?: any; + }; + data: { + title: string; + duration: number; + thumbnail: string; + engine: string | Readable | Duplex; + views: number; + author: string; + description: string; + url: string; + version?: string; + important?: boolean; + source?: TrackSource; + }[]; } export enum QueryType { @@ -226,3 +242,7 @@ export interface PlaylistJSON { }; tracks: TrackJSON[]; } + +export interface DiscordPlayerInitOptions { + autoRegisterExtractor?: boolean; +} diff --git a/src/utils/PlayerError.ts b/src/utils/PlayerError.ts deleted file mode 100644 index af8110d..0000000 --- a/src/utils/PlayerError.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default class PlayerError extends Error { - constructor(msg: string, name?: string) { - super(); - this.name = name ?? "PlayerError"; - this.message = msg; - Error.captureStackTrace(this); - } -} - -export { PlayerError }; diff --git a/src/utils/Util.ts b/src/utils/Util.ts index 8baff32..6489b6e 100644 --- a/src/utils/Util.ts +++ b/src/utils/Util.ts @@ -39,6 +39,14 @@ class Util { static isVoiceEmpty(channel: VoiceChannel | StageChannel) { return channel.members.filter((member) => !member.user.bot).size === 0; } + + static require(id: string) { + try { + return require(id); + } catch { + return null; + } + } } export { Util };