commit
067f7f2178
7 changed files with 157 additions and 63 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "discord-player",
|
||||
"version": "4.0.6",
|
||||
"version": "4.0.7",
|
||||
"description": "Complete framework to facilitate music commands using discord.js",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
@ -48,10 +48,10 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/Androz2091/discord-player/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Androz2091/discord-player#readme",
|
||||
"homepage": "https://discord-player.js.org",
|
||||
"dependencies": {
|
||||
"discord-ytdl-core": "^5.0.3",
|
||||
"soundcloud-scraper": "^4.0.3",
|
||||
"soundcloud-scraper": "^4.0.4",
|
||||
"spotify-url-info": "^2.2.0",
|
||||
"youtube-sr": "^4.0.6",
|
||||
"ytdl-core": "^4.7.0"
|
||||
|
|
143
src/Player.ts
143
src/Player.ts
|
@ -39,7 +39,19 @@ export class Player extends EventEmitter {
|
|||
* @type {DiscordCollection<Queue>}
|
||||
*/
|
||||
public queues = new Collection<Snowflake, Queue>();
|
||||
|
||||
/**
|
||||
* Collection of results collectors
|
||||
* @type {DiscordCollection<DiscordCollector<DiscordSnowflake, DiscordMessage>>}
|
||||
* @private
|
||||
*/
|
||||
private _resultsCollectors = new Collection<string, Collector<Snowflake, Message>>();
|
||||
|
||||
/**
|
||||
* Collection of cooldowns timeout
|
||||
* @type {DiscordCollection<Timeout>}
|
||||
* @private
|
||||
*/
|
||||
private _cooldownsTimeout = new Collection<string, NodeJS.Timeout>();
|
||||
|
||||
/**
|
||||
|
@ -131,6 +143,14 @@ export class Player extends EventEmitter {
|
|||
return this.Extractors.delete(extractorName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to search tracks
|
||||
* @param {DiscordMessage} message The message
|
||||
* @param {string} query The query
|
||||
* @param {boolean} [firstResult=false] If it should return the first result
|
||||
* @returns {Promise<Track>}
|
||||
* @private
|
||||
*/
|
||||
private _searchTracks(message: Message, query: string, firstResult?: boolean): Promise<Track> {
|
||||
return new Promise(async (resolve) => {
|
||||
let tracks: Track[] = [];
|
||||
|
@ -144,7 +164,7 @@ export class Player extends EventEmitter {
|
|||
const track = new Track(this, {
|
||||
title: data.title,
|
||||
url: data.url,
|
||||
duration: Util.buildTimeCode(Util.parseMS(data.duration / 1000)),
|
||||
duration: Util.buildTimeCode(Util.parseMS(data.duration)),
|
||||
description: data.description,
|
||||
thumbnail: data.thumbnail,
|
||||
views: data.playCount,
|
||||
|
@ -167,7 +187,10 @@ export class Player extends EventEmitter {
|
|||
if (matchSpotifyURL) {
|
||||
const spotifyData = await spotify.getPreview(query).catch(() => {});
|
||||
if (spotifyData) {
|
||||
tracks = await Util.ytSearch(`${spotifyData.artist} - ${spotifyData.title}`, {
|
||||
const searchString = this.options.disableArtistSearch
|
||||
? spotifyData.title
|
||||
: `${spotifyData.artist} - ${spotifyData.title}`;
|
||||
tracks = await Util.ytSearch(searchString, {
|
||||
user: message.author,
|
||||
player: this,
|
||||
limit: 1
|
||||
|
@ -177,38 +200,48 @@ export class Player extends EventEmitter {
|
|||
}
|
||||
break;
|
||||
|
||||
// todo: make spotify playlist/album load faster
|
||||
case 'spotify_album':
|
||||
case 'spotify_playlist': {
|
||||
this.emit(PlayerEvents.PLAYLIST_PARSE_START, null, message);
|
||||
const playlist = await spotify.getData(query);
|
||||
if (!playlist) return void this.emit(PlayerEvents.NO_RESULTS, message, query);
|
||||
|
||||
// tslint:disable:no-shadowed-variable
|
||||
const tracks = [];
|
||||
// tslint:disable-next-line:no-shadowed-variable
|
||||
let tracks = await Promise.all<Track>(
|
||||
playlist.tracks.items.map(async (item: any) => {
|
||||
const sq =
|
||||
queryType === 'spotify_album'
|
||||
? `${
|
||||
this.options.disableArtistSearch
|
||||
? item.artists[0].name
|
||||
: `${item.artists[0].name} - `
|
||||
}${item.name ?? item.track.name}`
|
||||
: `${
|
||||
this.options.disableArtistSearch
|
||||
? item.track.artists[0].name
|
||||
: `${item.track.artists[0].name} - `
|
||||
}${item.name ?? item.track.name}`;
|
||||
|
||||
for (const item of playlist.tracks.items) {
|
||||
const sq =
|
||||
queryType === 'spotify_album'
|
||||
? `${item.artists[0].name} - ${item.name}`
|
||||
: `${item.track.artists[0].name} - ${item.name}`;
|
||||
const data = await Util.ytSearch(sq, {
|
||||
limit: 1,
|
||||
player: this,
|
||||
user: message.author,
|
||||
pl: true
|
||||
});
|
||||
const data = await Util.ytSearch(sq, {
|
||||
limit: 1,
|
||||
player: this,
|
||||
user: message.author,
|
||||
pl: true
|
||||
});
|
||||
|
||||
if (data[0]) tracks.push(data[0]);
|
||||
}
|
||||
if (data.length) return data[0];
|
||||
})
|
||||
);
|
||||
|
||||
tracks = tracks.filter((f) => !!f);
|
||||
if (!tracks.length) return void this.emit(PlayerEvents.NO_RESULTS, message, query);
|
||||
|
||||
const pl = {
|
||||
...playlist,
|
||||
tracks,
|
||||
duration: tracks.reduce((a, c) => a + c.durationMS, 0),
|
||||
thumbnail: playlist.images[0]?.url ?? tracks[0].thumbnail
|
||||
duration: tracks?.reduce((a, c) => a + (c?.durationMS ?? 0), 0) ?? 0,
|
||||
thumbnail: playlist.images[0]?.url ?? tracks[0].thumbnail,
|
||||
title: playlist.title ?? playlist.name ?? ''
|
||||
};
|
||||
|
||||
this.emit(PlayerEvents.PLAYLIST_PARSE_END, pl, message);
|
||||
|
@ -221,6 +254,7 @@ export class Player extends EventEmitter {
|
|||
const queue = (await this._createQueue(message, track).catch(
|
||||
(e) => void this.emit(PlayerEvents.ERROR, e, message)
|
||||
)) as Queue;
|
||||
this.emit(PlayerEvents.PLAYLIST_ADD, message, queue, pl);
|
||||
this.emit(PlayerEvents.TRACK_START, message, queue.tracks[0], queue);
|
||||
this._addTracksToQueue(message, tracks);
|
||||
}
|
||||
|
@ -258,9 +292,14 @@ export class Player extends EventEmitter {
|
|||
// @ts-ignore
|
||||
playlist.requestedBy = message.author;
|
||||
|
||||
Object.defineProperty(playlist, 'tracks', {
|
||||
get: () => playlist.videos ?? []
|
||||
});
|
||||
|
||||
this.emit(PlayerEvents.PLAYLIST_PARSE_END, playlist, message);
|
||||
|
||||
// @ts-ignore
|
||||
// tslint:disable-next-line:no-shadowed-variable
|
||||
const tracks = playlist.videos as Track[];
|
||||
|
||||
if (this.isPlaying(message)) {
|
||||
|
@ -298,7 +337,7 @@ export class Player extends EventEmitter {
|
|||
const r = new Track(this, {
|
||||
title: song.title,
|
||||
url: song.url,
|
||||
duration: Util.buildTimeCode(Util.parseMS(song.duration / 1000)),
|
||||
duration: Util.buildTimeCode(Util.parseMS(song.duration)),
|
||||
description: song.description,
|
||||
thumbnail: song.thumbnail ?? 'https://soundcloud.com/pwa-icon-192.png',
|
||||
views: song.playCount ?? 0,
|
||||
|
@ -380,7 +419,7 @@ export class Player extends EventEmitter {
|
|||
* Play a song
|
||||
* @param {DiscordMessage} message The discord.js message object
|
||||
* @param {string|Track} query Search query, can be `Player.Track` instance
|
||||
* @param {Boolean} [firstResult] If it should play the first result
|
||||
* @param {Boolean} [firstResult=false] If it should play the first result
|
||||
* @example await player.play(message, "never gonna give you up", true)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
@ -949,6 +988,7 @@ export class Player extends EventEmitter {
|
|||
return {
|
||||
uptime: this.client.uptime,
|
||||
connections: this.client.voice.connections.size,
|
||||
// tslint:disable:no-shadowed-variable
|
||||
users: this.client.voice.connections.reduce(
|
||||
(a, c) => a + c.channel.members.filter((a) => a.user.id !== this.client.user.id).size,
|
||||
0
|
||||
|
@ -991,6 +1031,13 @@ export class Player extends EventEmitter {
|
|||
return this.skip(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to handle VoiceStateUpdate events
|
||||
* @param {DiscordVoiceState} oldState The old voice state
|
||||
* @param {DiscordVoiceState} newState The new voice state
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
private _handleVoiceStateUpdate(oldState: VoiceState, newState: VoiceState): void {
|
||||
const queue = this.queues.find((g) => g.guildID === oldState.guild.id);
|
||||
if (!queue) return;
|
||||
|
@ -1006,15 +1053,17 @@ export class Player extends EventEmitter {
|
|||
|
||||
if (!oldState.channelID || newState.channelID) {
|
||||
const emptyTimeout = this._cooldownsTimeout.get(`empty_${oldState.guild.id}`);
|
||||
const channelEmpty = Util.isVoiceEmpty(queue.voiceConnection.channel);
|
||||
|
||||
// @todo: stage channels
|
||||
const channelEmpty = Util.isVoiceEmpty(queue.voiceConnection.channel as VoiceChannel);
|
||||
if (!channelEmpty && emptyTimeout) {
|
||||
clearTimeout(emptyTimeout);
|
||||
this._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
|
||||
}
|
||||
} else {
|
||||
if (!Util.isVoiceEmpty(queue.voiceConnection.channel)) return;
|
||||
if (!Util.isVoiceEmpty(queue.voiceConnection.channel as VoiceChannel)) return;
|
||||
const timeout = setTimeout(() => {
|
||||
if (!Util.isVoiceEmpty(queue.voiceConnection.channel)) return;
|
||||
if (!Util.isVoiceEmpty(queue.voiceConnection.channel as VoiceChannel)) return;
|
||||
if (!this.queues.has(queue.guildID)) return;
|
||||
queue.voiceConnection.channel.leave();
|
||||
this.queues.delete(queue.guildID);
|
||||
|
@ -1024,7 +1073,14 @@ export class Player extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
private _addTrackToQueue(message: Message, track: Track): Queue {
|
||||
/**
|
||||
* Internal method used to add tracks to the queue
|
||||
* @param {DiscordMessage} message The discord message
|
||||
* @param {Track} track The track
|
||||
* @returns {Queue}
|
||||
* @private
|
||||
*/
|
||||
_addTrackToQueue(message: Message, track: Track): Queue {
|
||||
const queue = this.getQueue(message);
|
||||
if (!queue)
|
||||
this.emit(
|
||||
|
@ -1038,7 +1094,14 @@ export class Player extends EventEmitter {
|
|||
return queue;
|
||||
}
|
||||
|
||||
private _addTracksToQueue(message: Message, tracks: Track[]): Queue {
|
||||
/**
|
||||
* Same as `_addTrackToQueue` but used for multiple tracks
|
||||
* @param {DiscordMessage} message Discord message
|
||||
* @param {Track[]} tracks The tracks
|
||||
* @returns {Queue}
|
||||
* @private
|
||||
*/
|
||||
_addTracksToQueue(message: Message, tracks: Track[]): Queue {
|
||||
const queue = this.getQueue(message);
|
||||
if (!queue)
|
||||
throw new PlayerError(
|
||||
|
@ -1048,6 +1111,13 @@ export class Player extends EventEmitter {
|
|||
return queue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method used to create queue
|
||||
* @param {DiscordMessage} message The message
|
||||
* @param {Track} track The track
|
||||
* @returns {Promise<Queue>}
|
||||
* @private
|
||||
*/
|
||||
private _createQueue(message: Message, track: Track): Promise<Queue> {
|
||||
return new Promise((resolve) => {
|
||||
const channel = message.member.voice ? message.member.voice.channel : null;
|
||||
|
@ -1086,6 +1156,13 @@ export class Player extends EventEmitter {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method used to init stream playing
|
||||
* @param {Queue} queue The queue
|
||||
* @param {boolean} firstPlay If this is a first play
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
private async _playTrack(queue: Queue, firstPlay: boolean): Promise<void> {
|
||||
if (queue.stopped) return;
|
||||
|
||||
|
@ -1140,6 +1217,14 @@ export class Player extends EventEmitter {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to play audio
|
||||
* @param {Queue} queue The queue
|
||||
* @param {boolean} updateFilter If this method was called for audio filter update
|
||||
* @param {number} [seek] Time in ms to seek to
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
private _playStream(queue: Queue, updateFilter: boolean, seek?: number): Promise<void> {
|
||||
return new Promise(async (resolve) => {
|
||||
const ffmpeg = Util.checkFFmpeg();
|
||||
|
@ -1324,7 +1409,8 @@ export default Player;
|
|||
*/
|
||||
|
||||
/**
|
||||
* Emitted when an error is triggered
|
||||
* Emitted when an error is triggered.
|
||||
* <warn>This event should handled properly by the users otherwise it might crash the process!</warn>
|
||||
* @event Player#error
|
||||
* @param {String} error It can be `NotConnected`, `UnableToJoin`, `NotPlaying`, `ParseError`, `LiveVideo` or `VideoUnavailable`.
|
||||
* @param {DiscordMessage} message The message
|
||||
|
@ -1356,6 +1442,7 @@ export default Player;
|
|||
* @property {YTDLDownloadOptions} [ytdlDownloadOptions={}] The download options passed to `ytdl-core`
|
||||
* @property {Boolean} [useSafeSearch=false] If it should use `safe search` method for youtube searches
|
||||
* @property {Boolean} [disableAutoRegister=false] If it should disable auto-registeration of `@discord-player/extractor`
|
||||
* @property {Boolean} [disableArtistSearch=false] If it should disable artist search for spotify
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -156,7 +156,12 @@ export class Queue extends EventEmitter {
|
|||
* @type {Number}
|
||||
*/
|
||||
get currentStreamTime(): number {
|
||||
return this.voiceConnection?.dispatcher?.streamTime + this.additionalStreamTime || 0;
|
||||
const NC = this.filters.nightcore ? 1.25 : null;
|
||||
const VW = this.filters.vaporwave ? 0.8 : null;
|
||||
const streamTime = this.voiceConnection?.dispatcher?.streamTime + this.additionalStreamTime || 0;
|
||||
|
||||
if (NC && VW) return streamTime * (NC + VW);
|
||||
return NC ? streamTime * NC : VW ? streamTime * VW : streamTime;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface PlayerOptions {
|
|||
ytdlDownloadOptions?: downloadOptions;
|
||||
useSafeSearch?: boolean;
|
||||
disableAutoRegister?: boolean;
|
||||
disableArtistSearch?: boolean;
|
||||
}
|
||||
|
||||
export type FiltersName = keyof QueueFilters;
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
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 enum 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 enum 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,
|
||||
|
|
|
@ -3917,10 +3917,10 @@ sort-array@^2.0.0:
|
|||
object-get "^2.1.0"
|
||||
typical "^2.6.0"
|
||||
|
||||
soundcloud-scraper@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/soundcloud-scraper/-/soundcloud-scraper-4.0.3.tgz#cd7ed1d7b6ed1d7729fd7580c011281f652b920f"
|
||||
integrity sha512-A0a6sVJ2wkkWIX8Ft3L63sfHBlFDRAaPFif+SWi07KCNLh8YTcylw45pts76pndxlupKwV2NgOTIYeF/F9tg8w==
|
||||
soundcloud-scraper@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/soundcloud-scraper/-/soundcloud-scraper-4.0.4.tgz#dfd6a45dc6e63fac7d6b31f2ba1d23a199ca4fb6"
|
||||
integrity sha512-ei3KuPsVZRiq9j2GN580gQwVGZUWMdkmDAANSPm8qweUa4/UnKAnYiUsc/6volZsQiGsnJAP9+8HECDxNcTg6A==
|
||||
dependencies:
|
||||
cheerio "^1.0.0-rc.3"
|
||||
m3u8stream "^0.8.0"
|
||||
|
|
Loading…
Reference in a new issue