✨New player features (#236)
Co-authored-by: Androz <androz2091@gmail.com>
This commit is contained in:
parent
00f77fbd67
commit
c804bd671f
11 changed files with 884 additions and 64 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,3 +4,4 @@ package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
docs
|
docs
|
||||||
.vscode
|
.vscode
|
||||||
|
test
|
3
index.js
3
index.js
|
@ -1,6 +1,7 @@
|
||||||
process.env.YTDL_NO_UPDATE = true;
|
process.env.YTDL_NO_UPDATE = true
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
Extractors: require('./src/Extractors/Extractor'),
|
||||||
version: require('./package.json').version,
|
version: require('./package.json').version,
|
||||||
Player: require('./src/Player')
|
Player: require('./src/Player')
|
||||||
}
|
}
|
||||||
|
|
11
package.json
11
package.json
|
@ -6,7 +6,7 @@
|
||||||
"types": "typings/index.d.ts",
|
"types": "typings/index.d.ts",
|
||||||
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
|
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node index.js",
|
"test": "cd test && node index.js",
|
||||||
"generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose"
|
"generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -33,13 +33,16 @@
|
||||||
"@types/node": "^14.14.7",
|
"@types/node": "^14.14.7",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"discord-ytdl-core": "^5.0.0",
|
"discord-ytdl-core": "^5.0.0",
|
||||||
|
"jsdom": "^16.4.0",
|
||||||
"merge-options": "^3.0.4",
|
"merge-options": "^3.0.4",
|
||||||
"moment": "^2.27.0",
|
|
||||||
"node-fetch": "^2.6.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",
|
"spotify-url-info": "^2.2.0",
|
||||||
|
"xvdl": "^1.0.2",
|
||||||
"youtube-sr": "^3.0.0",
|
"youtube-sr": "^3.0.0",
|
||||||
"ytdl-core": "^4.4.2"
|
"ytdl-core": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@discordjs/opus": "^0.3.2",
|
"@discordjs/opus": "^0.3.2",
|
||||||
|
|
46
src/Extractors/Discord.js
Normal file
46
src/Extractors/Discord.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
const https = require('https')
|
||||||
|
|
||||||
|
class Discord {
|
||||||
|
constructor () {
|
||||||
|
throw new Error(`The ${this.constructor.name} class may not be instantiated!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('stream').Readable} Readable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads discord attachment
|
||||||
|
* @param {string} url Discord attachment url
|
||||||
|
* @returns {Promise<Readable>}
|
||||||
|
*/
|
||||||
|
static async download (url) {
|
||||||
|
const data = await Discord.getInfo(url)
|
||||||
|
return data.stream
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns discord attachment info
|
||||||
|
* @param {string} url Attachment url
|
||||||
|
*/
|
||||||
|
static getInfo (url) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
https.get(url, res => {
|
||||||
|
const data = {
|
||||||
|
title: res.req.path.split('/').pop(),
|
||||||
|
format: res.headers['content-type'],
|
||||||
|
size: !isNaN(res.headers['content-length']) ? Math.round((parseInt(res.headers['content-length']) / (1024 * 1024)) * 100) / 100 : 0,
|
||||||
|
sizeFormat: 'MB'
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(data, 'stream', {
|
||||||
|
get: () => res
|
||||||
|
})
|
||||||
|
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Discord
|
7
src/Extractors/Extractor.js
Normal file
7
src/Extractors/Extractor.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
DiscordExtractor: require('./Discord'),
|
||||||
|
FacebookExtractor: require('./Facebook'),
|
||||||
|
ReverbnationExtractor: require('reverbnation-scraper'),
|
||||||
|
VimeoExtractor: require('./Vimeo'),
|
||||||
|
XVideosExtractor: require('xvdl').XVDL
|
||||||
|
}
|
148
src/Extractors/Facebook.js
Normal file
148
src/Extractors/Facebook.js
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
const fetch = require('node-fetch').default
|
||||||
|
const { JSDOM } = require('jsdom')
|
||||||
|
|
||||||
|
class Facebook {
|
||||||
|
constructor () {
|
||||||
|
throw new Error(`The ${this.constructor.name} class may not be instantiated!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates facebook url
|
||||||
|
* @param {string} url URL to validate
|
||||||
|
*/
|
||||||
|
static validateURL (url) {
|
||||||
|
const REGEX = /(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/
|
||||||
|
if (!url || typeof url !== 'string') return false
|
||||||
|
return REGEX.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('stream').Readable} Readable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads facebook video
|
||||||
|
* @param {string} url Video url to download
|
||||||
|
* @returns {Promise<Readable>}
|
||||||
|
*/
|
||||||
|
static download (url) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!Facebook.validateURL(url)) reject(new Error('Invalid url.'))
|
||||||
|
const info = await Facebook.getInfo(url)
|
||||||
|
if (!info || !info.streamURL) return reject(new Error('video not found'))
|
||||||
|
const link = info.streamURL
|
||||||
|
let req = require('https')
|
||||||
|
|
||||||
|
if (link.startsWith('http://')) req = require('http')
|
||||||
|
|
||||||
|
req.get(link, res => {
|
||||||
|
resolve(res)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches facebook video info
|
||||||
|
* @param {string} url Facebook video url
|
||||||
|
*/
|
||||||
|
static async getInfo (url) {
|
||||||
|
if (!Facebook.validateURL(url)) throw new Error('Invalid url.')
|
||||||
|
try {
|
||||||
|
const html = await Facebook._parseHTML(url)
|
||||||
|
const document = new JSDOM(html).window.document
|
||||||
|
const rawdata = document.querySelector('script[type="application/ld+json"]').innerHTML
|
||||||
|
const json = JSON.parse(rawdata)
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
name: json.name,
|
||||||
|
title: document.querySelector('meta[property="og:title"]').attributes.item(1).value,
|
||||||
|
description: json.description,
|
||||||
|
rawVideo: json.contentUrl,
|
||||||
|
thumbnail: json.thumbnailUrl,
|
||||||
|
uploadedAt: new Date(json.uploadDate),
|
||||||
|
duration: Facebook.parseTime(json.duration),
|
||||||
|
interactionCount: json.interactionCount,
|
||||||
|
streamURL: json.url,
|
||||||
|
publishedAt: new Date(json.datePublished),
|
||||||
|
width: json.width,
|
||||||
|
height: json.height,
|
||||||
|
live: !!json.publication[0].isLiveBroadcast,
|
||||||
|
nsfw: !json.isFamilyFriendly,
|
||||||
|
genre: json.genre,
|
||||||
|
keywords: json.keywords ? json.keywords.split(', ') : [],
|
||||||
|
comments: json.commentCount,
|
||||||
|
size: json.contentSize,
|
||||||
|
quality: json.videoQuality,
|
||||||
|
author: {
|
||||||
|
type: json.author['@type'],
|
||||||
|
name: json.author.name,
|
||||||
|
url: json.author.url
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
type: json.publisher['@type'],
|
||||||
|
name: json.publisher.name,
|
||||||
|
url: json.publisher.url,
|
||||||
|
avatar: json.publisher.logo.url
|
||||||
|
},
|
||||||
|
url: html.split('",page_uri:"')[1].split('",')[0],
|
||||||
|
shares: html.split(',share_count:{')[1].split('},')[0].split(':')[1],
|
||||||
|
views: html.split(',video_view_count:')[1].split(',')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses time in ms
|
||||||
|
* @param {string} duration Raw duration to parse
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static parseTime (duration) {
|
||||||
|
if (typeof duration !== 'string') return duration
|
||||||
|
let a = duration.match(/\d+/g)
|
||||||
|
|
||||||
|
if (duration.indexOf('M') >= 0 && duration.indexOf('H') === -1 && duration.indexOf('S') === -1) {
|
||||||
|
a = [0, a[0], 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1) {
|
||||||
|
a = [a[0], 0, a[1]]
|
||||||
|
}
|
||||||
|
if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1 && duration.indexOf('S') === -1) {
|
||||||
|
a = [a[0], 0, 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
duration = 0
|
||||||
|
|
||||||
|
if (a.length === 3) {
|
||||||
|
duration = duration + parseInt(a[0]) * 3600
|
||||||
|
duration = duration + parseInt(a[1]) * 60
|
||||||
|
duration = duration + parseInt(a[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.length === 2) {
|
||||||
|
duration = duration + parseInt(a[0]) * 60
|
||||||
|
duration = duration + parseInt(a[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.length === 1) {
|
||||||
|
duration = duration + parseInt(a[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ignore
|
||||||
|
* @param {string} url website url to parse html
|
||||||
|
*/
|
||||||
|
static async _parseHTML (url) {
|
||||||
|
const res = await fetch(url.replace('/m.', '/'))
|
||||||
|
return await res.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Facebook
|
68
src/Extractors/Vimeo.js
Normal file
68
src/Extractors/Vimeo.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
const fetch = require('node-fetch').default
|
||||||
|
|
||||||
|
class Vimeo {
|
||||||
|
constructor () {
|
||||||
|
throw new Error(`The ${this.constructor.name} class may not be instantiated!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('stream').Readable} Readable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads from vimeo
|
||||||
|
* @param {number} id Vimeo video id
|
||||||
|
* @returns {Promise<Readable>}
|
||||||
|
*/
|
||||||
|
static download (id) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const info = await Vimeo.getInfo(id)
|
||||||
|
if (!info) return null
|
||||||
|
|
||||||
|
const downloader = info.stream.url.startsWith('https://') ? require('https') : require('http')
|
||||||
|
|
||||||
|
downloader.get(info.stream.url, res => {
|
||||||
|
resolve(res)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns video info
|
||||||
|
* @param {number} id Video id
|
||||||
|
*/
|
||||||
|
static async getInfo (id) {
|
||||||
|
if (!id) throw new Error('Invalid id')
|
||||||
|
const url = `https://player.vimeo.com/video/${id}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url)
|
||||||
|
const data = await res.text()
|
||||||
|
const json = JSON.parse(data.split('<script> (function(document, player) { var config = ')[1].split(';')[0])
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
id: json.video.id,
|
||||||
|
duration: json.video.duration,
|
||||||
|
title: json.video.title,
|
||||||
|
url: json.video.url,
|
||||||
|
thumbnail: json.video.thumbs['1280'] || json.video.thumbs.base,
|
||||||
|
width: json.video.width,
|
||||||
|
height: json.video.height,
|
||||||
|
stream: json.request.files.progressive[0],
|
||||||
|
author: {
|
||||||
|
accountType: json.video.owner.account_type,
|
||||||
|
id: json.video.owner.id,
|
||||||
|
name: json.video.owner.name,
|
||||||
|
url: json.video.owner.url,
|
||||||
|
avatar: json.video.owner.img_2x || json.video.owner.img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Vimeo
|
435
src/Player.js
435
src/Player.js
|
@ -1,14 +1,15 @@
|
||||||
const ytdl = require('discord-ytdl-core')
|
const ytdl = require('discord-ytdl-core')
|
||||||
const Discord = require('discord.js')
|
const Discord = require('discord.js')
|
||||||
const ytsr = require('youtube-sr')
|
const ytsr = require('youtube-sr').default
|
||||||
const spotify = require('spotify-url-info')
|
const spotify = require('spotify-url-info')
|
||||||
const soundcloud = require('soundcloud-scraper')
|
const soundcloud = require('soundcloud-scraper')
|
||||||
const moment = require('moment')
|
const ms = require('parse-ms')
|
||||||
const Queue = require('./Queue')
|
const Queue = require('./Queue')
|
||||||
const Track = require('./Track')
|
const Track = require('./Track')
|
||||||
const Util = require('./Util')
|
const Util = require('./Util')
|
||||||
const { EventEmitter } = require('events')
|
const { EventEmitter } = require('events')
|
||||||
const Client = new soundcloud.Client()
|
const Client = new soundcloud.Client()
|
||||||
|
const { VimeoExtractor, DiscordExtractor, FacebookExtractor, ReverbnationExtractor, XVideosExtractor } = require('./Extractors/Extractor')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef Filters
|
* @typedef Filters
|
||||||
|
@ -31,6 +32,15 @@ const Client = new soundcloud.Client()
|
||||||
* @property {boolean} [haas=false] Whether the haas filter is enabled.
|
* @property {boolean} [haas=false] Whether the haas filter is enabled.
|
||||||
* @property {boolean} [mcompand=false] Whether the mcompand filter is enabled.
|
* @property {boolean} [mcompand=false] Whether the mcompand filter is enabled.
|
||||||
* @property {boolean} [mono=false] Whether the mono output is enabled.
|
* @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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
|
@ -52,7 +62,16 @@ const filters = {
|
||||||
gate: 'agate',
|
gate: 'agate',
|
||||||
haas: 'haas',
|
haas: 'haas',
|
||||||
mcompand: 'mcompand',
|
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'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -64,6 +83,8 @@ const filters = {
|
||||||
* @property {number} [leaveOnEmptyCooldown=0] Used when leaveOnEmpty is enabled, to let the time to users to come back in the voice channel.
|
* @property {number} [leaveOnEmptyCooldown=0] Used when leaveOnEmpty is enabled, to let the time to users to come back in the voice channel.
|
||||||
* @property {boolean} [autoSelfDeaf=true] Whether the bot should automatically turn off its headphones when joining a voice channel.
|
* @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 {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..
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,7 +98,9 @@ const defaultPlayerOptions = {
|
||||||
leaveOnEmpty: true,
|
leaveOnEmpty: true,
|
||||||
leaveOnEmptyCooldown: 0,
|
leaveOnEmptyCooldown: 0,
|
||||||
autoSelfDeaf: true,
|
autoSelfDeaf: true,
|
||||||
quality: 'high'
|
quality: 'high',
|
||||||
|
enableLive: false,
|
||||||
|
ytdlRequestOptions: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Player extends EventEmitter {
|
class Player extends EventEmitter {
|
||||||
|
@ -94,7 +117,7 @@ class Player extends EventEmitter {
|
||||||
* @type {Util}
|
* @type {Util}
|
||||||
*/
|
*/
|
||||||
this.util = Util
|
this.util = Util
|
||||||
this.util.checkFFMPEG();
|
this.util.checkFFMPEG()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discord.js client instance
|
* Discord.js client instance
|
||||||
|
@ -130,11 +153,23 @@ class Player extends EventEmitter {
|
||||||
this._resultsCollectors = new Discord.Collection()
|
this._resultsCollectors = 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
|
* @ignore
|
||||||
* @param {String} query
|
* @param {String} query
|
||||||
*/
|
*/
|
||||||
resolveQueryType (query) {
|
resolveQueryType (query, forceType) {
|
||||||
|
if (forceType && typeof forceType === 'string') return forceType
|
||||||
|
|
||||||
if (this.util.isSpotifyLink(query)) {
|
if (this.util.isSpotifyLink(query)) {
|
||||||
return 'spotify-song'
|
return 'spotify-song'
|
||||||
} else if (this.util.isYTPlaylistLink(query)) {
|
} else if (this.util.isYTPlaylistLink(query)) {
|
||||||
|
@ -145,6 +180,16 @@ class Player extends EventEmitter {
|
||||||
return 'soundcloud-song'
|
return 'soundcloud-song'
|
||||||
} else if (this.util.isSpotifyPLLink(query)) {
|
} else if (this.util.isSpotifyPLLink(query)) {
|
||||||
return 'spotify-playlist'
|
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 if (this.util.isXVLink(query)) {
|
||||||
|
return 'xvlink'
|
||||||
} else {
|
} else {
|
||||||
return 'youtube-video-keywords'
|
return 'youtube-video-keywords'
|
||||||
}
|
}
|
||||||
|
@ -175,16 +220,154 @@ class Player extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (queryType === 'soundcloud-song') {
|
} else if (queryType === 'soundcloud-song') {
|
||||||
const soundcloudData = await Client.getSongInfo(query).catch(() => {})
|
const data = await Client.getSongInfo(query).catch(() => { })
|
||||||
if (soundcloudData) {
|
if (data) {
|
||||||
updatedQuery = `${soundcloudData.author.name} - ${soundcloudData.title}`
|
const track = new Track({
|
||||||
queryType = 'youtube-video-keywords'
|
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 (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)
|
||||||
|
} else if (queryType === 'xvlink') {
|
||||||
|
const data = await XVideosExtractor.getInfo(query).catch(() => {})
|
||||||
|
if (!data || !(data.streams.lq || data.streams.hq)) return this.emit('noResults', message, query)
|
||||||
|
|
||||||
|
const track = new Track({
|
||||||
|
title: data.title,
|
||||||
|
url: data.url,
|
||||||
|
thumbnail: data.thumbnail,
|
||||||
|
lengthSeconds: data.length,
|
||||||
|
description: '',
|
||||||
|
views: data.views,
|
||||||
|
author: {
|
||||||
|
name: data.channel.name
|
||||||
|
}
|
||||||
|
}, message.author, this)
|
||||||
|
|
||||||
|
Object.defineProperties(track, {
|
||||||
|
arbitrary: {
|
||||||
|
get: () => true
|
||||||
|
},
|
||||||
|
stream: {
|
||||||
|
get: () => data.streams.lq || data.streams.hq
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tracks.push(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryType === 'youtube-video-keywords') {
|
if (queryType === 'youtube-video-keywords') {
|
||||||
await ytsr.search(updatedQuery || query, { type: 'video' }).then((results) => {
|
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))
|
tracks = results.map((r) => new Track(r, message.author, this))
|
||||||
}
|
}
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
|
@ -247,6 +430,37 @@ class Player extends EventEmitter {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Check whether there is a music played in the server
|
||||||
* @param {Discord.Message} message
|
* @param {Discord.Message} message
|
||||||
|
@ -255,6 +469,23 @@ class Player extends EventEmitter {
|
||||||
return this.queues.some((g) => g.guildID === message.guild.id)
|
return this.queues.some((g) => g.guildID === message.guild.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
message.guild.voice.setChannel(channel)
|
||||||
|
.then(() => queue.voiceConnection.dispatcher.resume())
|
||||||
|
.catch(() => this.emit('error', 'UnableToJoin', message))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a track to the queue
|
* Add a track to the queue
|
||||||
* @ignore
|
* @ignore
|
||||||
|
@ -319,12 +550,16 @@ class Player extends EventEmitter {
|
||||||
* @param {String} query
|
* @param {String} query
|
||||||
*/
|
*/
|
||||||
async _handlePlaylist (message, query) {
|
async _handlePlaylist (message, query) {
|
||||||
|
this.emit('playlistParseStart', {}, message)
|
||||||
const playlist = await ytsr.getPlaylist(query)
|
const playlist = await ytsr.getPlaylist(query)
|
||||||
if (!playlist) return this.emit('noResults', message, query)
|
if (!playlist) return this.emit('noResults', message, query)
|
||||||
playlist.tracks = playlist.videos.map((item) => new Track(item, message.author, this, true))
|
playlist.tracks = playlist.videos.map((item) => new Track(item, message.author, this, true))
|
||||||
playlist.duration = playlist.tracks.reduce((prev, next) => prev + next.duration, 0)
|
playlist.duration = playlist.tracks.reduce((prev, next) => prev + next.duration, 0)
|
||||||
playlist.thumbnail = playlist.tracks[0].thumbnail
|
playlist.thumbnail = playlist.tracks[0].thumbnail
|
||||||
playlist.requestedBy = message.author
|
playlist.requestedBy = message.author
|
||||||
|
|
||||||
|
this.emit('playlistParseEnd', playlist, message)
|
||||||
|
|
||||||
if (this.isPlaying(message)) {
|
if (this.isPlaying(message)) {
|
||||||
const queue = this._addTracksToQueue(message, playlist.tracks)
|
const queue = this._addTracksToQueue(message, playlist.tracks)
|
||||||
this.emit('playlistAdd', message, queue, playlist)
|
this.emit('playlistAdd', message, queue, playlist)
|
||||||
|
@ -335,17 +570,19 @@ class Player extends EventEmitter {
|
||||||
this._addTracksToQueue(message, playlist.tracks)
|
this._addTracksToQueue(message, playlist.tracks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handleSpotifyPlaylist (message, query) {
|
async _handleSpotifyPlaylist (message, query) {
|
||||||
|
this.emit('playlistParseStart', {}, message)
|
||||||
const playlist = await spotify.getData(query)
|
const playlist = await spotify.getData(query)
|
||||||
if (!playlist) return this.emit('noResults', message, query)
|
if (!playlist) return this.emit('noResults', message, query)
|
||||||
let tracks = []
|
const tracks = []
|
||||||
let s;
|
let s = 0
|
||||||
for (var i = 0; i < playlist.tracks.items.length; i++) {
|
for (let i = 0; i < playlist.tracks.items.length; i++) {
|
||||||
let query = `${playlist.tracks.items[i].track.artists[0].name} - ${playlist.tracks.items[i].track.name}`
|
const query = `${playlist.tracks.items[i].track.artists[0].name} - ${playlist.tracks.items[i].track.name}`
|
||||||
let results = await ytsr.search(query, { type: 'video' })
|
const results = await ytsr.search(query, { type: 'video', limit: 1 })
|
||||||
if (results.length < 1) {
|
if (results.length < 1) {
|
||||||
s++ // could be used later for skipped tracks due to result not being found
|
s++ // could be used later for skipped tracks due to result not being found
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
tracks.push(results[0])
|
tracks.push(results[0])
|
||||||
}
|
}
|
||||||
|
@ -353,6 +590,8 @@ class Player extends EventEmitter {
|
||||||
playlist.duration = playlist.tracks.reduce((prev, next) => prev + next.duration, 0)
|
playlist.duration = playlist.tracks.reduce((prev, next) => prev + next.duration, 0)
|
||||||
playlist.thumbnail = playlist.images[0].url
|
playlist.thumbnail = playlist.images[0].url
|
||||||
playlist.requestedBy = message.author
|
playlist.requestedBy = message.author
|
||||||
|
|
||||||
|
this.emit('playlistParseEnd', playlist, message)
|
||||||
if (this.isPlaying(message)) {
|
if (this.isPlaying(message)) {
|
||||||
const queue = this._addTracksToQueue(message, playlist.tracks)
|
const queue = this._addTracksToQueue(message, playlist.tracks)
|
||||||
this.emit('playlistAdd', message, queue, playlist)
|
this.emit('playlistAdd', message, queue, playlist)
|
||||||
|
@ -363,17 +602,18 @@ class Player extends EventEmitter {
|
||||||
this._addTracksToQueue(message, playlist.tracks)
|
this._addTracksToQueue(message, playlist.tracks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handleSpotifyAlbum (message, query) {
|
async _handleSpotifyAlbum (message, query) {
|
||||||
const album = await spotify.getData(query)
|
const album = await spotify.getData(query)
|
||||||
if (!album) return this.emit('noResults', message, query)
|
if (!album) return this.emit('noResults', message, query)
|
||||||
let tracks = []
|
const tracks = []
|
||||||
let s;
|
let s = 0
|
||||||
for (var i = 0; i < album.tracks.items.length; i++) {
|
for (let i = 0; i < album.tracks.items.length; i++) {
|
||||||
let query = `${album.tracks.items[i].artists[0].name} - ${album.tracks.items[i].name}`
|
const query = `${album.tracks.items[i].artists[0].name} - ${album.tracks.items[i].name}`
|
||||||
let results = await ytsr.search(query, { type: 'video' })
|
const results = await ytsr.search(query, { type: 'video' })
|
||||||
if (results.length < 1) {
|
if (results.length < 1) {
|
||||||
s++ // could be used later for skipped tracks due to result not being found
|
s++ // could be used later for skipped tracks due to result not being found
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
tracks.push(results[0])
|
tracks.push(results[0])
|
||||||
}
|
}
|
||||||
|
@ -392,6 +632,84 @@ class Player extends EventEmitter {
|
||||||
this._addTracksToQueue(message, album.tracks)
|
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])
|
||||||
|
this._addTracksToQueue(message, res.tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom search function
|
||||||
|
* @param {string} query Search query
|
||||||
|
* @param {("youtube"|"soundcloud"|"xvideos")} 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(() => {}) || []
|
||||||
|
case 'xvideos':
|
||||||
|
// eslint-disable-next-line
|
||||||
|
let videos = await XVideosExtractor.search(query).catch(() => {})
|
||||||
|
if (!videos) return []
|
||||||
|
return videos.videos
|
||||||
|
default:
|
||||||
|
return 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`.
|
* Play a track in the server. Supported query types are `keywords`, `YouTube video links`, `YouTube playlists links`, `Spotify track link` or `SoundCloud song link`.
|
||||||
* @param {Discord.Message} message Discord `message`
|
* @param {Discord.Message} message Discord `message`
|
||||||
|
@ -403,6 +721,11 @@ class Player extends EventEmitter {
|
||||||
* client.player.play(message, "Despacito", true);
|
* client.player.play(message, "Despacito", true);
|
||||||
*/
|
*/
|
||||||
async play (message, query, firstResult = false) {
|
async play (message, query, firstResult = false) {
|
||||||
|
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.isYTPlaylistLink(query)) {
|
if (this.util.isYTPlaylistLink(query)) {
|
||||||
return this._handlePlaylist(message, query)
|
return this._handlePlaylist(message, query)
|
||||||
}
|
}
|
||||||
|
@ -412,12 +735,16 @@ class Player extends EventEmitter {
|
||||||
if (this.util.isSpotifyAlbumLink(query)) {
|
if (this.util.isSpotifyAlbumLink(query)) {
|
||||||
return this._handleSpotifyAlbum(message, query)
|
return this._handleSpotifyAlbum(message, query)
|
||||||
}
|
}
|
||||||
|
if (this.util.isSoundcloudPlaylist(query)) {
|
||||||
|
return this._handleSoundCloudPlaylist(message, query)
|
||||||
|
}
|
||||||
|
|
||||||
let trackToPlay
|
let trackToPlay
|
||||||
if (query instanceof Track) {
|
if (query instanceof Track) {
|
||||||
trackToPlay = query
|
trackToPlay = query
|
||||||
} else if (this.util.isYTVideoLink(query)) {
|
} else if (this.util.isYTVideoLink(query)) {
|
||||||
const videoData = await ytdl.getBasicInfo(query)
|
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 */
|
const lastThumbnail = videoData.videoDetails.thumbnails.length - 1 /* get the highest quality thumbnail */
|
||||||
trackToPlay = new Track({
|
trackToPlay = new Track({
|
||||||
title: videoData.videoDetails.title,
|
title: videoData.videoDetails.title,
|
||||||
|
@ -622,7 +949,13 @@ class Player extends EventEmitter {
|
||||||
if (!queue) return this.emit('error', 'NotPlaying', message)
|
if (!queue) return this.emit('error', 'NotPlaying', message)
|
||||||
// Shuffle the queue (except the first track)
|
// Shuffle the queue (except the first track)
|
||||||
const currentTrack = queue.tracks.shift()
|
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)
|
queue.tracks.unshift(currentTrack)
|
||||||
// Return the queue
|
// Return the queue
|
||||||
return queue
|
return queue
|
||||||
|
@ -678,14 +1011,16 @@ class Player extends EventEmitter {
|
||||||
const bar = '▬▬▬▬▬▬▬▬▬▬▬▬▬▬'.split('')
|
const bar = '▬▬▬▬▬▬▬▬▬▬▬▬▬▬'.split('')
|
||||||
bar.splice(index, 0, '🔘')
|
bar.splice(index, 0, '🔘')
|
||||||
if (timecodes) {
|
if (timecodes) {
|
||||||
const currentTimecode = (currentStreamTime >= 3600000 ? moment(currentStreamTime).format('H:mm:ss') : moment(currentStreamTime).format('m:ss'))
|
const parsed = ms(currentStreamTime)
|
||||||
|
const currentTimecode = Util.buildTimecode(parsed)
|
||||||
return `${currentTimecode} ┃ ${bar.join('')} ┃ ${queue.playing.duration}`
|
return `${currentTimecode} ┃ ${bar.join('')} ┃ ${queue.playing.duration}`
|
||||||
} else {
|
} else {
|
||||||
return `${bar.join('')}`
|
return `${bar.join('')}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (timecodes) {
|
if (timecodes) {
|
||||||
const currentTimecode = (currentStreamTime >= 3600000 ? moment(currentStreamTime).format('H:mm:ss') : moment(currentStreamTime).format('m:ss'))
|
const parsed = ms(currentStreamTime)
|
||||||
|
const currentTimecode = Util.buildTimecode(parsed)
|
||||||
return `${currentTimecode} ┃ 🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬ ┃ ${queue.playing.duration}`
|
return `${currentTimecode} ┃ 🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬ ┃ ${queue.playing.duration}`
|
||||||
} else {
|
} else {
|
||||||
return '🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬'
|
return '🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬'
|
||||||
|
@ -729,11 +1064,11 @@ class Player extends EventEmitter {
|
||||||
}, this.options.leaveOnEmptyCooldown || 0)
|
}, this.options.leaveOnEmptyCooldown || 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
_playYTDLStream (queue, updateFilter) {
|
_playYTDLStream (queue, updateFilter, seek) {
|
||||||
return new Promise((resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const ffmeg = this.util.checkFFMPEG();
|
const ffmeg = this.util.checkFFMPEG()
|
||||||
if (!ffmeg) return;
|
if (!ffmeg) return
|
||||||
const seekTime = updateFilter ? queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime : undefined
|
const seekTime = typeof seek === 'number' ? seek : updateFilter ? queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime : undefined
|
||||||
const encoderArgsFilters = []
|
const encoderArgsFilters = []
|
||||||
Object.keys(queue.filters).forEach((filterName) => {
|
Object.keys(queue.filters).forEach((filterName) => {
|
||||||
if (queue.filters[filterName]) {
|
if (queue.filters[filterName]) {
|
||||||
|
@ -746,14 +1081,26 @@ class Player extends EventEmitter {
|
||||||
} else {
|
} else {
|
||||||
encoderArgs = ['-af', encoderArgsFilters.join(',')]
|
encoderArgs = ['-af', encoderArgsFilters.join(',')]
|
||||||
}
|
}
|
||||||
const newStream = ytdl(queue.playing.url, {
|
|
||||||
|
let newStream
|
||||||
|
if (!queue.playing.soundcloud && !queue.playing.arbitrary) {
|
||||||
|
newStream = ytdl(queue.playing.url, {
|
||||||
quality: this.options.quality === 'low' ? 'lowestaudio' : 'highestaudio',
|
quality: this.options.quality === 'low' ? 'lowestaudio' : 'highestaudio',
|
||||||
filter: 'audioonly',
|
filter: 'audioonly',
|
||||||
opusEncoded: true,
|
opusEncoded: true,
|
||||||
encoderArgs,
|
encoderArgs,
|
||||||
seek: seekTime / 1000,
|
seek: seekTime / 1000,
|
||||||
highWaterMark: 1 << 25
|
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(() => {
|
setTimeout(() => {
|
||||||
if (queue.stream) queue.stream.destroy()
|
if (queue.stream) queue.stream.destroy()
|
||||||
queue.stream = newStream
|
queue.stream = newStream
|
||||||
|
@ -793,10 +1140,10 @@ class Player extends EventEmitter {
|
||||||
if (this.options.leaveOnEnd && !queue.stopped) {
|
if (this.options.leaveOnEnd && !queue.stopped) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
queue.voiceConnection.channel.leave()
|
queue.voiceConnection.channel.leave()
|
||||||
// Remove the guild from the guilds list
|
|
||||||
this.queues.delete(queue.guildID)
|
|
||||||
}, this.options.leaveOnEndCooldown || 0)
|
}, this.options.leaveOnEndCooldown || 0)
|
||||||
}
|
}
|
||||||
|
// Remove the guild from the guilds list
|
||||||
|
this.queues.delete(queue.guildID)
|
||||||
// Emit stop event
|
// Emit stop event
|
||||||
if (queue.stopped) {
|
if (queue.stopped) {
|
||||||
return this.emit('musicStop')
|
return this.emit('musicStop')
|
||||||
|
@ -909,6 +1256,20 @@ module.exports = Player
|
||||||
/**
|
/**
|
||||||
* Emitted when an error is triggered
|
* Emitted when an error is triggered
|
||||||
* @event Player#error
|
* @event Player#error
|
||||||
* @param {string} error It can be `NotConnected`, `UnableToJoin`, `NotPlaying` or `LiveVideo`.
|
* @param {string} error It can be `NotConnected`, `UnableToJoin`, `NotPlaying`, `ParseError` or `LiveVideo`.
|
||||||
* @param {Discord.Message} message
|
* @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
|
||||||
|
*/
|
||||||
|
|
|
@ -42,7 +42,7 @@ class Track {
|
||||||
* The video thumbnail
|
* The video thumbnail
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
this.thumbnail = typeof videoData.thumbnail === 'object'
|
this.thumbnail = videoData.thumbnail && typeof videoData.thumbnail === 'object'
|
||||||
? videoData.thumbnail.url
|
? videoData.thumbnail.url
|
||||||
: videoData.thumbnail
|
: videoData.thumbnail
|
||||||
/**
|
/**
|
||||||
|
|
54
src/Util.js
54
src/Util.js
|
@ -1,10 +1,13 @@
|
||||||
const ytsr = require('youtube-sr')
|
const ytsr = require('youtube-sr').default
|
||||||
const soundcloud = require('soundcloud-scraper')
|
const soundcloud = require('soundcloud-scraper')
|
||||||
const chalk = require('chalk')
|
const chalk = require('chalk')
|
||||||
|
|
||||||
const spotifySongRegex = (/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/)
|
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 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 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 xvRegex = (/(http|https):\/\/(.+)?xvideos.com\/(.+)\/(.+)/)
|
||||||
|
|
||||||
module.exports = class Util {
|
module.exports = class Util {
|
||||||
constructor () {
|
constructor () {
|
||||||
|
@ -15,15 +18,15 @@ module.exports = class Util {
|
||||||
try {
|
try {
|
||||||
const prism = require('prism-media')
|
const prism = require('prism-media')
|
||||||
prism.FFmpeg.getInfo()
|
prism.FFmpeg.getInfo()
|
||||||
return true;
|
return true
|
||||||
} catch (e) {
|
} catch {
|
||||||
this.alertFFMPEG();
|
this.alertFFMPEG()
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static alertFFMPEG () {
|
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.');
|
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) {
|
static isVoiceEmpty (channel) {
|
||||||
|
@ -47,10 +50,47 @@ module.exports = class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
static isYTPlaylistLink (query) {
|
static isYTPlaylistLink (query) {
|
||||||
return ytsr.validate(query, 'PLAYLIST')
|
return ytsr.validate(query, 'PLAYLIST_ID')
|
||||||
}
|
}
|
||||||
|
|
||||||
static isYTVideoLink (query) {
|
static isYTVideoLink (query) {
|
||||||
return ytsr.validate(query, 'VIDEO')
|
return ytsr.validate(query, 'VIDEO')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 isXVLink (query) {
|
||||||
|
return xvRegex.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).join(':')
|
||||||
|
return final.length <= 3 ? `0:${final.length === 1 ? `0${final}` : final || 0}` : final
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
151
typings/index.d.ts
vendored
151
typings/index.d.ts
vendored
|
@ -2,7 +2,8 @@ declare module 'discord-player' {
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { Client, Collection, Message, MessageCollector, Snowflake, User, VoiceChannel, VoiceConnection } from 'discord.js';
|
import { Client, Collection, Message, MessageCollector, Snowflake, User, VoiceChannel, VoiceConnection } from 'discord.js';
|
||||||
import { Playlist as YTSRPlaylist } from 'youtube-sr';
|
import { Playlist as YTSRPlaylist } from 'youtube-sr';
|
||||||
import { Stream } from 'stream';
|
import { Stream, Readable } from 'stream';
|
||||||
|
import * as XVDL from 'xvdl';
|
||||||
|
|
||||||
export const version: string;
|
export const version: string;
|
||||||
|
|
||||||
|
@ -12,6 +13,11 @@ declare module 'discord-player' {
|
||||||
static isSpotifyLink(query: string): boolean;
|
static isSpotifyLink(query: string): boolean;
|
||||||
static isYTPlaylistLink(query: string): boolean;
|
static isYTPlaylistLink(query: string): boolean;
|
||||||
static isYTVideoLink(query: string): boolean;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Player extends EventEmitter {
|
export class Player extends EventEmitter {
|
||||||
|
@ -23,6 +29,7 @@ declare module 'discord-player' {
|
||||||
public queues: Collection<Snowflake, Queue>;
|
public queues: Collection<Snowflake, Queue>;
|
||||||
public filters: PlayerFilters;
|
public filters: PlayerFilters;
|
||||||
|
|
||||||
|
public static get AudioFilters(): PlayerFilters;
|
||||||
public isPlaying(message: Message): boolean;
|
public isPlaying(message: Message): boolean;
|
||||||
public setFilters(message: Message, newFilters: Partial<Filters>): Promise<void>;
|
public setFilters(message: Message, newFilters: Partial<Filters>): Promise<void>;
|
||||||
public play(message: Message, query: string | Track, firstResult?: boolean): Promise<void>;
|
public play(message: Message, query: string | Track, firstResult?: boolean): Promise<void>;
|
||||||
|
@ -55,7 +62,36 @@ declare module 'discord-player' {
|
||||||
autoSelfDeaf: boolean;
|
autoSelfDeaf: boolean;
|
||||||
quality: MusicQuality;
|
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 = {
|
type FiltersStatuses = {
|
||||||
[key in Filters]: boolean;
|
[key in Filters]: boolean;
|
||||||
}
|
}
|
||||||
|
@ -72,7 +108,7 @@ declare module 'discord-player' {
|
||||||
requestedBy: User;
|
requestedBy: User;
|
||||||
}
|
}
|
||||||
type Playlist = YTSRPlaylist & CustomPlaylist;
|
type Playlist = YTSRPlaylist & CustomPlaylist;
|
||||||
type PlayerError = 'NotConnected' | 'UnableToJoin' | 'NotPlaying' | 'LiveVideo';
|
type PlayerError = 'NotConnected' | 'UnableToJoin' | 'NotPlaying' | 'LiveVideo' | 'ParseError';
|
||||||
interface PlayerEvents {
|
interface PlayerEvents {
|
||||||
searchResults: [Message, string, Track[]];
|
searchResults: [Message, string, Track[]];
|
||||||
searchInvalidResponse: [Message, string, Track[], string, MessageCollector];
|
searchInvalidResponse: [Message, string, Track[], string, MessageCollector];
|
||||||
|
@ -87,6 +123,8 @@ declare module 'discord-player' {
|
||||||
queueCreate: [Message, Queue];
|
queueCreate: [Message, Queue];
|
||||||
queueEnd: [Message, Queue];
|
queueEnd: [Message, Queue];
|
||||||
error: [PlayerError, Message];
|
error: [PlayerError, Message];
|
||||||
|
playlistParseStart: [any, Message];
|
||||||
|
playlistParseEnd: [any, Message];
|
||||||
}
|
}
|
||||||
class Queue {
|
class Queue {
|
||||||
constructor(guildID: string, message: Message, filters: PlayerFilters);
|
constructor(guildID: string, message: Message, filters: PlayerFilters);
|
||||||
|
@ -128,4 +166,111 @@ declare module 'discord-player' {
|
||||||
public durationMS: number;
|
public durationMS: number;
|
||||||
public queue: Queue;
|
public queue: 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;
|
||||||
|
reactions: {
|
||||||
|
total: number;
|
||||||
|
like: number;
|
||||||
|
love: number;
|
||||||
|
care: number;
|
||||||
|
wow: number;
|
||||||
|
haha: number;
|
||||||
|
sad: number;
|
||||||
|
angry: number;
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
XVideosExtractor: XVDL.XVDL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Extractors: Extractors;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue