Skip to content

Commit

Permalink
Merge pull request #1716 from Androz2091/types/extractor
Browse files Browse the repository at this point in the history
feat(ExtractorExecutionContext): infer ext initialization options
  • Loading branch information
twlite authored Apr 14, 2023
2 parents bd4e49d + 63fff29 commit 4ec7367
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 39 deletions.
4 changes: 2 additions & 2 deletions packages/discord-player/src/extractors/BaseExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends object = object> {
/**
* Identifier for this extractor
*/
Expand All @@ -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<string, unknown> = {}) {}
public constructor(public context: ExtractorExecutionContext, public options: T = <T>{}) {}

/**
* Identifier of this extractor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class ExtractorExecutionContext extends PlayerEventsEmitter<ExtractorExec

knownExtractorKeys.forEach((key) => {
if (!mod.module[key]) return;
this.register(mod.module[key]);
this.register(<typeof BaseExtractor>mod.module[key], {});
});

return { success: true, error: null };
Expand Down Expand Up @@ -98,7 +98,7 @@ export class ExtractorExecutionContext extends PlayerEventsEmitter<ExtractorExec
* @param _extractor The extractor to register
* @param options Options supplied to the extractor
*/
public async register(_extractor: typeof BaseExtractor, options: Record<string, unknown> = {}) {
public async register<O extends object, T extends typeof BaseExtractor<O>>(_extractor: T, options: ConstructorParameters<T>['1']) {
if (typeof _extractor.identifier !== 'string' || this.store.has(_extractor.identifier)) return;
const extractor = new _extractor(this, options);

Expand Down Expand Up @@ -154,12 +154,12 @@ export class ExtractorExecutionContext extends PlayerEventsEmitter<ExtractorExec
* @param fn The runner function
* @param filterBlocked Filter blocked extractors
*/
public async run<T = unknown>(fn: ExtractorExecutionFN<T>, filterBlocked = true) {
public async run<T = unknown, E extends Record<string, unknown> = Record<string, unknown>>(fn: ExtractorExecutionFN<T>, 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<E>).catch((e: Error) => {
this.player.debug(`Extractor ${ext.identifier} failed with error: ${e}`);
return false;
});
Expand All @@ -181,4 +181,5 @@ export interface ExtractorExecutionResult<T = unknown> {
extractor: BaseExtractor;
result: T;
}

export type ExtractorExecutionFN<T = unknown> = (extractor: BaseExtractor) => Promise<T | boolean>;
35 changes: 27 additions & 8 deletions packages/extractor/src/extractors/AppleMusicExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Readable | string>;
}

export class AppleMusicExtractor extends BaseExtractor<AppleMusicExtractorInit> {
public static identifier = 'com.discord-player.applemusicextractor' as const;
private _stream!: StreamFN;
private _isYtdl = false;

public async activate(): Promise<void> {
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<boolean> {
Expand Down Expand Up @@ -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);
Expand Down
43 changes: 32 additions & 11 deletions packages/extractor/src/extractors/SpotifyExtractor.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
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';
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<Readable | string>;
}

export class SpotifyExtractor extends BaseExtractor<SpotifyExtractorInit> {
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<void> {
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();
}

Expand Down Expand Up @@ -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);
Expand Down
31 changes: 17 additions & 14 deletions packages/extractor/src/extractors/YoutubeExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Readable | string>;
}

export class YoutubeExtractor extends BaseExtractor<YoutubeExtractorInit> {
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;
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit 4ec7367

Please sign in to comment.