Merge branch 'v5'

This commit is contained in:
Snowflake107 2021-06-11 21:03:54 +05:45
commit 04bd807050
18 changed files with 1107 additions and 2839 deletions

View file

@ -1,6 +1,7 @@
{
"printWidth": 120,
"trailingComma": "none",
"singleQuote": true,
"tabWidth": 4
"singleQuote": false,
"tabWidth": 4,
"semi": true
}

View file

@ -5,6 +5,8 @@ Complete framework to facilitate music commands using **[discord.js](https://dis
[![versionBadge](https://img.shields.io/npm/v/discord-player?style=for-the-badge)](https://npmjs.com/discord-player)
[![discordBadge](https://img.shields.io/discord/558328638911545423?style=for-the-badge&color=7289da)](https://androz2091.fr/discord)
> V5 WIP
## Installation
### Install **[discord-player](https://npmjs.com/package/discord-player)**

View file

@ -1,6 +1,6 @@
{
"name": "discord-player",
"version": "4.0.5",
"version": "5.0.0-dev",
"description": "Complete framework to facilitate music commands using discord.js",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -8,7 +8,7 @@
"lib/"
],
"scripts": {
"test": "yarn build && cd test && node index.js",
"test": "cd test && ts-node index.ts",
"build": "tsc",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "tslint -p tsconfig.json",
@ -50,11 +50,13 @@
},
"homepage": "https://github.com/Androz2091/discord-player#readme",
"dependencies": {
"discord-ytdl-core": "^5.0.2",
"soundcloud-scraper": "^4.0.3",
"spotify-url-info": "^2.2.0",
"youtube-sr": "^4.0.4",
"ytdl-core": "^4.5.0"
"@discordjs/voice": "^0.3.1",
"discord-ytdl-core": "^5.0.3",
"soundcloud-scraper": "^5.0.0",
"spotify-url-info": "^2.2.3",
"tiny-typed-emitter": "^2.0.3",
"youtube-sr": "^4.1.4",
"ytdl-core": "^4.8.2"
},
"devDependencies": {
"@babel/cli": "^7.13.16",
@ -65,12 +67,18 @@
"@discordjs/opus": "^0.5.0",
"@types/node": "^14.14.41",
"@types/ws": "^7.4.1",
"discord.js": "^12.5.3",
"discord-api-types": "^0.18.1",
"discord.js": "^13.0.0-dev.f5f3f772865ee98bbb44df938e0e71f9f8865c10",
"discord.js-docgen": "discordjs/docgen#ts-patch",
"jsdoc-babel": "^0.5.0",
"prettier": "^2.2.1",
"ts-node": "^10.0.0",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescipt": "^1.0.0",
"typescript": "^4.2.3"
},
"optionalDependencies": {
"sodium": "^3.0.2"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import { ExtractorModelData } from '../types/types';
import { ExtractorModelData } from "../types/types";
class ExtractorModel {
name: string;
@ -16,7 +16,7 @@ class ExtractorModel {
*/
this.name = extractorName;
Object.defineProperty(this, '_raw', { value: data, configurable: false, writable: false, enumerable: false });
Object.defineProperty(this, "_raw", { value: data, configurable: false, writable: false, enumerable: false });
}
/**
@ -54,7 +54,7 @@ class ExtractorModel {
* @type {String}
*/
get version(): string {
return this._raw.version ?? '0.0.0';
return this._raw.version ?? "0.0.0";
}
/**
@ -66,5 +66,4 @@ class ExtractorModel {
}
}
export default ExtractorModel;
export { ExtractorModel };

View file

@ -0,0 +1,18 @@
import { Player } from "../Player";
import { Track } from "./Track";
class Playlist {
public readonly player: Player;
public tracks: Track[];
constructor(player: Player, tracks: Track[]) {
this.player = player;
this.tracks = tracks ?? [];
}
*[Symbol.iterator]() {
yield* this.tracks;
}
}
export { Playlist };

View file

@ -1,204 +1,66 @@
import { Message, Snowflake, VoiceConnection } from 'discord.js';
import AudioFilters from '../utils/AudioFilters';
import { Player } from '../Player';
import { EventEmitter } from 'events';
import { Track } from './Track';
import { QueueFilters } from '../types/types';
import { Guild, StageChannel, VoiceChannel } from "discord.js";
import { Player } from "../Player";
import { VoiceSubscription } from "../VoiceInterface/VoiceSubscription";
import Track from "./Track";
import { PlayerOptions } from "../types/types";
export class Queue extends EventEmitter {
public player!: Player;
public guildID: Snowflake;
public voiceConnection?: VoiceConnection;
public stream?: any;
public tracks: Track[];
public previousTracks: Track[];
public stopped: boolean;
public lastSkipped: boolean;
public volume: number;
public paused: boolean;
public repeatMode: boolean;
public loopMode: boolean;
public filters: QueueFilters;
public additionalStreamTime: number;
public firstMessage: Message;
class Queue {
public readonly guild: Guild;
public readonly player: Player;
public voiceConnection: VoiceSubscription;
public tracks: Track[] = [];
public options: PlayerOptions;
/**
* If autoplay is enabled in this queue
* @type {boolean}
*/
public autoPlay = false;
constructor(player: Player, guild: Guild, options: PlayerOptions = {}) {
this.player = player;
this.guild = guild;
this.options = {};
/**
* Queue constructor
* @param {Player} player The player that instantiated this Queue
* @param {DiscordMessage} message The message object
*/
constructor(player: Player, message: Message) {
super();
/**
* The player that instantiated this Queue
* @name Queue#player
* @type {Player}
* @readonly
*/
Object.defineProperty(this, 'player', { value: player, enumerable: false });
/**
* ID of the guild assigned to this queue
* @type {DiscordSnowflake}
*/
this.guildID = message.guild.id;
/**
* The voice connection of this queue
* @type {DiscordVoiceConnection}
*/
this.voiceConnection = null;
/**
* Tracks of this queue
* @type {Track[]}
*/
this.tracks = [];
/**
* Previous tracks of this queue
* @type {Track[]}
*/
this.previousTracks = [];
/**
* If the player of this queue is stopped
* @type {boolean}
*/
this.stopped = false;
/**
* If last track was skipped
* @type {boolean}
*/
this.lastSkipped = false;
/**
* Queue volume
* @type {Number}
*/
this.volume = 100;
/**
* If the player of this queue is paused
* @type {boolean}
*/
this.paused = Boolean(this.voiceConnection?.dispatcher?.paused);
/**
* If repeat mode is enabled in this queue
* @type {boolean}
*/
this.repeatMode = false;
/**
* If loop mode is enabled in this queue
* @type {boolean}
*/
this.loopMode = false;
/**
* The additional calculated stream time
* @type {Number}
*/
this.additionalStreamTime = 0;
/**
* The initial message object
* @type {DiscordMessage}
*/
this.firstMessage = message;
/**
* The audio filters in this queue
* @type {QueueFilters}
*/
this.filters = {};
Object.keys(AudioFilters).forEach((fn) => {
this.filters[fn as keyof QueueFilters] = false;
});
Object.assign(
this.options,
{
leaveOnEnd: true,
leaveOnEndCooldown: 1000,
leaveOnStop: true,
leaveOnEmpty: true,
leaveOnEmptyCooldown: 1000,
autoSelfDeaf: true,
enableLive: false,
ytdlDownloadOptions: {},
useSafeSearch: false,
disableAutoRegister: false,
fetchBeforeQueued: false
} as PlayerOptions,
options
);
}
/**
* Currently playing track
* @type {Track}
*/
get playing(): Track {
return this.tracks[0];
get current() {
return this.voiceConnection.audioResource?.metadata ?? this.tracks[0];
}
/**
* Calculated volume of this queue
* @type {Number}
*/
get calculatedVolume(): number {
return this.filters.normalizer ? this.volume + 70 : this.volume;
async joinVoiceChannel(channel: StageChannel | VoiceChannel) {
if (!["stage", "voice"].includes(channel.type))
throw new TypeError(`Channel type must be voice or stage, got ${channel.type}!`);
const connection = await this.player.voiceUtils.connect(channel);
this.voiceConnection = connection;
return this;
}
/**
* Total duration
* @type {Number}
*/
get totalTime(): number {
return this.tracks.length > 0 ? this.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0;
destroy() {
this.voiceConnection.stop();
this.voiceConnection.disconnect();
this.player.queues.delete(this.guild.id);
}
/**
* Current stream time
* @type {Number}
*/
get currentStreamTime(): number {
return this.voiceConnection?.dispatcher?.streamTime + this.additionalStreamTime || 0;
play() {
throw new Error("Not implemented");
}
/**
* Sets audio filters in this player
* @param {QueueFilters} filters Audio filters to set
* @type {Promise<void>}
*/
setFilters(filters: QueueFilters): Promise<void> {
return this.player.setFilters(this.firstMessage, filters);
}
/**
* Returns array of all enabled filters
* @type {String[]}
*/
getFiltersEnabled(): string[] {
const filters: string[] = [];
for (const filter in this.filters) {
if (this.filters[filter as keyof QueueFilters] !== false) filters.push(filter);
}
return filters;
}
/**
* Returns all disabled filters
* @type {String[]}
*/
getFiltersDisabled(): string[] {
const enabled = this.getFiltersEnabled();
return Object.keys(this.filters).filter((f) => !enabled.includes(f));
}
/**
* String representation of this Queue
* @returns {String}
*/
toString(): string {
return `<Queue ${this.guildID}>`;
*[Symbol.iterator]() {
yield* this.tracks;
}
}
export default Queue;
export { Queue };

View file

@ -1,9 +1,9 @@
import { Player } from '../Player';
import { User } from 'discord.js';
import { TrackData } from '../types/types';
import Queue from './Queue';
import { User } from "discord.js";
import { Player } from "../Player";
import { RawTrackData } from "../types/types";
import { Queue } from "./Queue";
export class Track {
class Track {
public player!: Player;
public title!: string;
public description!: string;
@ -14,21 +14,21 @@ export class Track {
public views!: number;
public requestedBy!: User;
public fromPlaylist!: boolean;
public raw!: TrackData;
public raw!: RawTrackData;
/**
* Track constructor
* @param {Player} player The player that instantiated this Track
* @param {TrackData} data Track data
* @param {RawTrackData} data Track data
*/
constructor(player: Player, data: TrackData) {
constructor(player: Player, data: RawTrackData) {
/**
* The player that instantiated this Track
* @name Track#player
* @type {Player}
* @readonly
*/
Object.defineProperty(this, 'player', { value: player, enumerable: false });
Object.defineProperty(this, "player", { value: player, enumerable: false });
/**
* Title of this track
@ -87,25 +87,24 @@ export class Track {
/**
* Raw track data
* @name Track#raw
* @type {TrackData}
* @type {RawTrackData}
*/
void this._patch(data);
}
private _patch(data: TrackData) {
this.title = data.title ?? '';
this.description = data.description ?? '';
this.author = data.author ?? '';
this.url = data.url ?? '';
this.thumbnail = data.thumbnail ?? '';
this.duration = data.duration ?? '';
private _patch(data: RawTrackData) {
this.title = data.title ?? "";
this.author = data.author ?? "";
this.url = data.url ?? "";
this.thumbnail = data.thumbnail ?? "";
this.duration = data.duration ?? "";
this.views = data.views ?? 0;
this.requestedBy = data.requestedBy;
this.fromPlaylist = Boolean(data.fromPlaylist);
// raw
Object.defineProperty(this, 'raw', { get: () => data, enumerable: false });
Object.defineProperty(this, "raw", { get: () => data, enumerable: false });
}
/**
@ -128,12 +127,20 @@ export class Track {
};
return this.duration
.split(':')
.split(":")
.reverse()
.map((m, i) => parseInt(m) * times(60, i))
.reduce((a, c) => a + c, 0);
}
/**
* Returns source of this track
* @type {TrackSource}
*/
get source() {
return this.raw.source ?? "arbitrary";
}
/**
* String representation of this track
* @returns {String}
@ -144,3 +151,5 @@ export class Track {
}
export default Track;
export { Track };

View file

@ -0,0 +1,141 @@
import {
AudioPlayer,
AudioPlayerError,
AudioPlayerStatus,
AudioResource,
createAudioPlayer,
createAudioResource,
entersState,
StreamType,
VoiceConnection,
VoiceConnectionStatus
} from "@discordjs/voice";
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;
debug: (message: string) => any;
start: () => any;
finish: () => any;
}
class VoiceSubscription extends EventEmitter<VoiceEvents> {
public readonly voiceConnection: VoiceConnection;
public readonly audioPlayer: AudioPlayer;
public connectPromise?: Promise<void>;
public audioResource?: AudioResource<Track>;
constructor(connection: VoiceConnection) {
super();
this.voiceConnection = connection;
this.audioPlayer = createAudioPlayer();
this.voiceConnection.on("stateChange", (_, newState) => {
if (newState.status === VoiceConnectionStatus.Disconnected) {
if (this.voiceConnection.reconnectAttempts < 5) {
setTimeout(() => {
if (this.voiceConnection.state.status === VoiceConnectionStatus.Disconnected) {
this.voiceConnection.reconnect();
}
}, (this.voiceConnection.reconnectAttempts + 1) * 5000).unref();
} else {
this.voiceConnection.destroy();
}
} else if (newState.status === VoiceConnectionStatus.Destroyed) {
this.stop();
} else if (
!this.connectPromise &&
(newState.status === VoiceConnectionStatus.Connecting ||
newState.status === VoiceConnectionStatus.Signalling)
) {
this.connectPromise = entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000)
.then(() => undefined)
.catch(() => {
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed)
this.voiceConnection.destroy();
})
.finally(() => (this.connectPromise = undefined));
}
});
this.audioPlayer.on("stateChange", (oldState, newState) => {
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
void this.emit("finish");
} else if (newState.status === AudioPlayerStatus.Playing) {
void this.emit("start");
}
});
this.audioPlayer.on("debug", (m) => void this.emit("debug", m));
this.audioPlayer.on("error", (error) => void this.emit("error", error));
this.voiceConnection.subscribe(this.audioPlayer);
}
/**
* Creates stream
* @param {Readable|Duplex|string} src The stream source
* @param {({type?:StreamType;data?:any;inlineVolume?:boolean})} [ops] Options
* @returns {AudioResource}
*/
createStream(src: Readable | Duplex | string, ops?: { type?: StreamType; data?: any; inlineVolume?: boolean }) {
this.audioResource = createAudioResource(src, {
inputType: ops?.type ?? StreamType.Arbitrary,
metadata: ops?.data,
inlineVolume: Boolean(ops?.inlineVolume)
});
return this.audioResource;
}
/**
* The player status
*/
get status() {
return this.audioPlayer.state.status;
}
/**
* Disconnects from voice
*/
disconnect() {
this.voiceConnection.destroy();
}
/**
* Stops the player
*/
stop() {
this.audioPlayer.stop();
}
pause(interpolateSilence?: boolean) {
return this.audioPlayer.pause(interpolateSilence);
}
resume() {
return this.audioPlayer.unpause();
}
/**
* Play stream
* @param {AudioResource} resource The audio resource to play
*/
playStream(resource: AudioResource<Track> = this.audioResource) {
if (!resource) throw new PlayerError("Audio resource is not available!");
if (!this.audioResource && resource) this.audioResource = resource;
this.audioPlayer.play(resource);
return this;
}
get streamTime() {
if (!this.audioResource) return 0;
return this.audioResource.playbackDuration;
}
}
export { VoiceSubscription };

View file

@ -0,0 +1,53 @@
import { VoiceChannel, StageChannel, Collection, Snowflake } from "discord.js";
import { entersState, joinVoiceChannel, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice";
import { VoiceSubscription } from "./VoiceSubscription";
class VoiceUtils {
public cache = new Collection<Snowflake, VoiceSubscription>();
/**
* Joins a voice channel
* @param {StageChannel|VoiceChannel} channel The voice channel
* @param {({deaf?: boolean;maxTime?: number;})} [options] Join options
* @returns {Promise<VoiceSubscription>}
*/
public async connect(
channel: VoiceChannel | StageChannel,
options?: {
deaf?: boolean;
maxTime?: number;
}
): Promise<VoiceSubscription> {
let conn = joinVoiceChannel({
guildId: channel.guild.id,
channelId: channel.id,
adapterCreator: channel.guild.voiceAdapterCreator,
selfDeaf: Boolean(options?.deaf)
});
try {
conn = await entersState(conn, VoiceConnectionStatus.Ready, options?.maxTime ?? 20000);
const sub = new VoiceSubscription(conn);
this.cache.set(channel.guild.id, sub);
return sub;
} catch (err) {
conn.destroy();
throw err;
}
}
/**
* Disconnects voice connection
* @param {VoiceConnection} connection The voice connection
*/
public disconnect(connection: VoiceConnection | VoiceSubscription) {
if (connection instanceof VoiceSubscription) return connection.voiceConnection.destroy();
return connection.destroy();
}
public getConnection(guild: Snowflake) {
return this.cache.get(guild);
}
}
export { VoiceUtils };

View file

@ -1,9 +1,4 @@
export { AudioFilters } from './utils/AudioFilters';
export * as Constants from './utils/Constants';
export { ExtractorModel } from './Structures/ExtractorModel';
export { Player } from './Player';
export { Util } from './utils/Util';
export { Track } from './Structures/Track';
export { Queue } from './Structures/Queue';
export * from './types/types';
export { PlayerError } from './utils/PlayerError';
export { AudioFilters } from "./utils/AudioFilters";
export { PlayerError } from "./utils/PlayerError";
export { VoiceUtils } from "./VoiceInterface/VoiceUtils";
export { VoiceEvents, VoiceSubscription } from "./VoiceInterface/VoiceSubscription";

View file

@ -1,42 +1,12 @@
import { downloadOptions } from 'ytdl-core';
import { User } from 'discord.js';
import { Readable, Duplex } from 'stream';
export interface PlayerOptions {
leaveOnEnd?: boolean;
leaveOnEndCooldown?: number;
leaveOnStop?: boolean;
leaveOnEmpty?: boolean;
leaveOnEmptyCooldown?: number;
autoSelfDeaf?: boolean;
enableLive?: boolean;
ytdlDownloadOptions?: downloadOptions;
useSafeSearch?: boolean;
disableAutoRegister?: boolean;
}
import { User } from "discord.js";
import { downloadOptions } from "ytdl-core";
import { Readable, Duplex } from "stream";
export type FiltersName = keyof QueueFilters;
export type TrackSource = 'soundcloud' | 'youtube' | 'arbitrary';
export interface TrackData {
title: string;
description: string;
author: string;
url: string;
thumbnail: string;
duration: string;
views: number;
requestedBy: User;
fromPlaylist: boolean;
source?: TrackSource;
engine?: any;
live?: boolean;
}
export type QueueFilters = {
bassboost?: boolean;
'8D'?: boolean;
"8D"?: boolean;
vaporwave?: boolean;
nightcore?: boolean;
phaser?: boolean;
@ -65,19 +35,51 @@ export type QueueFilters = {
fadein?: boolean;
};
export type QueryType =
| 'soundcloud_track'
| 'soundcloud_playlist'
| 'spotify_song'
| 'spotify_album'
| 'spotify_playlist'
| 'youtube_video'
| 'youtube_playlist'
| 'vimeo'
| 'facebook'
| 'reverbnation'
| 'attachment'
| 'youtube_search';
export type TrackSource = "soundcloud" | "youtube" | "spotify" | "arbitrary";
export interface RawTrackData {
title: string;
description: string;
author: string;
url: string;
thumbnail: string;
duration: string;
views: number;
requestedBy: User;
fromPlaylist: boolean;
source?: TrackSource;
engine?: any;
live?: boolean;
}
export interface TimeData {
days: number;
hours: number;
minutes: number;
seconds: number;
}
export interface PlayerProgressbarOptions {
timecodes?: boolean;
queue?: boolean;
length?: number;
line?: string;
indicator?: string;
}
export interface PlayerOptions {
leaveOnEnd?: boolean;
leaveOnEndCooldown?: number;
leaveOnStop?: boolean;
leaveOnEmpty?: boolean;
leaveOnEmptyCooldown?: number;
autoSelfDeaf?: boolean;
enableLive?: boolean;
ytdlDownloadOptions?: downloadOptions;
useSafeSearch?: boolean;
disableAutoRegister?: boolean;
fetchBeforeQueued?: boolean;
}
export interface ExtractorModelData {
title: string;
@ -90,67 +92,21 @@ export interface ExtractorModelData {
url: string;
version?: string;
important?: boolean;
source?: TrackSource;
}
export interface PlayerProgressbarOptions {
timecodes?: boolean;
queue?: boolean;
length?: number;
}
export interface LyricsData {
title: string;
id: number;
thumbnail: string;
image: string;
url: string;
artist: {
name: string;
id: number;
url: string;
image: string;
};
lyrics?: string;
}
export interface PlayerStats {
uptime: number;
connections: number;
users: number;
queues: number;
extractors: number;
versions: {
ffmpeg: string;
node: string;
v8: string;
};
system: {
arch: string;
platform:
| 'aix'
| 'android'
| 'darwin'
| 'freebsd'
| 'linux'
| 'openbsd'
| 'sunos'
| 'win32'
| 'cygwin'
| 'netbsd';
cpu: number;
memory: {
total: string;
usage: string;
rss: string;
arrayBuffers: string;
};
uptime: number;
};
}
export interface TimeData {
days: number;
hours: number;
minutes: number;
seconds: number;
export enum QueryType {
YOUTUBE = "youtube",
YOUTUBE_PLAYLIST = "youtube_playlist",
SOUNDCLOUD_TRACK = "soundcloud_track",
SOUNDCLOUD_PLAYLIST = "soundcloud_playlist",
SOUNDCLOUD = "soundcloud",
SPOTIFY_SONG = "spotify_song",
SPOTIFY_ALBUM = "spotify_album",
SPOTIFY_PLAYLIST = "spotify_playlist",
FACEBOOK = "facebook",
VIMEO = "vimeo",
ARBITRARY = "arbitrary",
REVERBNATION = "reverbnation",
YOUTUBE_SEARCH = "youtube_search"
}

View file

@ -1,4 +1,4 @@
import { FiltersName } from '../types/types';
import { FiltersName } from "../types/types";
/**
* The available audio filters
@ -34,63 +34,67 @@ import { FiltersName } from '../types/types';
*/
const FilterList = {
bassboost: 'bass=g=20',
'8D': 'apulsator=hz=0.09',
vaporwave: 'aresample=48000,asetrate=48000*0.8',
nightcore: 'aresample=48000,asetrate=48000*1.25',
phaser: 'aphaser=in_gain=0.4',
tremolo: 'tremolo',
vibrato: 'vibrato=f=6.5',
reverse: 'areverse',
treble: 'treble=g=5',
normalizer: 'dynaudnorm=g=101',
surrounding: 'surround',
pulsator: 'apulsator=hz=1',
subboost: 'asubboost',
karaoke: 'stereotools=mlev=0.03',
flanger: 'flanger',
gate: 'agate',
haas: 'haas',
mcompand: 'mcompand',
mono: 'pan=mono|c0=.5*c0+.5*c1',
mstlr: 'stereotools=mode=ms>lr',
mstrr: 'stereotools=mode=ms>rr',
compressor: 'compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6',
expander: 'compand=attacks=0:points=-80/-169|-54/-80|-49.5/-64.6|-41.1/-41.1|-25.8/-15|-10.8/-4.5|0/0|20/8.3',
softlimiter: 'compand=attacks=0:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8',
chorus: 'chorus=0.7:0.9:55:0.4:0.25:2',
chorus2d: 'chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.4:2|1.3',
chorus3d: 'chorus=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3',
fadein: 'afade=t=in:ss=0:d=10',
bassboost: "bass=g=20",
"8D": "apulsator=hz=0.09",
vaporwave: "aresample=48000,asetrate=48000*0.8",
nightcore: "aresample=48000,asetrate=48000*1.25",
phaser: "aphaser=in_gain=0.4",
tremolo: "tremolo",
vibrato: "vibrato=f=6.5",
reverse: "areverse",
treble: "treble=g=5",
normalizer: "dynaudnorm=g=101",
surrounding: "surround",
pulsator: "apulsator=hz=1",
subboost: "asubboost",
karaoke: "stereotools=mlev=0.03",
flanger: "flanger",
gate: "agate",
haas: "haas",
mcompand: "mcompand",
mono: "pan=mono|c0=.5*c0+.5*c1",
mstlr: "stereotools=mode=ms>lr",
mstrr: "stereotools=mode=ms>rr",
compressor: "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6",
expander: "compand=attacks=0:points=-80/-169|-54/-80|-49.5/-64.6|-41.1/-41.1|-25.8/-15|-10.8/-4.5|0/0|20/8.3",
softlimiter: "compand=attacks=0:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8",
chorus: "chorus=0.7:0.9:55:0.4:0.25:2",
chorus2d: "chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.4:2|1.3",
chorus3d: "chorus=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3",
fadein: "afade=t=in:ss=0:d=10",
*[Symbol.iterator](): IterableIterator<{ name: FiltersName; value: string }> {
for (const [k, v] of Object.entries(this)) {
if (typeof this[k as FiltersName] === 'string') yield { name: k as FiltersName, value: v as string };
if (typeof this[k as FiltersName] === "string") yield { name: k as FiltersName, value: v as string };
}
},
get names() {
return Object.keys(this).filter((p) => !['names', 'length'].includes(p) && typeof this[p as FiltersName] !== 'function');
return Object.keys(this).filter(
(p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function"
);
},
get length() {
return Object.keys(this).filter((p) => !['names', 'length'].includes(p) && typeof this[p as FiltersName] !== 'function').length;
return Object.keys(this).filter(
(p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function"
).length;
},
toString() {
return `${Object.values(this).join(',')}`;
return `${Object.values(this).join(",")}`;
},
create(filter?: FiltersName[]): string {
if (!filter || !Array.isArray(filter)) return this.toString();
return filter
.filter((predicate) => typeof predicate === 'string')
.filter((predicate) => typeof predicate === "string")
.map((m) => this[m])
.join(',');
.join(",");
},
define(filterName: string, value: string): void {
if (typeof this[filterName as FiltersName] && typeof this[filterName as FiltersName] === 'function') return;
if (typeof this[filterName as FiltersName] && typeof this[filterName as FiltersName] === "function") return;
this[filterName as FiltersName] = value;
},

View file

@ -1,40 +1 @@
import { PlayerOptions as DP_OPTIONS } from '../types/types';
export const PlayerEvents = {
BOT_DISCONNECT: 'botDisconnect',
CHANNEL_EMPTY: 'channelEmpty',
CONNECTION_CREATE: 'connectionCreate',
ERROR: 'error',
MUSIC_STOP: 'musicStop',
NO_RESULTS: 'noResults',
PLAYLIST_ADD: 'playlistAdd',
PLAYLIST_PARSE_END: 'playlistParseEnd',
PLAYLIST_PARSE_START: 'playlistParseStart',
QUEUE_CREATE: 'queueCreate',
QUEUE_END: 'queueEnd',
SEARCH_CANCEL: 'searchCancel',
SEARCH_INVALID_RESPONSE: 'searchInvalidResponse',
SEARCH_RESULTS: 'searchResults',
TRACK_ADD: 'trackAdd',
TRACK_START: 'trackStart'
};
export const PlayerErrorEventCodes = {
LIVE_VIDEO: 'LiveVideo',
NOT_CONNECTED: 'NotConnected',
UNABLE_TO_JOIN: 'UnableToJoin',
NOT_PLAYING: 'NotPlaying',
PARSE_ERROR: 'ParseError',
VIDEO_UNAVAILABLE: 'VideoUnavailable',
MUSIC_STARTING: 'MusicStarting'
};
export const PlayerOptions: DP_OPTIONS = {
leaveOnEnd: true,
leaveOnStop: true,
leaveOnEmpty: true,
leaveOnEmptyCooldown: 0,
autoSelfDeaf: true,
enableLive: false,
ytdlDownloadOptions: {}
};
export {};

View file

@ -1,7 +1,7 @@
export default class PlayerError extends Error {
constructor(msg: string, name?: string) {
super();
this.name = name ?? 'PlayerError';
this.name = name ?? "PlayerError";
this.message = msg;
Error.captureStackTrace(this);
}

View file

@ -0,0 +1,48 @@
import { validateID, validateURL } from "ytdl-core";
import { YouTube } from "youtube-sr";
import { QueryType } from "../types/types";
// @ts-ignore
import { validateURL as SoundcloudValidateURL } from "soundcloud-scraper";
// scary things below *sigh*
const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/;
const spotifyPlaylistRegex =
/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})/;
const spotifyAlbumRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:album\/|\?uri=spotify:album:)((\w|-){22})/;
const vimeoRegex =
/(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/;
const facebookRegex = /(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/;
const reverbnationRegex = /https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)/;
const attachmentRegex =
/^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/;
// scary things above *sigh*
class QueryResolver {
static resolve(query: string): QueryType {
if (SoundcloudValidateURL(query, "track")) return QueryType.SOUNDCLOUD_TRACK;
if (SoundcloudValidateURL(query, "playlist") || query.includes("/sets/")) return QueryType.SOUNDCLOUD_PLAYLIST;
if (validateID(query) || validateURL(query)) return QueryType.YOUTUBE;
if (YouTube.validate(query, "PLAYLIST_ID")) return QueryType.YOUTUBE_PLAYLIST;
if (spotifySongRegex.test(query)) return QueryType.SPOTIFY_SONG;
if (spotifyPlaylistRegex.test(query)) return QueryType.SPOTIFY_PLAYLIST;
if (spotifyAlbumRegex.test(query)) return QueryType.SPOTIFY_ALBUM;
if (vimeoRegex.test(query)) return QueryType.VIMEO;
if (facebookRegex.test(query)) return QueryType.FACEBOOK;
if (reverbnationRegex.test(query)) return QueryType.REVERBNATION;
if (attachmentRegex.test(query)) return QueryType.ARBITRARY;
return QueryType.YOUTUBE_SEARCH;
}
static getVimeoID(query: string): string {
return QueryResolver.resolve(query) === QueryType.VIMEO
? query
.split("/")
.filter((x) => !!x)
.pop()
: null;
}
}
export { QueryResolver };

View file

@ -1,231 +1 @@
import { QueryType, TimeData } from '../types/types';
import { FFmpeg } from 'prism-media';
import YouTube from 'youtube-sr';
import { Track } from '../Structures/Track';
// @ts-ignore
import { validateURL as SoundcloudValidateURL } from 'soundcloud-scraper';
import { VoiceChannel } from 'discord.js';
const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/;
const spotifyPlaylistRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})/;
const spotifyAlbumRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:album\/|\?uri=spotify:album:)((\w|-){22})/;
const vimeoRegex = /(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/;
const facebookRegex = /(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/;
const reverbnationRegex = /https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)/;
const attachmentRegex = /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/;
export class Util {
/**
* Static Player Util class
*/
constructor() {
throw new Error(`The ${this.constructor.name} class is static and cannot be instantiated!`);
}
/**
* Checks FFmpeg Version
* @param {Boolean} [force] If it should forcefully get the version
* @returns {String}
*/
static getFFmpegVersion(force?: boolean): string {
try {
const info = FFmpeg.getInfo(Boolean(force));
return info.version;
} catch {
return null;
}
}
/**
* Checks FFmpeg
* @param {Boolean} [force] If it should forcefully get the version
* @returns {Boolean}
*/
static checkFFmpeg(force?: boolean): boolean {
const version = Util.getFFmpegVersion(force);
return version === null ? false : true;
}
/**
* Alerts if FFmpeg is not available
*/
static alertFFmpeg(): void {
const hasFFmpeg = Util.checkFFmpeg();
if (!hasFFmpeg)
console.warn(
'[Discord Player] FFmpeg/Avconv not found! Install via "npm install ffmpeg-static" or download from https://ffmpeg.org/download.html'
);
}
/**
* Resolves query type
* @param {String} query The query
* @returns {QueryType}
*/
static getQueryType(query: string): QueryType {
if (SoundcloudValidateURL(query) && !query.includes('/sets/')) return 'soundcloud_track';
if (SoundcloudValidateURL(query) && query.includes('/sets/')) return 'soundcloud_playlist';
if (spotifySongRegex.test(query)) return 'spotify_song';
if (spotifyAlbumRegex.test(query)) return 'spotify_album';
if (spotifyPlaylistRegex.test(query)) return 'spotify_playlist';
if (YouTube.validate(query, 'PLAYLIST')) return 'youtube_playlist';
if (YouTube.validate(query, 'VIDEO')) return 'youtube_video';
if (vimeoRegex.test(query)) return 'vimeo';
if (facebookRegex.test(query)) return 'facebook';
if (reverbnationRegex.test(query)) return 'reverbnation';
if (Util.isURL(query)) return 'attachment';
return 'youtube_search';
}
/**
* Checks if the given string is url
* @param {String} str URL to check
* @returns {Boolean}
*/
static isURL(str: string): boolean {
return str.length < 2083 && attachmentRegex.test(str);
}
/**
* Returns Vimeo ID
* @param {String} query Vimeo link
* @returns {String}
*/
static getVimeoID(query: string): string {
return Util.getQueryType(query) === 'vimeo'
? query
.split('/')
.filter((x) => !!x)
.pop()
: null;
}
/**
* Parses ms time
* @param {Number} milliseconds Time to parse
* @returns {TimeData}
*/
static parseMS(milliseconds: number): TimeData {
const roundTowardsZero = milliseconds > 0 ? Math.floor : Math.ceil;
return {
days: roundTowardsZero(milliseconds / 86400000),
hours: roundTowardsZero(milliseconds / 3600000) % 24,
minutes: roundTowardsZero(milliseconds / 60000) % 60,
seconds: roundTowardsZero(milliseconds / 1000) % 60
};
}
/**
* Creates simple duration string
* @param {object} durObj Duration object
* @returns {String}
*/
static durationString(durObj: object): string {
return Object.values(durObj)
.map((m) => (isNaN(m) ? 0 : m))
.join(':');
}
/**
* Makes youtube searches
* @param {String} query The query
* @param {any} options Options
* @returns {Promise<Track[]>}
*/
static ytSearch(query: string, options?: any): Promise<Track[]> {
return new Promise(async (resolve) => {
await YouTube.search(query, {
type: 'video',
safeSearch: Boolean(options?.player.options.useSafeSearch),
limit: options.limit ?? 10
})
.then((results) => {
resolve(
results.map(
(r) =>
new Track(options?.player, {
title: r.title,
description: r.description,
author: r.channel.name,
url: r.url,
thumbnail: r.thumbnail.displayThumbnailURL(),
duration: Util.buildTimeCode(Util.parseMS(r.duration)),
views: r.views,
requestedBy: options?.user,
fromPlaylist: Boolean(options?.pl),
source: 'youtube'
})
)
);
})
.catch(() => resolve([]));
});
}
/**
* Checks if this system is running in replit.com
* @returns {Boolean}
*/
static isRepl(): boolean {
if ('DP_REPL_NOCHECK' in process.env) return false;
const REPL_IT_PROPS = [
'REPL_SLUG',
'REPL_OWNER',
'REPL_IMAGE',
'REPL_PUBKEYS',
'REPL_ID',
'REPL_LANGUAGE',
'REPLIT_DB_URL'
];
for (const prop of REPL_IT_PROPS) if (prop in process.env) return true;
return false;
}
/**
* Checks if the given voice channel is empty
* @param {DiscordVoiceChannel} channel The voice channel
* @returns {Boolean}
*/
static isVoiceEmpty(channel: VoiceChannel): boolean {
return channel.members.filter((member) => !member.user.bot).size === 0;
}
/**
* Builds time code
* @param {object} data The data to build time code from
* @returns {String}
*/
static buildTimeCode(data: any): string {
const items = Object.keys(data);
const required = ['days', 'hours', 'minutes', 'seconds'];
const parsed = items.filter((x) => required.includes(x)).map((m) => (data[m] > 0 ? data[m] : ''));
const final = parsed
.filter((x) => !!x)
.map((x) => x.toString().padStart(2, '0'))
.join(':');
return final.length <= 3 ? `0:${final.padStart(2, '0') || 0}` : final;
}
/**
* Manage CJS require
* @param {String} id id to require
* @returns {any}
*/
static require(id: string): any {
try {
return require(id);
} catch {
return null;
}
}
}
export default Util;
export {};

1355
yarn.lock

File diff suppressed because it is too large Load diff