feat: extractors implementation
This commit is contained in:
parent
7f34fbd786
commit
62bf57ac74
7 changed files with 124 additions and 44 deletions
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in a new issue