feat: extractors implementation

This commit is contained in:
Snowflake107 2021-06-14 19:35:36 +05:45
parent 7f34fbd786
commit 62bf57ac74
7 changed files with 124 additions and 44 deletions

View file

@ -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 { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
import { Queue } from "./Structures/Queue"; import { Queue } from "./Structures/Queue";
import { VoiceUtils } from "./VoiceInterface/VoiceUtils"; 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 Track from "./Structures/Track";
import { QueryResolver } from "./utils/QueryResolver"; import { QueryResolver } from "./utils/QueryResolver";
import YouTube from "youtube-sr"; import YouTube from "youtube-sr";
@ -11,19 +11,24 @@ import Spotify from "spotify-url-info";
// @ts-ignore // @ts-ignore
import { Client as SoundCloud } from "soundcloud-scraper"; import { Client as SoundCloud } from "soundcloud-scraper";
import { Playlist } from "./Structures/Playlist"; import { Playlist } from "./Structures/Playlist";
import { ExtractorModel } from "./Structures/ExtractorModel";
const soundcloud = new SoundCloud(); const soundcloud = new SoundCloud();
class DiscordPlayer extends EventEmitter<PlayerEvents> { class DiscordPlayer extends EventEmitter<PlayerEvents> {
public readonly client: Client; public readonly client: Client;
public readonly options: DiscordPlayerInitOptions = {
autoRegisterExtractor: true
};
public readonly queues = new Collection<Snowflake, Queue>(); public readonly queues = new Collection<Snowflake, Queue>();
public readonly voiceUtils = new VoiceUtils(); public readonly voiceUtils = new VoiceUtils();
public readonly extractors = new Collection<string, ExtractorModel>();
/** /**
* Creates new Discord Player * Creates new Discord Player
* @param {Discord.Client} client The Discord Client * @param {Discord.Client} client The Discord Client
*/ */
constructor(client: Client) { constructor(client: Client, options: DiscordPlayerInitOptions = {}) {
super(); super();
/** /**
@ -32,7 +37,21 @@ class DiscordPlayer extends EventEmitter<PlayerEvents> {
*/ */
this.client = client; this.client = client;
/**
* The extractors collection
* @type {ExtractorModel}
*/
this.options = Object.assign(this.options, options);
this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this)); 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 { private _handleVoiceState(oldState: VoiceState, newState: VoiceState): void {
@ -120,7 +139,33 @@ class DiscordPlayer extends EventEmitter<PlayerEvents> {
if (!options) throw new Error("DiscordPlayer#search needs search options!"); if (!options) throw new Error("DiscordPlayer#search needs search options!");
if (!("searchEngine" in options)) options.searchEngine = QueryType.AUTO; 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; const qt = options.searchEngine === QueryType.AUTO ? QueryResolver.resolve(query) : options.searchEngine;
switch (qt) { switch (qt) {
case QueryType.YOUTUBE_SEARCH: { case QueryType.YOUTUBE_SEARCH: {
@ -345,6 +390,29 @@ class DiscordPlayer extends EventEmitter<PlayerEvents> {
} }
} }
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]() { *[Symbol.iterator]() {
yield* Array.from(this.queues.values()); yield* Array.from(this.queues.values());
} }

View file

@ -29,14 +29,18 @@ class ExtractorModel {
if (!data) return null; if (!data) return null;
return { return {
title: data.title, playlist: data.playlist ?? null,
duration: data.duration, data:
thumbnail: data.thumbnail, data.info?.map((m: any) => ({
engine: data.engine, title: m.title,
views: data.views, duration: m.duration,
author: data.author, thumbnail: m.thumbnail,
description: data.description, engine: m.engine,
url: data.url views: m.views,
author: m.author,
description: m.description,
url: m.url
})) ?? []
}; };
} }
@ -56,14 +60,6 @@ class ExtractorModel {
get version(): string { get version(): string {
return this._raw.version ?? "0.0.0"; 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 }; export { ExtractorModel };

View file

@ -14,7 +14,6 @@ import { StageChannel, VoiceChannel } from "discord.js";
import { Duplex, Readable } from "stream"; import { Duplex, Readable } from "stream";
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter"; import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
import Track from "../Structures/Track"; import Track from "../Structures/Track";
import PlayerError from "../utils/PlayerError";
export interface VoiceEvents { export interface VoiceEvents {
error: (error: AudioPlayerError) => any; error: (error: AudioPlayerError) => any;
@ -130,7 +129,7 @@ class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
* @param {AudioResource} resource The audio resource to play * @param {AudioResource} resource The audio resource to play
*/ */
async playStream(resource: AudioResource<Track> = this.audioResource) { async playStream(resource: AudioResource<Track> = 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.audioResource) this.audioResource = resource;
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000); if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000);
this.audioPlayer.play(resource); this.audioPlayer.play(resource);

View file

@ -4,6 +4,5 @@ export { Playlist } from "./Structures/Playlist";
export { Player } from "./Player"; export { Player } from "./Player";
export { Queue } from "./Structures/Queue"; export { Queue } from "./Structures/Queue";
export { Track } from "./Structures/Track"; export { Track } from "./Structures/Track";
export { PlayerError } from "./utils/PlayerError";
export { VoiceUtils } from "./VoiceInterface/VoiceUtils"; export { VoiceUtils } from "./VoiceInterface/VoiceUtils";
export { VoiceEvents, StreamDispatcher } from "./VoiceInterface/BasicStreamDispatcher"; export { VoiceEvents, StreamDispatcher } from "./VoiceInterface/BasicStreamDispatcher";

View file

@ -88,17 +88,33 @@ export interface PlayerOptions {
} }
export interface ExtractorModelData { export interface ExtractorModelData {
title: string; playlist?: {
duration: number; title: string;
thumbnail: string; description: string;
engine: string | Readable | Duplex; thumbnail: string;
views: number; type: "album" | "playlist";
author: string; source: TrackSource;
description: string; author: {
url: string; name: string;
version?: string; url: string;
important?: boolean; };
source?: TrackSource; 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 { export enum QueryType {
@ -226,3 +242,7 @@ export interface PlaylistJSON {
}; };
tracks: TrackJSON[]; tracks: TrackJSON[];
} }
export interface DiscordPlayerInitOptions {
autoRegisterExtractor?: boolean;
}

View file

@ -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 };

View file

@ -39,6 +39,14 @@ class Util {
static isVoiceEmpty(channel: VoiceChannel | StageChannel) { static isVoiceEmpty(channel: VoiceChannel | StageChannel) {
return channel.members.filter((member) => !member.user.bot).size === 0; return channel.members.filter((member) => !member.user.bot).size === 0;
} }
static require(id: string) {
try {
return require(id);
} catch {
return null;
}
}
} }
export { Util }; export { Util };