diff --git a/packages/discord-player/src/extractors/BaseExtractor.ts b/packages/discord-player/src/extractors/BaseExtractor.ts index c62f708a6..669202fe1 100644 --- a/packages/discord-player/src/extractors/BaseExtractor.ts +++ b/packages/discord-player/src/extractors/BaseExtractor.ts @@ -6,7 +6,7 @@ import { PlayerEvents, SearchQueryType } from '../types/types'; import { ExtractorExecutionContext } from './ExtractorExecutionContext'; import type { RequestOptions } from 'http'; -export class BaseExtractor { +export class BaseExtractor { /** * Identifier for this extractor */ @@ -17,7 +17,7 @@ export class BaseExtractor { * @param context Context that instantiated this extractor * @param options Initialization options for this extractor */ - public constructor(public context: ExtractorExecutionContext, public options: Record = {}) {} + public constructor(public context: ExtractorExecutionContext, public options: T = {}) {} /** * Identifier of this extractor diff --git a/packages/discord-player/src/extractors/ExtractorExecutionContext.ts b/packages/discord-player/src/extractors/ExtractorExecutionContext.ts index a86722d7f..5aae960ca 100644 --- a/packages/discord-player/src/extractors/ExtractorExecutionContext.ts +++ b/packages/discord-player/src/extractors/ExtractorExecutionContext.ts @@ -64,7 +64,7 @@ export class ExtractorExecutionContext extends PlayerEventsEmitter { if (!mod.module[key]) return; - this.register(mod.module[key]); + this.register(mod.module[key], {}); }); return { success: true, error: null }; @@ -98,7 +98,7 @@ export class ExtractorExecutionContext extends PlayerEventsEmitter = {}) { + public async register>(_extractor: T, options: ConstructorParameters['1']) { if (typeof _extractor.identifier !== 'string' || this.store.has(_extractor.identifier)) return; const extractor = new _extractor(this, options); @@ -154,12 +154,12 @@ export class ExtractorExecutionContext extends PlayerEventsEmitter(fn: ExtractorExecutionFN, filterBlocked = true) { + public async run = Record>(fn: ExtractorExecutionFN, filterBlocked = true) { const blocked = this.player.options.blockExtractors ?? []; for (const ext of this.store.values()) { if (filterBlocked && blocked.some((e) => e === ext.identifier)) continue; this.player.debug(`Executing extractor ${ext.identifier}...`); - const result = await fn(ext).catch((e: Error) => { + const result = await fn(ext as BaseExtractor).catch((e: Error) => { this.player.debug(`Extractor ${ext.identifier} failed with error: ${e}`); return false; }); @@ -181,4 +181,5 @@ export interface ExtractorExecutionResult { extractor: BaseExtractor; result: T; } + export type ExtractorExecutionFN = (extractor: BaseExtractor) => Promise; diff --git a/packages/extractor/src/extractors/AppleMusicExtractor.ts b/packages/extractor/src/extractors/AppleMusicExtractor.ts index 5166f978b..d96ca5767 100644 --- a/packages/extractor/src/extractors/AppleMusicExtractor.ts +++ b/packages/extractor/src/extractors/AppleMusicExtractor.ts @@ -4,13 +4,30 @@ import { Readable } from 'stream'; import { YoutubeExtractor } from './YoutubeExtractor'; import { StreamFN, loadYtdl, makeYTSearch } from './common/helper'; -export class AppleMusicExtractor extends BaseExtractor { +export interface AppleMusicExtractorInit { + createStream?: (ext: AppleMusicExtractor, url: string) => Promise; +} + +export class AppleMusicExtractor extends BaseExtractor { public static identifier = 'com.discord-player.applemusicextractor' as const; private _stream!: StreamFN; + private _isYtdl = false; public async activate(): Promise { + const fn = this.options.createStream; + + if (typeof fn === 'function') { + this._isYtdl = false; + this._stream = (q: string) => { + return fn(this, q); + }; + + return; + } + const lib = await loadYtdl(this.context.player.options.ytdlOptions); this._stream = lib.stream; + this._isYtdl = true; } public async validate(query: string, type?: SearchQueryType | null | undefined): Promise { @@ -187,13 +204,15 @@ export class AppleMusicExtractor extends BaseExtractor { let url = info.url; - if (YoutubeExtractor.validateURL(info.raw.url)) url = info.raw.url; - else { - const _url = await makeYTSearch(`${info.title} ${info.author}`, 'video') - .then((r) => r[0].url) - .catch(Util.noop); - if (!_url) throw new Error(`Could not extract stream for this track`); - info.raw.url = url = _url; + if (this._isYtdl) { + if (YoutubeExtractor.validateURL(info.raw.url)) url = info.raw.url; + else { + const _url = await makeYTSearch(`${info.title} ${info.author}`, 'video') + .then((r) => r[0].url) + .catch(Util.noop); + if (!_url) throw new Error(`Could not extract stream for this track`); + info.raw.url = url = _url; + } } return this._stream(url); diff --git a/packages/extractor/src/extractors/SpotifyExtractor.ts b/packages/extractor/src/extractors/SpotifyExtractor.ts index 31a44df09..ba60c6570 100644 --- a/packages/extractor/src/extractors/SpotifyExtractor.ts +++ b/packages/extractor/src/extractors/SpotifyExtractor.ts @@ -1,5 +1,5 @@ import { BaseExtractor, ExtractorInfo, ExtractorSearchContext, Playlist, QueryType, SearchQueryType, Track, Util } from 'discord-player'; -import { Readable } from 'stream'; +import type { Readable } from 'stream'; import { YoutubeExtractor } from './YoutubeExtractor'; import { StreamFN, getFetch, loadYtdl, makeYTSearch } from './common/helper'; import spotify, { Spotify, SpotifyAlbum, SpotifyPlaylist, SpotifySong } from 'spotify-url-info'; @@ -7,20 +7,39 @@ import { SpotifyAPI } from '../internal'; const re = /^(?:https:\/\/open\.spotify\.com\/(?:user\/[A-Za-z0-9]+\/)?|spotify:)(album|playlist|track)(?:[/:])([A-Za-z0-9]+).*$/; -export class SpotifyExtractor extends BaseExtractor { +export interface SpotifyExtractorInit { + clientId?: string | null; + clientSecret?: string | null; + createStream?: (ext: SpotifyExtractor, url: string) => Promise; +} + +export class SpotifyExtractor extends BaseExtractor { public static identifier = 'com.discord-player.spotifyextractor' as const; private _stream!: StreamFN; + private _isYtdl = false; private _lib!: Spotify; private _credentials = { - clientId: process.env.DP_SPOTIFY_CLIENT_ID || null, - clientSecret: process.env.DP_SPOTIFY_CLIENT_SECRET || null + clientId: this.options.clientId || process.env.DP_SPOTIFY_CLIENT_ID || null, + clientSecret: this.options.clientSecret || process.env.DP_SPOTIFY_CLIENT_SECRET || null }; public internal = new SpotifyAPI(this._credentials); public async activate(): Promise { + const fn = this.options.createStream; + + if (typeof fn === 'function') { + this._isYtdl = false; + this._stream = (q: string) => { + return fn(this, q); + }; + + return; + } + const lib = await loadYtdl(this.context.player.options.ytdlOptions); this._stream = lib.stream; this._lib = spotify(getFetch); + this._isYtdl = true; if (this.internal.isTokenExpired()) await this.internal.requestToken(); } @@ -270,13 +289,15 @@ export class SpotifyExtractor extends BaseExtractor { let url = info.url; - if (YoutubeExtractor.validateURL(info.raw.url)) url = info.raw.url; - else { - const _url = await makeYTSearch(`${info.title} ${info.author}`, 'video') - .then((r) => r[0].url) - .catch(Util.noop); - if (!_url) throw new Error(`Could not extract stream for this track`); - info.raw.url = url = _url; + if (this._isYtdl) { + if (YoutubeExtractor.validateURL(info.raw.url)) url = info.raw.url; + else { + const _url = await makeYTSearch(`${info.title} ${info.author}`, 'video') + .then((r) => r[0].url) + .catch(Util.noop); + if (!_url) throw new Error(`Could not extract stream for this track`); + info.raw.url = url = _url; + } } return this._stream(url); diff --git a/packages/extractor/src/extractors/YoutubeExtractor.ts b/packages/extractor/src/extractors/YoutubeExtractor.ts index f61e74bfd..fb72a6fc8 100644 --- a/packages/extractor/src/extractors/YoutubeExtractor.ts +++ b/packages/extractor/src/extractors/YoutubeExtractor.ts @@ -13,18 +13,33 @@ import { } from 'discord-player'; import { StreamFN, YouTubeLibs, loadYtdl, makeYTSearch } from './common/helper'; +import type { Readable } from 'stream'; // taken from ytdl-core const validQueryDomains = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'music.youtube.com', 'gaming.youtube.com']); const validPathDomains = /^https?:\/\/(youtu\.be\/|(www\.)?youtube\.com\/(embed|v|shorts)\/)/; const idRegex = /^[a-zA-Z0-9-_]{11}$/; -export class YoutubeExtractor extends BaseExtractor { +export interface YoutubeExtractorInit { + createStream?: (ext: YoutubeExtractor, url: string) => Promise; +} + +export class YoutubeExtractor extends BaseExtractor { public static identifier = 'com.discord-player.youtubeextractor' as const; private _stream!: StreamFN; public _ytLibName!: string; public async activate() { + const fn = this.options.createStream; + + if (typeof fn === 'function') { + this._stream = (q: string) => { + return fn(this, q); + }; + + return; + } + const { stream, name } = await loadYtdl(this.context.player.options.ytdlOptions); this._stream = stream; this._ytLibName = name; @@ -208,19 +223,7 @@ export class YoutubeExtractor extends BaseExtractor { } let url = info.url; - - if (info.queryType === 'spotifySong' || info.queryType === 'appleMusicSong') { - if (YoutubeExtractor.validateURL(info.raw.url)) url = info.raw.url; - else { - const _url = await YouTube.searchOne(`${info.title} ${info.author}`, 'video') - .then((r) => r.url) - .catch(Util.noop); - if (!_url) throw new Error(`Could not extract stream for this track`); - info.raw.url = url = _url; - } - } - - if (url) url = url.includes('youtube.com') ? url.replace(/(m(usic)?|gaming)\./, '') : url; + url = url.includes('youtube.com') ? url.replace(/(m(usic)?|gaming)\./, '') : url; return this._stream(url); }