Merge branch 'v5'
This commit is contained in:
commit
04bd807050
18 changed files with 1107 additions and 2839 deletions
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"semi": true
|
||||
}
|
|
@ -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)**
|
||||
|
|
24
package.json
24
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
1472
src/Player.ts
1472
src/Player.ts
File diff suppressed because it is too large
Load diff
|
@ -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 };
|
||||
|
|
18
src/Structures/Playlist.ts
Normal file
18
src/Structures/Playlist.ts
Normal 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 };
|
|
@ -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 };
|
||||
|
|
|
@ -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 };
|
||||
|
|
141
src/VoiceInterface/VoiceSubscription.ts
Normal file
141
src/VoiceInterface/VoiceSubscription.ts
Normal 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 };
|
53
src/VoiceInterface/VoiceUtils.ts
Normal file
53
src/VoiceInterface/VoiceUtils.ts
Normal 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 };
|
13
src/index.ts
13
src/index.ts
|
@ -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";
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
48
src/utils/QueryResolver.ts
Normal file
48
src/utils/QueryResolver.ts
Normal 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 };
|
|
@ -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 {};
|
||||
|
|
Loading…
Reference in a new issue