✨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
|
||||
docs
|
||||
.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 = {
|
||||
Extractors: require('./src/Extractors/Extractor'),
|
||||
version: require('./package.json').version,
|
||||
Player: require('./src/Player')
|
||||
}
|
||||
|
|
11
package.json
11
package.json
|
@ -6,7 +6,7 @@
|
|||
"types": "typings/index.d.ts",
|
||||
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
|
||||
"scripts": {
|
||||
"test": "node index.js",
|
||||
"test": "cd test && node index.js",
|
||||
"generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -33,13 +33,16 @@
|
|||
"@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",
|
||||
"xvdl": "^1.0.2",
|
||||
"youtube-sr": "^3.0.0",
|
||||
"ytdl-core": "^4.4.2"
|
||||
"ytdl-core": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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
|
453
src/Player.js
453
src/Player.js
|
@ -1,14 +1,15 @@
|
|||
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')
|
||||
const Track = require('./Track')
|
||||
const Util = require('./Util')
|
||||
const { EventEmitter } = require('events')
|
||||
const Client = new soundcloud.Client()
|
||||
const { VimeoExtractor, DiscordExtractor, FacebookExtractor, ReverbnationExtractor, XVideosExtractor } = require('./Extractors/Extractor')
|
||||
|
||||
/**
|
||||
* @typedef Filters
|
||||
|
@ -31,6 +32,15 @@ const Client = new soundcloud.Client()
|
|||
* @property {boolean} [haas=false] Whether the haas 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} [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 = {
|
||||
|
@ -52,7 +62,16 @@ const filters = {
|
|||
gate: 'agate',
|
||||
haas: 'haas',
|
||||
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 {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..
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -77,7 +98,9 @@ const defaultPlayerOptions = {
|
|||
leaveOnEmpty: true,
|
||||
leaveOnEmptyCooldown: 0,
|
||||
autoSelfDeaf: true,
|
||||
quality: 'high'
|
||||
quality: 'high',
|
||||
enableLive: false,
|
||||
ytdlRequestOptions: {}
|
||||
}
|
||||
|
||||
class Player extends EventEmitter {
|
||||
|
@ -94,7 +117,7 @@ class Player extends EventEmitter {
|
|||
* @type {Util}
|
||||
*/
|
||||
this.util = Util
|
||||
this.util.checkFFMPEG();
|
||||
this.util.checkFFMPEG()
|
||||
|
||||
/**
|
||||
* Discord.js client instance
|
||||
|
@ -130,11 +153,23 @@ class Player extends EventEmitter {
|
|||
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
|
||||
* @param {String} query
|
||||
*/
|
||||
resolveQueryType (query) {
|
||||
resolveQueryType (query, forceType) {
|
||||
if (forceType && typeof forceType === 'string') return forceType
|
||||
|
||||
if (this.util.isSpotifyLink(query)) {
|
||||
return 'spotify-song'
|
||||
} else if (this.util.isYTPlaylistLink(query)) {
|
||||
|
@ -145,6 +180,16 @@ class Player extends EventEmitter {
|
|||
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 if (this.util.isXVLink(query)) {
|
||||
return 'xvlink'
|
||||
} else {
|
||||
return 'youtube-video-keywords'
|
||||
}
|
||||
|
@ -175,16 +220,154 @@ class Player extends EventEmitter {
|
|||
}
|
||||
}
|
||||
} 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 (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') {
|
||||
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))
|
||||
}
|
||||
}).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
|
||||
* @param {Discord.Message} message
|
||||
|
@ -255,6 +469,23 @@ class Player extends EventEmitter {
|
|||
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
|
||||
* @ignore
|
||||
|
@ -319,12 +550,16 @@ class Player extends EventEmitter {
|
|||
* @param {String} query
|
||||
*/
|
||||
async _handlePlaylist (message, query) {
|
||||
this.emit('playlistParseStart', {}, message)
|
||||
const playlist = await ytsr.getPlaylist(query)
|
||||
if (!playlist) return this.emit('noResults', message, query)
|
||||
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.thumbnail = playlist.tracks[0].thumbnail
|
||||
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)
|
||||
|
@ -335,17 +570,19 @@ class Player extends EventEmitter {
|
|||
this._addTracksToQueue(message, playlist.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
async _handleSpotifyPlaylist (message, query) {
|
||||
this.emit('playlistParseStart', {}, message)
|
||||
const playlist = await spotify.getData(query)
|
||||
if (!playlist) return this.emit('noResults', message, query)
|
||||
let tracks = []
|
||||
let s;
|
||||
for (var i = 0; i < playlist.tracks.items.length; i++) {
|
||||
let query = `${playlist.tracks.items[i].track.artists[0].name} - ${playlist.tracks.items[i].track.name}`
|
||||
let results = await ytsr.search(query, { type: 'video' })
|
||||
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;
|
||||
s++ // could be used later for skipped tracks due to result not being found
|
||||
continue
|
||||
}
|
||||
tracks.push(results[0])
|
||||
}
|
||||
|
@ -353,6 +590,8 @@ class Player extends EventEmitter {
|
|||
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)
|
||||
|
@ -363,21 +602,22 @@ class Player extends EventEmitter {
|
|||
this._addTracksToQueue(message, playlist.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
async _handleSpotifyAlbum (message, query) {
|
||||
const album = await spotify.getData(query)
|
||||
if (!album) return this.emit('noResults', message, query)
|
||||
let tracks = []
|
||||
let s;
|
||||
for (var i = 0; i < album.tracks.items.length; i++) {
|
||||
let query = `${album.tracks.items[i].artists[0].name} - ${album.tracks.items[i].name}`
|
||||
let results = await ytsr.search(query, { type: 'video' })
|
||||
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;
|
||||
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
|
||||
|
@ -392,6 +632,84 @@ class Player extends EventEmitter {
|
|||
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`.
|
||||
* @param {Discord.Message} message Discord `message`
|
||||
|
@ -403,6 +721,11 @@ class Player extends EventEmitter {
|
|||
* client.player.play(message, "Despacito", true);
|
||||
*/
|
||||
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)) {
|
||||
return this._handlePlaylist(message, query)
|
||||
}
|
||||
|
@ -412,12 +735,16 @@ class Player extends EventEmitter {
|
|||
if (this.util.isSpotifyAlbumLink(query)) {
|
||||
return this._handleSpotifyAlbum(message, query)
|
||||
}
|
||||
if (this.util.isSoundcloudPlaylist(query)) {
|
||||
return this._handleSoundCloudPlaylist(message, query)
|
||||
}
|
||||
|
||||
let trackToPlay
|
||||
if (query instanceof Track) {
|
||||
trackToPlay = query
|
||||
} else if (this.util.isYTVideoLink(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 */
|
||||
trackToPlay = new Track({
|
||||
title: videoData.videoDetails.title,
|
||||
|
@ -622,7 +949,13 @@ class Player extends EventEmitter {
|
|||
if (!queue) return this.emit('error', 'NotPlaying', message)
|
||||
// Shuffle the queue (except the first track)
|
||||
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)
|
||||
// Return the queue
|
||||
return queue
|
||||
|
@ -678,14 +1011,16 @@ class Player extends EventEmitter {
|
|||
const bar = '▬▬▬▬▬▬▬▬▬▬▬▬▬▬'.split('')
|
||||
bar.splice(index, 0, '🔘')
|
||||
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}`
|
||||
} else {
|
||||
return `${bar.join('')}`
|
||||
}
|
||||
} else {
|
||||
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}`
|
||||
} else {
|
||||
return '🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬'
|
||||
|
@ -729,11 +1064,11 @@ class Player extends EventEmitter {
|
|||
}, this.options.leaveOnEmptyCooldown || 0)
|
||||
}
|
||||
|
||||
_playYTDLStream (queue, updateFilter) {
|
||||
return new Promise((resolve) => {
|
||||
const ffmeg = this.util.checkFFMPEG();
|
||||
if (!ffmeg) return;
|
||||
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]) {
|
||||
|
@ -746,14 +1081,26 @@ class Player extends EventEmitter {
|
|||
} else {
|
||||
encoderArgs = ['-af', encoderArgsFilters.join(',')]
|
||||
}
|
||||
const newStream = ytdl(queue.playing.url, {
|
||||
quality: this.options.quality === 'low' ? 'lowestaudio' : 'highestaudio',
|
||||
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(() => {
|
||||
if (queue.stream) queue.stream.destroy()
|
||||
queue.stream = newStream
|
||||
|
@ -793,10 +1140,10 @@ class Player extends EventEmitter {
|
|||
if (this.options.leaveOnEnd && !queue.stopped) {
|
||||
setTimeout(() => {
|
||||
queue.voiceConnection.channel.leave()
|
||||
// Remove the guild from the guilds list
|
||||
this.queues.delete(queue.guildID)
|
||||
}, this.options.leaveOnEndCooldown || 0)
|
||||
}
|
||||
// Remove the guild from the guilds list
|
||||
this.queues.delete(queue.guildID)
|
||||
// Emit stop event
|
||||
if (queue.stopped) {
|
||||
return this.emit('musicStop')
|
||||
|
@ -909,6 +1256,20 @@ module.exports = Player
|
|||
/**
|
||||
* Emitted when an error is triggered
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @type {string}
|
||||
*/
|
||||
this.thumbnail = typeof videoData.thumbnail === 'object'
|
||||
this.thumbnail = videoData.thumbnail && typeof videoData.thumbnail === 'object'
|
||||
? videoData.thumbnail.url
|
||||
: videoData.thumbnail
|
||||
/**
|
||||
|
|
58
src/Util.js
58
src/Util.js
|
@ -1,10 +1,13 @@
|
|||
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\/.*/)
|
||||
const xvRegex = (/(http|https):\/\/(.+)?xvideos.com\/(.+)\/(.+)/)
|
||||
|
||||
module.exports = class Util {
|
||||
constructor () {
|
||||
|
@ -15,15 +18,15 @@ module.exports = class Util {
|
|||
try {
|
||||
const prism = require('prism-media')
|
||||
prism.FFmpeg.getInfo()
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.alertFFMPEG();
|
||||
return false;
|
||||
return true
|
||||
} catch {
|
||||
this.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.');
|
||||
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) {
|
||||
|
@ -41,16 +44,53 @@ module.exports = class Util {
|
|||
static isSpotifyPLLink (query) {
|
||||
return spotifyPlaylistRegex.test(query)
|
||||
}
|
||||
|
||||
static isSpotifyAlbumLink(query) {
|
||||
|
||||
static isSpotifyAlbumLink (query) {
|
||||
return spotifyAlbumRegex.test(query)
|
||||
}
|
||||
|
||||
static isYTPlaylistLink (query) {
|
||||
return ytsr.validate(query, 'PLAYLIST')
|
||||
return ytsr.validate(query, 'PLAYLIST_ID')
|
||||
}
|
||||
|
||||
static isYTVideoLink (query) {
|
||||
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 { Client, Collection, Message, MessageCollector, Snowflake, User, VoiceChannel, VoiceConnection } from 'discord.js';
|
||||
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;
|
||||
|
||||
|
@ -12,6 +13,11 @@ declare module 'discord-player' {
|
|||
static isSpotifyLink(query: string): boolean;
|
||||
static isYTPlaylistLink(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 {
|
||||
|
@ -23,6 +29,7 @@ declare module 'discord-player' {
|
|||
public queues: Collection<Snowflake, Queue>;
|
||||
public filters: PlayerFilters;
|
||||
|
||||
public static get AudioFilters(): PlayerFilters;
|
||||
public isPlaying(message: Message): boolean;
|
||||
public setFilters(message: Message, newFilters: Partial<Filters>): Promise<void>;
|
||||
public play(message: Message, query: string | Track, firstResult?: boolean): Promise<void>;
|
||||
|
@ -55,7 +62,36 @@ declare module 'discord-player' {
|
|||
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 = {
|
||||
[key in Filters]: boolean;
|
||||
}
|
||||
|
@ -72,7 +108,7 @@ declare module 'discord-player' {
|
|||
requestedBy: User;
|
||||
}
|
||||
type Playlist = YTSRPlaylist & CustomPlaylist;
|
||||
type PlayerError = 'NotConnected' | 'UnableToJoin' | 'NotPlaying' | 'LiveVideo';
|
||||
type PlayerError = 'NotConnected' | 'UnableToJoin' | 'NotPlaying' | 'LiveVideo' | 'ParseError';
|
||||
interface PlayerEvents {
|
||||
searchResults: [Message, string, Track[]];
|
||||
searchInvalidResponse: [Message, string, Track[], string, MessageCollector];
|
||||
|
@ -87,6 +123,8 @@ declare module 'discord-player' {
|
|||
queueCreate: [Message, Queue];
|
||||
queueEnd: [Message, Queue];
|
||||
error: [PlayerError, Message];
|
||||
playlistParseStart: [any, Message];
|
||||
playlistParseEnd: [any, Message];
|
||||
}
|
||||
class Queue {
|
||||
constructor(guildID: string, message: Message, filters: PlayerFilters);
|
||||
|
@ -128,4 +166,111 @@ declare module 'discord-player' {
|
|||
public durationMS: number;
|
||||
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