discord-player
Advanced tools
Comparing version 3.2.1 to 3.3.1
10
index.js
@@ -0,4 +1,10 @@ | ||
process.env.YTDL_NO_UPDATE = true | ||
module.exports = { | ||
version: require('./package.json').version, | ||
Player: require('./src/Player') | ||
Extractors: require('./src/Extractors/Extractor'), | ||
Player: require('./src/Player'), | ||
Queue: require('./src/Queue'), | ||
Track: require('./src/Track'), | ||
Util: require('./src/Util'), | ||
version: require('./package.json').version | ||
} |
{ | ||
"name": "discord-player", | ||
"version": "3.2.1", | ||
"version": "3.3.1", | ||
"description": "Complete framework to facilitate music commands using discord.js v12", | ||
@@ -9,3 +9,3 @@ "main": "index.js", | ||
"scripts": { | ||
"test": "node index.js", | ||
"test": "cd test && node index.js", | ||
"generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose" | ||
@@ -34,20 +34,23 @@ }, | ||
"dependencies": { | ||
"@types/node": "^14.14.7", | ||
"chalk": "^4.1.0", | ||
"discord-ytdl-core": "^5.0.0", | ||
"jsdom": "^16.4.0", | ||
"merge-options": "^3.0.4", | ||
"moment": "^2.27.0", | ||
"node-fetch": "^2.6.0", | ||
"soundcloud-scraper": "^4.0.0", | ||
"parse-ms": "^2.1.0", | ||
"reverbnation-scraper": "^2.0.0", | ||
"soundcloud-scraper": "^4.0.3", | ||
"spotify-url-info": "^2.2.0", | ||
"youtube-sr": "^2.0.5", | ||
"ytdl-core": "^4.4.0" | ||
"youtube-sr": "^4.0.0", | ||
"ytdl-core": "^4.5.0" | ||
}, | ||
"devDependencies": { | ||
"@discordjs/opus": "^0.3.2", | ||
"@discordjs/opus": "^0.4.0", | ||
"@types/node": "14.14.31", | ||
"discord.js": "^12.2.0", | ||
"eslint": "^7.1.0", | ||
"eslint": "^7.20.0", | ||
"eslint-config-standard": "^16.0.2", | ||
"eslint-plugin-import": "^2.20.2", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-promise": "^4.2.1", | ||
"eslint-plugin-promise": "^4.3.1", | ||
"eslint-plugin-standard": "^5.0.0", | ||
@@ -54,0 +57,0 @@ "jsdoc": "^3.6.3", |
@@ -5,3 +5,2 @@ # Discord Player | ||
[![versionBadge](https://img.shields.io/npm/v/discord-player?style=for-the-badge)](https://npmjs.com/discord-player) | ||
[![patreonBadge](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2FAndroz2091%2Fpledges&style=for-the-badge)](https://patreon.com/Androz2091) | ||
@@ -165,2 +164,5 @@ **Note**: this module uses recent discordjs features and requires discord.js version 12. | ||
break; | ||
case 'VideoUnavailable': | ||
message.channel.send('This YouTube video is not available!'); | ||
break; | ||
default: | ||
@@ -167,0 +169,0 @@ message.channel.send(`Something went wrong... Error: ${error}`) |
const ytdl = require('discord-ytdl-core') | ||
const Discord = require('discord.js') | ||
const ytsr = require('youtube-sr') | ||
const ytsr = require('youtube-sr').default | ||
const spotify = require('spotify-url-info') | ||
const soundcloud = require('soundcloud-scraper') | ||
const moment = require('moment') | ||
const ms = require('parse-ms') | ||
const Queue = require('./Queue') | ||
@@ -12,2 +12,3 @@ const Track = require('./Track') | ||
const Client = new soundcloud.Client() | ||
const { VimeoExtractor, DiscordExtractor, FacebookExtractor, ReverbnationExtractor } = require('./Extractors/Extractor') | ||
@@ -35,2 +36,11 @@ /** | ||
* @property {boolean} [mono=false] Whether the mono output is enabled. | ||
* @property {boolean} [mstlr=false] Whether M/S signal to L/R signal converter is enabled. | ||
* @property {boolean} [mstrr=false] Whether M/S signal to R/R signal converter is enabled. | ||
* @property {boolean} [compressor=false] Whether compressor filter is enabled. | ||
* @property {boolean} [expander=false] Whether expander filter is enabled. | ||
* @property {boolean} [softlimiter=false] Whether softlimiter filter is enabled. | ||
* @property {boolean} [chorus=false] Whether chorus (single delay) filter is enabled. | ||
* @property {boolean} [chorus2d=false] Whether chorus2d (two delays) filter is enabled. | ||
* @property {boolean} [chorus3d=false] Whether chorus3d (three delays) filter is enabled. | ||
* @property {boolean} [fadein=false] Whether fadein filter is enabled. | ||
*/ | ||
@@ -57,3 +67,12 @@ | ||
mcompand: 'mcompand', | ||
mono: 'pan=mono|c0=.5*c0+.5*c1' | ||
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' | ||
} | ||
@@ -69,2 +88,5 @@ | ||
* @property {boolean} [autoSelfDeaf=true] Whether the bot should automatically turn off its headphones when joining a voice channel. | ||
* @property {string} [quality='high'] Music quality (high or low) | ||
* @property {boolean} [enableLive=false] If it should enable live contents | ||
* @property {object} [ytdlRequestOptions={}] YTDL request options to use cookies, proxy etc.. | ||
*/ | ||
@@ -82,3 +104,6 @@ | ||
leaveOnEmptyCooldown: 0, | ||
autoSelfDeaf: true | ||
autoSelfDeaf: true, | ||
quality: 'high', | ||
enableLive: false, | ||
ytdlRequestOptions: {} | ||
} | ||
@@ -100,2 +125,4 @@ | ||
this.util = Util | ||
this.util.checkFFMPEG() | ||
/** | ||
@@ -133,9 +160,27 @@ * Discord.js client instance | ||
this._resultsCollectors = new Discord.Collection() | ||
/** | ||
* @private | ||
* @type {Discord.Collection<string, Timeout>} | ||
*/ | ||
this._cooldownsTimeout = new Discord.Collection() | ||
} | ||
/** | ||
* Returns all the available audio filters | ||
* @type {Filters} | ||
* @example const filters = require('discord-player').Player.AudioFilters | ||
* console.log(`There are ${Object.keys(filters).length} filters!`) | ||
*/ | ||
static get AudioFilters () { | ||
return filters | ||
} | ||
/** | ||
* @ignore | ||
* @param {String} query | ||
*/ | ||
resolveQueryType (query) { | ||
resolveQueryType (query, forceType) { | ||
if (forceType && typeof forceType === 'string') return forceType | ||
if (this.util.isSpotifyLink(query)) { | ||
@@ -149,2 +194,12 @@ return 'spotify-song' | ||
return 'soundcloud-song' | ||
} else if (this.util.isSpotifyPLLink(query)) { | ||
return 'spotify-playlist' | ||
} else if (this.util.isVimeoLink(query)) { | ||
return 'vimeo' | ||
} else if (FacebookExtractor.validateURL(query)) { | ||
return 'facebook' | ||
} else if (this.util.isReverbnationLink(query)) { | ||
return 'reverbnation' | ||
} else if (this.util.isDiscordAttachment(query)) { | ||
return 'attachment' | ||
} else { | ||
@@ -161,5 +216,6 @@ return 'youtube-video-keywords' | ||
* @param {boolean} firstResult | ||
* @param {boolean} isAttachment | ||
* @returns {Promise<Track>} | ||
*/ | ||
_searchTracks (message, query, firstResult) { | ||
_searchTracks (message, query, firstResult, isAttachment) { | ||
return new Promise(async (resolve) => { | ||
@@ -181,7 +237,119 @@ let tracks = [] | ||
} else if (queryType === 'soundcloud-song') { | ||
const soundcloudData = await Client.getSongInfo(query).catch(() => {}) | ||
if (soundcloudData) { | ||
updatedQuery = `${soundcloudData.author.name} - ${soundcloudData.title}` | ||
queryType = 'youtube-video-keywords' | ||
const data = await Client.getSongInfo(query).catch(() => { }) | ||
if (data) { | ||
const track = new Track({ | ||
title: data.title, | ||
url: data.url, | ||
lengthSeconds: data.duration / 1000, | ||
description: data.description, | ||
thumbnail: data.thumbnail, | ||
views: data.playCount, | ||
author: data.author | ||
}, message.author, this) | ||
Object.defineProperty(track, 'soundcloud', { | ||
get: () => data | ||
}) | ||
tracks.push(track) | ||
} | ||
} else if (queryType === 'vimeo') { | ||
const data = await VimeoExtractor.getInfo(this.util.getVimeoID(query)).catch(e => {}) | ||
if (!data) return this.emit('noResults', message, query) | ||
const track = new Track({ | ||
title: data.title, | ||
url: data.url, | ||
thumbnail: data.thumbnail, | ||
lengthSeconds: data.duration, | ||
description: '', | ||
views: 0, | ||
author: data.author | ||
}, message.author, this) | ||
Object.defineProperties(track, { | ||
arbitrary: { | ||
get: () => true | ||
}, | ||
stream: { | ||
get: () => data.stream.url | ||
} | ||
}) | ||
tracks.push(track) | ||
} else if (queryType === 'facebook') { | ||
const data = await FacebookExtractor.getInfo(query).catch(e => {}) | ||
if (!data) return this.emit('noResults', message, query) | ||
if (data.live && !this.options.enableLive) return this.emit('error', 'LiveVideo', message) | ||
const track = new Track({ | ||
title: data.title, | ||
url: data.url, | ||
thumbnail: data.thumbnail, | ||
lengthSeconds: data.duration, | ||
description: data.description, | ||
views: data.views || data.interactionCount, | ||
author: data.author | ||
}, message.author, this) | ||
Object.defineProperties(track, { | ||
arbitrary: { | ||
get: () => true | ||
}, | ||
stream: { | ||
get: () => data.streamURL | ||
} | ||
}) | ||
tracks.push(track) | ||
} else if (queryType === 'reverbnation') { | ||
const data = await ReverbnationExtractor.getInfo(query).catch(() => {}) | ||
if (!data) return this.emit('noResults', message, query) | ||
const track = new Track({ | ||
title: data.title, | ||
url: data.url, | ||
thumbnail: data.thumbnail, | ||
lengthSeconds: data.duration / 1000, | ||
description: '', | ||
views: 0, | ||
author: data.artist | ||
}, message.author, this) | ||
Object.defineProperties(track, { | ||
arbitrary: { | ||
get: () => true | ||
}, | ||
stream: { | ||
get: () => data.streamURL | ||
} | ||
}) | ||
tracks.push(track) | ||
} else if (!!isAttachment || queryType === 'attachment') { | ||
const data = await DiscordExtractor.getInfo(query).catch(() => {}) | ||
if (!data || !(data.format.startsWith('audio/') || data.format.startsWith('video/'))) return this.emit('noResults', message, query) | ||
const track = new Track({ | ||
title: data.title, | ||
url: query, | ||
thumbnail: '', | ||
lengthSeconds: 0, | ||
description: '', | ||
views: 0, | ||
author: { | ||
name: 'Media Attachment' | ||
} | ||
}, message.author, this) | ||
Object.defineProperties(track, { | ||
arbitrary: { | ||
get: () => true | ||
}, | ||
stream: { | ||
get: () => query | ||
} | ||
}) | ||
tracks.push(track) | ||
} | ||
@@ -191,3 +359,3 @@ | ||
await ytsr.search(updatedQuery || query, { type: 'video' }).then((results) => { | ||
if (results.length !== 0) { | ||
if (results && results.length !== 0) { | ||
tracks = results.map((r) => new Track(r, message.author, this)) | ||
@@ -256,2 +424,33 @@ } | ||
/** | ||
* Sets currently playing music duration | ||
* @param {Discord.Message} message Discord message | ||
* @param {number} time Time in ms | ||
* @returns {Promise<void>} | ||
*/ | ||
setPosition (message, time) { | ||
return new Promise((resolve) => { | ||
const queue = this.queues.find((g) => g.guildID === message.guild.id) | ||
if (!queue) return this.emit('error', 'NotPlaying', message) | ||
if (typeof time !== 'number' && !isNaN(time)) time = parseInt(time) | ||
if (queue.playing.durationMS === time) return this.skip(message) | ||
if (queue.voiceConnection.dispatcher.streamTime === time || (queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime) === time) return resolve() | ||
if (time < 0) this._playYTDLStream(queue, false).then(() => resolve()) | ||
this._playYTDLStream(queue, false, time) | ||
.then(() => resolve()) | ||
}) | ||
} | ||
/** | ||
* Sets currently playing music duration | ||
* @param {Discord.Message} message Discord message | ||
* @param {number} time Time in ms | ||
* @returns {Promise<void>} | ||
*/ | ||
seek (message, time) { | ||
return this.setPosition(message, time) | ||
} | ||
/** | ||
* Check whether there is a music played in the server | ||
@@ -265,2 +464,19 @@ * @param {Discord.Message} message | ||
/** | ||
* Moves to new voice channel | ||
* @param {Discord.Message} message Message | ||
* @param {Discord.VoiceChannel} channel Voice channel | ||
*/ | ||
moveTo (message, channel) { | ||
if (!channel || channel.type !== 'voice') return | ||
const queue = this.queues.find((g) => g.guildID === message.guild.id) | ||
if (!queue) return this.emit('error', 'NotPlaying', message) | ||
if (queue.voiceConnection.channel.id === channel.id) return | ||
queue.voiceConnection.dispatcher.pause() | ||
channel.join() | ||
.then(() => queue.voiceConnection.dispatcher.resume()) | ||
.catch(() => this.emit('error', 'UnableToJoin', message)) | ||
} | ||
/** | ||
* Add a track to the queue | ||
@@ -329,2 +545,3 @@ * @ignore | ||
async _handlePlaylist (message, query) { | ||
this.emit('playlistParseStart', {}, message) | ||
const playlist = await ytsr.getPlaylist(query) | ||
@@ -336,2 +553,5 @@ if (!playlist) return this.emit('noResults', message, query) | ||
playlist.requestedBy = message.author | ||
this.emit('playlistParseEnd', playlist, message) | ||
if (this.isPlaying(message)) { | ||
@@ -343,3 +563,3 @@ const queue = this._addTracksToQueue(message, playlist.tracks) | ||
const queue = await this._createQueue(message, track).catch((e) => this.emit('error', e, message)) | ||
this.emit('trackStart', message, queue.tracks[0]) | ||
this.emit('trackStart', message, queue.tracks[0], queue) | ||
this._addTracksToQueue(message, playlist.tracks) | ||
@@ -349,3 +569,137 @@ } | ||
async _handleSpotifyPlaylist (message, query) { | ||
this.emit('playlistParseStart', {}, message) | ||
const playlist = await spotify.getData(query) | ||
if (!playlist) return this.emit('noResults', message, query) | ||
const tracks = [] | ||
let s = 0 | ||
for (let i = 0; i < playlist.tracks.items.length; i++) { | ||
const query = `${playlist.tracks.items[i].track.artists[0].name} - ${playlist.tracks.items[i].track.name}` | ||
const results = await ytsr.search(query, { type: 'video', limit: 1 }) | ||
if (results.length < 1) { | ||
s++ // could be used later for skipped tracks due to result not being found | ||
continue | ||
} | ||
tracks.push(results[0]) | ||
} | ||
playlist.tracks = tracks.map((item) => new Track(item, message.author)) | ||
playlist.duration = playlist.tracks.reduce((prev, next) => prev + next.duration, 0) | ||
playlist.thumbnail = playlist.images[0].url | ||
playlist.requestedBy = message.author | ||
this.emit('playlistParseEnd', playlist, message) | ||
if (this.isPlaying(message)) { | ||
const queue = this._addTracksToQueue(message, playlist.tracks) | ||
this.emit('playlistAdd', message, queue, playlist) | ||
} else { | ||
const track = playlist.tracks.shift() | ||
const queue = await this._createQueue(message, track).catch((e) => this.emit('error', e, message)) | ||
this.emit('trackStart', message, queue.tracks[0], queue) | ||
this._addTracksToQueue(message, playlist.tracks) | ||
} | ||
} | ||
async _handleSpotifyAlbum (message, query) { | ||
const album = await spotify.getData(query) | ||
if (!album) return this.emit('noResults', message, query) | ||
const tracks = [] | ||
let s = 0 | ||
for (let i = 0; i < album.tracks.items.length; i++) { | ||
const query = `${album.tracks.items[i].artists[0].name} - ${album.tracks.items[i].name}` | ||
const results = await ytsr.search(query, { type: 'video' }) | ||
if (results.length < 1) { | ||
s++ // could be used later for skipped tracks due to result not being found | ||
continue | ||
} | ||
tracks.push(results[0]) | ||
} | ||
album.tracks = tracks.map((item) => new Track(item, message.author)) | ||
album.duration = album.tracks.reduce((prev, next) => prev + next.duration, 0) | ||
album.thumbnail = album.images[0].url | ||
album.requestedBy = message.author | ||
if (this.isPlaying(message)) { | ||
const queue = this._addTracksToQueue(message, album.tracks) | ||
this.emit('playlistAdd', message, queue, album) | ||
} else { | ||
const track = album.tracks.shift() | ||
const queue = await this._createQueue(message, track).catch((e) => this.emit('error', e, message)) | ||
this.emit('trackStart', message, queue.tracks[0], queue) | ||
this._addTracksToQueue(message, album.tracks) | ||
} | ||
} | ||
async _handleSoundCloudPlaylist (message, query) { | ||
const data = await Client.getPlaylist(query).catch(() => {}) | ||
if (!data) return this.emit('noResults', message, query) | ||
const res = { | ||
id: data.id, | ||
title: data.title, | ||
tracks: [], | ||
author: data.author, | ||
duration: 0, | ||
thumbnail: data.thumbnail, | ||
requestedBy: message.author | ||
} | ||
this.emit('playlistParseStart', res, message) | ||
for (let i = 0; i < data.tracks.length; i++) { | ||
const song = data.tracks[i] | ||
const r = new Track({ | ||
title: song.title, | ||
url: song.url, | ||
lengthSeconds: song.duration / 1000, | ||
description: song.description, | ||
thumbnail: song.thumbnail || 'https://soundcloud.com/pwa-icon-192.png', | ||
views: song.playCount || 0, | ||
author: song.author || data.author | ||
}, message.author, this, true) | ||
Object.defineProperty(r, 'soundcloud', { | ||
get: () => song | ||
}) | ||
res.tracks.push(r) | ||
} | ||
if (!res.tracks.length) { | ||
this.emit('playlistParseEnd', res, message) | ||
return this.emit('error', 'ParseError', message) | ||
} | ||
res.duration = res.tracks.reduce((a, c) => a + c.lengthSeconds, 0) | ||
this.emit('playlistParseEnd', res, message) | ||
if (this.isPlaying(message)) { | ||
const queue = this._addTracksToQueue(message, res.tracks) | ||
this.emit('playlistAdd', message, queue, res) | ||
} else { | ||
const track = res.tracks.shift() | ||
const queue = await this._createQueue(message, track).catch((e) => this.emit('error', e, message)) | ||
this.emit('trackStart', message, queue.tracks[0], queue) | ||
this._addTracksToQueue(message, res.tracks) | ||
} | ||
} | ||
/** | ||
* Custom search function | ||
* @param {string} query Search query | ||
* @param {("youtube"|"soundcloud")} type Search type | ||
* @returns {Promise<any[]>} | ||
*/ | ||
async search (query, type = 'youtube') { | ||
if (!query || typeof query !== 'string') return [] | ||
switch (type.toLowerCase()) { | ||
case 'soundcloud': | ||
return await Client.search(query, 'track').catch(() => {}) || [] | ||
default: | ||
return await ytsr.search(query, { type: 'video' }).catch(() => {}) || [] | ||
} | ||
} | ||
/** | ||
* Play a track in the server. Supported query types are `keywords`, `YouTube video links`, `YouTube playlists links`, `Spotify track link` or `SoundCloud song link`. | ||
@@ -355,2 +709,3 @@ * @param {Discord.Message} message Discord `message` | ||
* @param {boolean} firstResult Whether the bot should play the first song found on youtube with the given query | ||
* @param {boolean} [isAttachment=false] If it should play it as attachment | ||
* @returns {Promise<void>} | ||
@@ -361,6 +716,26 @@ * | ||
*/ | ||
async play (message, query, firstResult = false) { | ||
if (this.util.isYTPlaylistLink(query)) { | ||
async play (message, query, firstResult = false, isAttachment = false) { | ||
if (this._cooldownsTimeout.has(`end_${message.guild.id}`)) { | ||
clearTimeout(this._cooldownsTimeout.get(`end_${message.guild.id}`)) | ||
this._cooldownsTimeout.delete(`end_${message.guild.id}`) | ||
} | ||
if (!query || typeof query !== 'string') throw new Error('Play function requires search query but received none!') | ||
// clean query | ||
query = query.replace(/<(.+)>/g, '$1') | ||
if (!this.util.isDiscordAttachment(query) && !isAttachment && this.util.isYTPlaylistLink(query)) { | ||
return this._handlePlaylist(message, query) | ||
} | ||
if (this.util.isSpotifyPLLink(query)) { | ||
return this._handleSpotifyPlaylist(message, query) | ||
} | ||
if (this.util.isSpotifyAlbumLink(query)) { | ||
return this._handleSpotifyAlbum(message, query) | ||
} | ||
if (this.util.isSoundcloudPlaylist(query)) { | ||
return this._handleSoundCloudPlaylist(message, query) | ||
} | ||
let trackToPlay | ||
@@ -371,3 +746,3 @@ if (query instanceof Track) { | ||
const videoData = await ytdl.getBasicInfo(query) | ||
if (videoData.videoDetails.isLiveContent) return this.emit('error', 'LiveVideo', message) | ||
if (videoData.videoDetails.isLiveContent && !this.options.enableLive) return this.emit('error', 'LiveVideo', message) | ||
const lastThumbnail = videoData.videoDetails.thumbnails.length - 1 /* get the highest quality thumbnail */ | ||
@@ -386,3 +761,3 @@ trackToPlay = new Track({ | ||
} else { | ||
trackToPlay = await this._searchTracks(message, query, firstResult) | ||
trackToPlay = await this._searchTracks(message, query, firstResult, !!isAttachment || this.util.isDiscordAttachment(query)) | ||
} | ||
@@ -395,3 +770,3 @@ if (trackToPlay) { | ||
const queue = await this._createQueue(message, trackToPlay) | ||
this.emit('trackStart', message, queue.tracks[0]) | ||
this.emit('trackStart', message, queue.tracks[0], queue) | ||
} | ||
@@ -426,3 +801,3 @@ } | ||
if (!queue) return this.emit('error', 'NotPlaying', message) | ||
// Pause the dispatcher | ||
// Resume the dispatcher | ||
queue.voiceConnection.dispatcher.resume() | ||
@@ -580,3 +955,9 @@ queue.paused = false | ||
const currentTrack = queue.tracks.shift() | ||
queue.tracks = queue.tracks.sort(() => Math.random() - 0.5) | ||
// Durstenfeld shuffle algorithm | ||
for (let i = queue.tracks.length - 1; i > 0; i--) { | ||
const j = Math.floor(Math.random() * (i + 1)); | ||
[queue.tracks[i], queue.tracks[j]] = [queue.tracks[j], queue.tracks[i]] | ||
} | ||
queue.tracks.unshift(currentTrack) | ||
@@ -617,4 +998,5 @@ // Return the queue | ||
* @param {Discord.Message} message | ||
* @param {Object} options | ||
* @param {boolean} options.timecodes | ||
* @param {Object} [options] | ||
* @param {boolean} [options.timecodes] Whether or not to show timecodes in the progress bar | ||
* @param {boolean} [options.queue] Whether to show the progress bar for the whole queue (if false, only the current song) | ||
* @returns {string} | ||
@@ -628,7 +1010,7 @@ */ | ||
// Stream time of the dispatcher | ||
const currentStreamTime = queue.voiceConnection.dispatcher | ||
? queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime | ||
: 0 | ||
const previousTracksTime = queue.previousTracks.length > 0 ? queue.previousTracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0 | ||
const currentStreamTime = options && options.queue ? previousTracksTime + queue.currentStreamTime : queue.currentStreamTime | ||
// Total stream time | ||
const totalTime = queue.playing.durationMS | ||
const totalTracksTime = queue.tracks.length > 0 ? queue.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0 | ||
const totalTime = options && options.queue ? previousTracksTime + totalTracksTime : queue.playing.durationMS | ||
// Stream progress | ||
@@ -641,4 +1023,5 @@ const index = Math.round((currentStreamTime / totalTime) * 15) | ||
if (timecodes) { | ||
const currentTimecode = (currentStreamTime >= 3600000 ? moment(currentStreamTime).format('H:mm:ss') : moment(currentStreamTime).format('m:ss')) | ||
return `${currentTimecode} ┃ ${bar.join('')} ┃ ${queue.playing.duration}` | ||
const currentTimecode = Util.buildTimecode(ms(currentStreamTime)) | ||
const endTimecode = Util.buildTimecode(ms(totalTime)) | ||
return `${currentTimecode} ┃ ${bar.join('')} ┃ ${endTimecode}` | ||
} else { | ||
@@ -649,4 +1032,5 @@ return `${bar.join('')}` | ||
if (timecodes) { | ||
const currentTimecode = (currentStreamTime >= 3600000 ? moment(currentStreamTime).format('H:mm:ss') : moment(currentStreamTime).format('m:ss')) | ||
return `${currentTimecode} ┃ 🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬ ┃ ${queue.playing.duration}` | ||
const currentTimecode = Util.buildTimecode(ms(currentStreamTime)) | ||
const endTimecode = Util.buildTimecode(ms(totalTime)) | ||
return `${currentTimecode} ┃ 🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬ ┃ ${endTimecode}` | ||
} else { | ||
@@ -675,28 +1059,40 @@ return '🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬' | ||
// check if the bot is in a channel | ||
if (!queue.voiceConnection || !queue.voiceConnection.channel) return | ||
// process leaveOnEmpty checks | ||
if (!this.options.leaveOnEmpty) return | ||
// If the member leaves a voice channel | ||
if (!oldState.channelID || newState.channelID) return | ||
// If the channel is not empty | ||
if (!this.util.isVoiceEmpty(queue.voiceConnection.channel)) return | ||
setTimeout(() => { | ||
// If the member joins a voice channel | ||
if (!oldState.channelID || newState.channelID) { | ||
const emptyTimeout = this._cooldownsTimeout.get(`empty_${oldState.guild.id}`) | ||
const channelEmpty = this.util.isVoiceEmpty(queue.voiceConnection.channel) | ||
if (!channelEmpty && emptyTimeout) { | ||
clearTimeout(emptyTimeout) | ||
this._cooldownsTimeout.delete(`empty_${oldState.guild.id}`) | ||
} | ||
} else { | ||
// If the channel is not empty | ||
if (!this.util.isVoiceEmpty(queue.voiceConnection.channel)) return | ||
if (!this.queues.has(queue.guildID)) return | ||
// Disconnect from the voice channel | ||
queue.voiceConnection.channel.leave() | ||
// Delete the queue | ||
this.queues.delete(queue.guildID) | ||
// Emit end event | ||
this.emit('channelEmpty', queue.firstMessage, queue) | ||
}, this.options.leaveOnEmptyCooldown || 0) | ||
const timeout = setTimeout(() => { | ||
if (!this.util.isVoiceEmpty(queue.voiceConnection.channel)) return | ||
if (!this.queues.has(queue.guildID)) return | ||
// Disconnect from the voice channel | ||
queue.voiceConnection.channel.leave() | ||
// Delete the queue | ||
this.queues.delete(queue.guildID) | ||
// Emit end event | ||
this.emit('channelEmpty', queue.firstMessage, queue) | ||
}, this.options.leaveOnEmptyCooldown || 0) | ||
this._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout) | ||
} | ||
} | ||
_playYTDLStream (queue, updateFilter) { | ||
return new Promise((resolve) => { | ||
const seekTime = updateFilter ? queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime : undefined | ||
_playYTDLStream (queue, updateFilter, seek) { | ||
return new Promise(async (resolve) => { | ||
const ffmeg = this.util.checkFFMPEG() | ||
if (!ffmeg) return | ||
const seekTime = typeof seek === 'number' ? seek : updateFilter ? queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime : undefined | ||
const encoderArgsFilters = [] | ||
Object.keys(queue.filters).forEach((filterName) => { | ||
if (queue.filters[filterName]) { | ||
encoderArgsFilters.push(filters[filterName]) | ||
encoderArgsFilters.push(this.filters[filterName]) | ||
} | ||
@@ -710,9 +1106,22 @@ }) | ||
} | ||
const newStream = ytdl(queue.playing.url, { | ||
filter: 'audioonly', | ||
opusEncoded: true, | ||
encoderArgs, | ||
seek: seekTime / 1000, | ||
highWaterMark: 1 << 25 | ||
}) | ||
let newStream | ||
if (!queue.playing.soundcloud && !queue.playing.arbitrary) { | ||
newStream = ytdl(queue.playing.url, { | ||
quality: this.options.quality === 'low' ? 'lowestaudio' : 'highestaudio', | ||
filter: 'audioonly', | ||
opusEncoded: true, | ||
encoderArgs, | ||
seek: seekTime / 1000, | ||
highWaterMark: 1 << 25, | ||
requestOptions: this.options.ytdlRequestOptions || {} | ||
}) | ||
} else { | ||
newStream = ytdl.arbitraryStream(queue.playing.soundcloud ? await queue.playing.soundcloud.downloadProgressive() : queue.playing.stream, { | ||
opusEncoded: true, | ||
encoderArgs, | ||
seek: seekTime / 1000 | ||
}) | ||
} | ||
setTimeout(() => { | ||
@@ -740,2 +1149,10 @@ if (queue.stream) queue.stream.destroy() | ||
}) | ||
newStream.on('error', (error) => { | ||
if (error.message.includes('Video unavailable')) { | ||
this.emit('error', 'VideoUnavailable', queue.firstMessage) | ||
this._playTrack(queue, false) | ||
} else { | ||
this.emit('error', error, queue.firstMessage) | ||
} | ||
}) | ||
}, 1000) | ||
@@ -753,11 +1170,14 @@ }) | ||
// If there isn't next music in the queue | ||
if (queue.tracks.length === 1 && !queue.repeatMode && !firstPlay) { | ||
if (queue.tracks.length === 1 && !queue.loopMode && !queue.repeatMode && !firstPlay) { | ||
// Leave the voice channel | ||
if (this.options.leaveOnEnd && !queue.stopped) { | ||
setTimeout(() => { | ||
// Remove the guild from the guilds list | ||
this.queues.delete(queue.guildID) | ||
const timeout = setTimeout(() => { | ||
queue.voiceConnection.channel.leave() | ||
// Remove the guild from the guilds list | ||
this.queues.delete(queue.guildID) | ||
}, this.options.leaveOnEndCooldown || 0) | ||
this._cooldownsTimeout.set(`end_${queue.guildID}`, timeout) | ||
} | ||
// Remove the guild from the guilds list | ||
this.queues.delete(queue.guildID) | ||
// Emit stop event | ||
@@ -791,4 +1211,4 @@ if (queue.stopped) { | ||
* @param {Discord.Message} message | ||
* @param {Track} track | ||
* @param {Queue} queue | ||
* @param {Track} track | ||
*/ | ||
@@ -876,4 +1296,18 @@ | ||
* @event Player#error | ||
* @param {string} error It can be `NotConnected`, `UnableToJoin` or `NotPlaying`. | ||
* @param {string} error It can be `NotConnected`, `UnableToJoin`, `NotPlaying`, `ParseError`, `LiveVideo` or `VideoUnavailable`. | ||
* @param {Discord.Message} message | ||
*/ | ||
/** | ||
* Emitted when discord-player attempts to parse playlist contents (mostly soundcloud playlists) | ||
* @event Player#playlistParseStart | ||
* @param {Object} playlist Raw playlist (unparsed) | ||
* @param {Discord.Message} message The message | ||
*/ | ||
/** | ||
* Emitted when discord-player finishes parsing playlist contents (mostly soundcloud playlists) | ||
* @event Player#playlistParseEnd | ||
* @param {Object} playlist The playlist data (parsed) | ||
* @param {Discord.Message} message The message | ||
*/ |
@@ -100,4 +100,10 @@ const Discord = require('discord.js') | ||
} | ||
get currentStreamTime () { | ||
return this.voiceConnection.dispatcher | ||
? this.voiceConnection.dispatcher.streamTime + this.additionalStreamTime | ||
: 0 | ||
} | ||
} | ||
module.exports = Queue |
@@ -45,3 +45,3 @@ const Discord = require('discord.js') | ||
*/ | ||
this.thumbnail = typeof videoData.thumbnail === 'object' | ||
this.thumbnail = videoData.thumbnail && typeof videoData.thumbnail === 'object' | ||
? videoData.thumbnail.url | ||
@@ -48,0 +48,0 @@ : videoData.thumbnail |
@@ -1,5 +0,10 @@ | ||
const ytsr = require('youtube-sr') | ||
const ytsr = require('youtube-sr').default | ||
const soundcloud = require('soundcloud-scraper') | ||
const chalk = require('chalk') | ||
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\/.*/) | ||
@@ -11,2 +16,17 @@ module.exports = class Util { | ||
static checkFFMPEG () { | ||
try { | ||
const prism = require('prism-media') | ||
prism.FFmpeg.getInfo() | ||
return true | ||
} catch { | ||
Util.alertFFMPEG() | ||
return false | ||
} | ||
} | ||
static alertFFMPEG () { | ||
console.log(chalk.red('ERROR:'), 'FFMPEG is not installed. Install with "npm install ffmpeg-static" or download it here: https://ffmpeg.org/download.html.') | ||
} | ||
static isVoiceEmpty (channel) { | ||
@@ -24,4 +44,12 @@ return channel.members.filter((member) => !member.user.bot).size === 0 | ||
static isSpotifyPLLink (query) { | ||
return spotifyPlaylistRegex.test(query) | ||
} | ||
static isSpotifyAlbumLink (query) { | ||
return spotifyAlbumRegex.test(query) | ||
} | ||
static isYTPlaylistLink (query) { | ||
return ytsr.validate(query, 'PLAYLIST') | ||
return ytsr.validate(query, 'PLAYLIST_ID') | ||
} | ||
@@ -32,2 +60,35 @@ | ||
} | ||
static isSoundcloudPlaylist (query) { | ||
return Util.isSoundcloudLink(query) && query.includes('/sets/') | ||
} | ||
static isVimeoLink (query) { | ||
return vimeoRegex.test(query) | ||
} | ||
static getVimeoID (query) { | ||
return Util.isVimeoLink(query) ? query.split('/').filter(x => !!x).pop() : null | ||
} | ||
static isFacebookLink (query) { | ||
return facebookRegex.test(query) | ||
} | ||
static isReverbnationLink (query) { | ||
return /https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)/.test(query) | ||
} | ||
static isDiscordAttachment (query) { | ||
return /https:\/\/cdn.discordapp.com\/attachments\/(\d{17,19})\/(\d{17,19})\/(.+)/.test(query) | ||
} | ||
static buildTimecode (data) { | ||
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 | ||
} | ||
} |
@@ -5,3 +5,3 @@ declare module 'discord-player' { | ||
import { Playlist as YTSRPlaylist } from 'youtube-sr'; | ||
import { Stream } from 'stream'; | ||
import { Stream, Readable } from 'stream'; | ||
@@ -16,2 +16,7 @@ export const version: string; | ||
static isYTVideoLink(query: string): boolean; | ||
static isSoundcloudPlaylist(query: string): boolean; | ||
static isVimeoLink(query: string): boolean; | ||
static getVimeoID(query: string): string; | ||
static isFacebookLink(query: string): boolean; | ||
static buildTimecode(data: any): string; | ||
} | ||
@@ -28,2 +33,3 @@ | ||
public static get AudioFilters(): PlayerFilters; | ||
public isPlaying(message: Message): boolean; | ||
@@ -44,3 +50,3 @@ public setFilters(message: Message, newFilters: Partial<Filters>): Promise<void>; | ||
public shuffle(message: Message): Queue; | ||
public remove(message: Message, trackOrPosition: Track|number): Track; | ||
public remove(message: Message, trackOrPosition: Track | number): Track; | ||
public createProgressBar(message: Message, progressBarOptions: ProgressBarOptions): string; | ||
@@ -52,2 +58,3 @@ | ||
} | ||
type MusicQuality = 'high' | 'low'; | ||
interface PlayerOptions { | ||
@@ -60,4 +67,34 @@ leaveOnEnd: boolean; | ||
autoSelfDeaf: boolean; | ||
quality: MusicQuality; | ||
} | ||
type Filters = 'bassboost' | '8D' | 'vaporwave' | 'nightcore'| 'phaser' | 'tremolo' | 'vibrato' | 'reverse' | 'treble' | 'normalizer' | 'surrounding' | 'pulsator' | 'subboost' | 'karaoke' | 'flanger' | 'gate' | 'haas' | 'mcompand'; | ||
type Filters = | ||
| 'bassboost' | ||
| '8D' | ||
| 'vaporwave' | ||
| 'nightcore' | ||
| 'phaser' | ||
| 'tremolo' | ||
| 'vibrato' | ||
| 'reverse' | ||
| 'treble' | ||
| 'normalizer' | ||
| 'surrounding' | ||
| 'pulsator' | ||
| 'subboost' | ||
| 'karaoke' | ||
| 'flanger' | ||
| 'gate' | ||
| 'haas' | ||
| 'mcompand' | ||
| 'mono' | ||
| 'mstlr' | ||
| 'mstrr' | ||
| 'compressor' | ||
| 'expander' | ||
| 'softlimiter' | ||
| 'chorus' | ||
| 'chorus2d' | ||
| 'chorus3d' | ||
| 'fadein'; | ||
type FiltersStatuses = { | ||
@@ -79,2 +116,3 @@ [key in Filters]: boolean; | ||
type Playlist = YTSRPlaylist & CustomPlaylist; | ||
type PlayerError = 'NotConnected' | 'UnableToJoin' | 'NotPlaying' | 'LiveVideo' | 'ParseError' | 'VideoUnavailable'; | ||
interface PlayerEvents { | ||
@@ -93,3 +131,5 @@ searchResults: [Message, string, Track[]]; | ||
queueEnd: [Message, Queue]; | ||
error: [string, Message]; | ||
error: [PlayerError, Message]; | ||
playlistParseStart: [any, Message]; | ||
playlistParseEnd: [any, Message]; | ||
} | ||
@@ -136,2 +176,98 @@ class Queue { | ||
} | ||
export interface RawExtractedData { | ||
title: string; | ||
format: string; | ||
size: number; | ||
sizeFormat: "MB"; | ||
stream: Readable; | ||
} | ||
export interface VimeoExtractedData { | ||
id: number; | ||
duration: number; | ||
title: string; | ||
url: string; | ||
thumbnail: string; | ||
width: number; | ||
height: number; | ||
stream: { | ||
cdn: string; | ||
fps: number; | ||
width: number; | ||
height: number; | ||
id: string; | ||
mime: string; | ||
origin: string; | ||
profile: number; | ||
quality: string; | ||
url: string; | ||
}; | ||
author: { | ||
accountType: string; | ||
id: number; | ||
name: string; | ||
url: string; | ||
avatar: string; | ||
} | ||
} | ||
interface FacebookExtractedData { | ||
name: string; | ||
title: string; | ||
description: string; | ||
rawVideo: string; | ||
thumbnail: string; | ||
uploadedAt: Date; | ||
duration: string; | ||
interactionCount: number; | ||
streamURL: string; | ||
publishedAt: Date; | ||
width: number; | ||
height: number; | ||
nsfw: boolean; | ||
genre: string; | ||
keywords: string[]; | ||
comments: number; | ||
size: string; | ||
quality: string; | ||
author: { | ||
type: string; | ||
name: string; | ||
url: string; | ||
}; | ||
publisher: { | ||
type: string; | ||
name: string; | ||
url: string; | ||
avatar: string; | ||
}; | ||
url: string; | ||
shares: string; | ||
views: string; | ||
} | ||
class Discord { | ||
static getInfo(url: string): Promise<RawExtractedData>; | ||
static download(url: string): Promise<Readable>; | ||
} | ||
class Facebook { | ||
static validateURL(url: string): boolean; | ||
static download(url: string): Promise<Readable>; | ||
static getInfo(url: string): Promise<FacebookExtractedData>; | ||
} | ||
class Vimeo { | ||
static getInfo(id: number): Promise<VimeoExtractedData>; | ||
static download(id: number): Promise<Readable>; | ||
} | ||
interface Extractors { | ||
DiscordExtractor: Discord; | ||
FacebookExtractor: Facebook; | ||
VimeoExtractor: Vimeo; | ||
} | ||
export const Extractors: Extractors; | ||
} |
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
87936
20
1996
179
11
11
1
7
+ Addedchalk@^4.1.0
+ Addedjsdom@^16.4.0
+ Addedparse-ms@^2.1.0
+ Addedreverbnation-scraper@^2.0.0
+ Added@tootallnate/once@1.1.2(transitive)
+ Addedabab@2.0.6(transitive)
+ Addedacorn@7.4.18.14.0(transitive)
+ Addedacorn-globals@6.0.0(transitive)
+ Addedacorn-walk@7.2.0(transitive)
+ Addedagent-base@6.0.2(transitive)
+ Addedansi-styles@4.3.0(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedbrowser-process-hrtime@1.0.0(transitive)
+ Addedcall-bind-apply-helpers@1.0.2(transitive)
+ Addedchalk@4.1.2(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcssom@0.3.80.4.4(transitive)
+ Addedcssstyle@2.3.0(transitive)
+ Addeddata-urls@2.0.0(transitive)
+ Addeddebug@4.4.0(transitive)
+ Addeddecimal.js@10.5.0(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addeddomexception@2.0.1(transitive)
+ Addeddunder-proto@1.0.1(transitive)
+ Addedes-define-property@1.0.1(transitive)
+ Addedes-errors@1.3.0(transitive)
+ Addedes-object-atoms@1.1.1(transitive)
+ Addedes-set-tostringtag@2.1.0(transitive)
+ Addedescodegen@2.1.0(transitive)
+ Addedesprima@4.0.1(transitive)
+ Addedestraverse@5.3.0(transitive)
+ Addedesutils@2.0.3(transitive)
+ Addedform-data@3.0.3(transitive)
+ Addedfunction-bind@1.1.2(transitive)
+ Addedget-intrinsic@1.2.7(transitive)
+ Addedget-proto@1.0.1(transitive)
+ Addedgopd@1.2.0(transitive)
+ Addedhas-flag@4.0.0(transitive)
+ Addedhas-symbols@1.1.0(transitive)
+ Addedhas-tostringtag@1.0.2(transitive)
+ Addedhasown@2.0.2(transitive)
+ Addedhtml-encoding-sniffer@2.0.1(transitive)
+ Addedhttp-proxy-agent@4.0.1(transitive)
+ Addedhttps-proxy-agent@5.0.1(transitive)
+ Addediconv-lite@0.4.24(transitive)
+ Addedis-potential-custom-element-name@1.0.1(transitive)
+ Addedjsdom@16.7.0(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addedmath-intrinsics@1.1.0(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedms@2.1.3(transitive)
+ Addednwsapi@2.2.16(transitive)
+ Addedparse-ms@2.1.0(transitive)
+ Addedparse5@6.0.1(transitive)
+ Addedpsl@1.15.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedquerystringify@2.2.0(transitive)
+ Addedrequires-port@1.0.0(transitive)
+ Addedreverbnation-scraper@2.0.0(transitive)
+ Addedsaxes@5.0.1(transitive)
+ Addedsource-map@0.6.1(transitive)
+ Addedsupports-color@7.2.0(transitive)
+ Addedsymbol-tree@3.2.4(transitive)
+ Addedtough-cookie@4.1.4(transitive)
+ Addedtr46@2.1.0(transitive)
+ Addeduniversalify@0.2.0(transitive)
+ Addedurl-parse@1.5.10(transitive)
+ Addedw3c-hr-time@1.0.2(transitive)
+ Addedw3c-xmlserializer@2.0.0(transitive)
+ Addedwebidl-conversions@5.0.06.1.0(transitive)
+ Addedwhatwg-encoding@1.0.5(transitive)
+ Addedwhatwg-mimetype@2.3.0(transitive)
+ Addedwhatwg-url@8.7.0(transitive)
+ Addedws@7.5.10(transitive)
+ Addedxml-name-validator@3.0.0(transitive)
+ Addedxmlchars@2.2.0(transitive)
+ Addedyoutube-sr@4.3.11(transitive)
- Removed@types/node@^14.14.7
- Removedmoment@^2.27.0
- Removed@types/node@14.18.63(transitive)
- Removedmoment@2.30.1(transitive)
- Removedyoutube-sr@2.0.5(transitive)
Updatedsoundcloud-scraper@^4.0.3
Updatedyoutube-sr@^4.0.0
Updatedytdl-core@^4.5.0