diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..94f480d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/package.json b/package.json index 69cea7b..8092060 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/Player.ts b/src/Player.ts index 85242d3..d10b4f0 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -39,7 +39,19 @@ export class Player extends EventEmitter { * @type {DiscordCollection} */ public queues = new Collection(); + + /** + * Collection of results collectors + * @type {DiscordCollection>} + * @private + */ private _resultsCollectors = new Collection>(); + + /** + * Collection of cooldowns timeout + * @type {DiscordCollection} + * @private + */ private _cooldownsTimeout = new Collection(); /** @@ -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} + * @private + */ private _searchTracks(message: Message, query: string, firstResult?: boolean): Promise { 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( + 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} */ @@ -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} + * @private + */ private _createQueue(message: Message, track: Track): Promise { 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} + * @private + */ private async _playTrack(queue: Queue, firstPlay: boolean): Promise { 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} + * @private + */ private _playStream(queue: Queue, updateFilter: boolean, seek?: number): Promise { 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. + * This event should handled properly by the users otherwise it might crash the process! * @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 */ /** diff --git a/src/Structures/Queue.ts b/src/Structures/Queue.ts index 1cee68b..a8fa7a5 100644 --- a/src/Structures/Queue.ts +++ b/src/Structures/Queue.ts @@ -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; } /** diff --git a/src/types/types.ts b/src/types/types.ts index 9e57e90..629188d 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -13,6 +13,7 @@ export interface PlayerOptions { ytdlDownloadOptions?: downloadOptions; useSafeSearch?: boolean; disableAutoRegister?: boolean; + disableArtistSearch?: boolean; } export type FiltersName = keyof QueueFilters; diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index ef5eb1b..afa41d5 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -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, diff --git a/yarn.lock b/yarn.lock index 14800c2..cf8a6a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"