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 { 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<PlayerEvents> {
public readonly client: Client;
public readonly options: DiscordPlayerInitOptions = {
autoRegisterExtractor: true
};
public readonly queues = new Collection<Snowflake, Queue>();
public readonly voiceUtils = new VoiceUtils();
public readonly extractors = new Collection<string, ExtractorModel>();
/**
* 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<PlayerEvents> {
*/
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<PlayerEvents> {
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<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]() {
yield* Array.from(this.queues.values());
}

View file

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

View file

@ -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<VoiceEvents> {
* @param {AudioResource} resource The audio resource to play
*/
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.voiceConnection.state.status !== VoiceConnectionStatus.Ready) await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000);
this.audioPlayer.play(resource);

View file

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

View file

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

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) {
return channel.members.filter((member) => !member.user.bot).size === 0;
}
static require(id: string) {
try {
return require(id);
} catch {
return null;
}
}
}
export { Util };