distube 2.8.15 → 3.0.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -98
- package/dist/DisTube.d.ts +522 -0
- package/dist/DisTube.d.ts.map +1 -0
- package/dist/DisTube.js +794 -0
- package/dist/DisTube.js.map +1 -0
- package/dist/constant.d.ts +130 -0
- package/dist/constant.d.ts.map +1 -0
- package/dist/constant.js +150 -0
- package/dist/constant.js.map +1 -0
- package/dist/core/DisTubeBase.d.ts +55 -0
- package/dist/core/DisTubeBase.d.ts.map +1 -0
- package/dist/core/DisTubeBase.js +76 -0
- package/dist/core/DisTubeBase.js.map +1 -0
- package/dist/core/DisTubeHandler.d.ts +84 -0
- package/dist/core/DisTubeHandler.d.ts.map +1 -0
- package/dist/core/DisTubeHandler.js +311 -0
- package/dist/core/DisTubeHandler.js.map +1 -0
- package/dist/core/DisTubeOptions.d.ts +26 -0
- package/dist/core/DisTubeOptions.d.ts.map +1 -0
- package/dist/core/DisTubeOptions.js +93 -0
- package/dist/core/DisTubeOptions.js.map +1 -0
- package/dist/core/DisTubeStream.d.ts +52 -0
- package/dist/core/DisTubeStream.d.ts.map +1 -0
- package/dist/core/DisTubeStream.js +109 -0
- package/dist/core/DisTubeStream.js.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +19 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/manager/BaseManager.d.ts +18 -0
- package/dist/core/manager/BaseManager.d.ts.map +1 -0
- package/dist/core/manager/BaseManager.js +44 -0
- package/dist/core/manager/BaseManager.js.map +1 -0
- package/dist/core/manager/QueueManager.d.ts +60 -0
- package/dist/core/manager/QueueManager.d.ts.map +1 -0
- package/dist/core/manager/QueueManager.js +202 -0
- package/dist/core/manager/QueueManager.js.map +1 -0
- package/dist/core/manager/index.d.ts +3 -0
- package/dist/core/manager/index.d.ts.map +1 -0
- package/dist/core/manager/index.js +15 -0
- package/dist/core/manager/index.js.map +1 -0
- package/dist/core/voice/DJSAdapter.d.ts +4 -0
- package/dist/core/voice/DJSAdapter.d.ts.map +1 -0
- package/dist/core/voice/DJSAdapter.js +61 -0
- package/dist/core/voice/DJSAdapter.js.map +1 -0
- package/dist/core/voice/DisTubeVoice.d.ts +83 -0
- package/dist/core/voice/DisTubeVoice.d.ts.map +1 -0
- package/dist/core/voice/DisTubeVoice.js +236 -0
- package/dist/core/voice/DisTubeVoice.js.map +1 -0
- package/dist/core/voice/DisTubeVoiceManager.d.ts +41 -0
- package/dist/core/voice/DisTubeVoiceManager.d.ts.map +1 -0
- package/dist/core/voice/DisTubeVoiceManager.js +67 -0
- package/dist/core/voice/DisTubeVoiceManager.js.map +1 -0
- package/dist/core/voice/index.d.ts +4 -0
- package/dist/core/voice/index.d.ts.map +1 -0
- package/dist/core/voice/index.js +16 -0
- package/dist/core/voice/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/http.d.ts +8 -0
- package/dist/plugin/http.d.ts.map +1 -0
- package/dist/plugin/http.js +20 -0
- package/dist/plugin/http.js.map +1 -0
- package/dist/plugin/https.d.ts +14 -0
- package/dist/plugin/https.d.ts.map +1 -0
- package/dist/plugin/https.js +50 -0
- package/dist/plugin/https.js.map +1 -0
- package/dist/plugin/index.d.ts +4 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +16 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/youtube-dl.d.ts +11 -0
- package/dist/plugin/youtube-dl.d.ts.map +1 -0
- package/dist/plugin/youtube-dl.js +75 -0
- package/dist/plugin/youtube-dl.js.map +1 -0
- package/dist/struct/CustomPlugin.d.ts +27 -0
- package/dist/struct/CustomPlugin.d.ts.map +1 -0
- package/dist/struct/CustomPlugin.js +35 -0
- package/dist/struct/CustomPlugin.js.map +1 -0
- package/dist/struct/DisTubeError.d.ts +54 -0
- package/dist/struct/DisTubeError.d.ts.map +1 -0
- package/dist/struct/DisTubeError.js +72 -0
- package/dist/struct/DisTubeError.js.map +1 -0
- package/dist/struct/ExtractorPlugin.d.ts +29 -0
- package/dist/struct/ExtractorPlugin.d.ts.map +1 -0
- package/dist/struct/ExtractorPlugin.js +32 -0
- package/dist/struct/ExtractorPlugin.js.map +1 -0
- package/dist/struct/Playlist.d.ts +42 -0
- package/dist/struct/Playlist.d.ts.map +1 -0
- package/dist/struct/Playlist.js +104 -0
- package/dist/struct/Playlist.js.map +1 -0
- package/dist/struct/Plugin.d.ts +82 -0
- package/dist/struct/Plugin.d.ts.map +1 -0
- package/dist/struct/Plugin.js +108 -0
- package/dist/struct/Plugin.js.map +1 -0
- package/dist/struct/Queue.d.ts +217 -0
- package/dist/struct/Queue.d.ts.map +1 -0
- package/dist/struct/Queue.js +480 -0
- package/dist/struct/Queue.js.map +1 -0
- package/dist/struct/SearchResult.d.ts +28 -0
- package/dist/struct/SearchResult.d.ts.map +1 -0
- package/dist/struct/SearchResult.js +79 -0
- package/dist/struct/SearchResult.js.map +1 -0
- package/dist/struct/Song.d.ts +68 -0
- package/dist/struct/Song.d.ts.map +1 -0
- package/dist/struct/Song.js +229 -0
- package/dist/struct/Song.js.map +1 -0
- package/dist/struct/TaskQueue.d.ts +33 -0
- package/dist/struct/TaskQueue.d.ts.map +1 -0
- package/dist/struct/TaskQueue.js +58 -0
- package/dist/struct/TaskQueue.js.map +1 -0
- package/dist/struct/index.d.ts +10 -0
- package/dist/struct/index.d.ts.map +1 -0
- package/dist/struct/index.js +22 -0
- package/dist/struct/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/type.d.ts +159 -0
- package/dist/type.d.ts.map +1 -0
- package/dist/type.js +3 -0
- package/dist/type.js.map +1 -0
- package/dist/util.d.ts +47 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +205 -0
- package/dist/util.js.map +1 -0
- package/package.json +53 -25
- package/.eslintrc.json +0 -150
- package/.gitattributes +0 -2
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -17
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/workflows/npm-publish.yml +0 -17
- package/jsdoc.json +0 -97
- package/src/DisTube.js +0 -1121
- package/src/Playlist.js +0 -107
- package/src/Queue.js +0 -120
- package/src/SearchResult.js +0 -81
- package/src/Song.js +0 -135
- package/src/duration.js +0 -39
- package/tsconfig.json +0 -24
- package/typings/DisTube.d.ts +0 -635
- package/typings/Playlist.d.ts +0 -73
- package/typings/Queue.d.ts +0 -106
- package/typings/SearchResult.d.ts +0 -59
- package/typings/Song.d.ts +0 -104
- package/typings/duration.d.ts +0 -2
package/src/DisTube.js
DELETED
|
@@ -1,1121 +0,0 @@
|
|
|
1
|
-
const ytdl = require("@distube/ytdl"),
|
|
2
|
-
ytsr = require("@distube/ytsr"),
|
|
3
|
-
ytpl = require("@distube/ytpl"),
|
|
4
|
-
{ EventEmitter } = require("events"),
|
|
5
|
-
Queue = require("./Queue"),
|
|
6
|
-
Song = require("./Song"),
|
|
7
|
-
SearchResult = require("./SearchResult"),
|
|
8
|
-
Playlist = require("./Playlist"),
|
|
9
|
-
Discord = require("discord.js"),
|
|
10
|
-
youtube_dl = require("@distube/youtube-dl"),
|
|
11
|
-
{ promisify } = require("util");
|
|
12
|
-
const youtube_dlOptions = ["--no-warnings", "--force-ipv4"];
|
|
13
|
-
youtube_dl.getInfo = promisify(youtube_dl.getInfo);
|
|
14
|
-
|
|
15
|
-
const isURL = string => {
|
|
16
|
-
if (string.includes(" ")) return false;
|
|
17
|
-
try {
|
|
18
|
-
const url = new URL(string);
|
|
19
|
-
if (!["https:", "http:"].includes(url.protocol) ||
|
|
20
|
-
url.origin === "null" || !url.host
|
|
21
|
-
) return false;
|
|
22
|
-
} catch { return false }
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
const parseNumber = string => (typeof string === "string" ? Number(string.replace(/\D+/g, "")) : Number(string)) || 0;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* DisTube options.
|
|
29
|
-
* @typedef {object} DisTubeOptions
|
|
30
|
-
* @prop {boolean} [emitNewSongOnly=false] `@1.3.0`. If `true`, {@link DisTube#event:playSong} is not emitted when looping a song or next song is the same as the previous one
|
|
31
|
-
* @prop {number} [highWaterMark=1<<24] `@2.2.0` ytdl's highWaterMark option.
|
|
32
|
-
* @prop {boolean} [leaveOnEmpty=true] Whether or not leaving voice channel if channel is empty in 60s. (Avoid accident leaving)
|
|
33
|
-
* @prop {boolean} [leaveOnFinish=false] Whether or not leaving voice channel when the queue ends.
|
|
34
|
-
* @prop {boolean} [leaveOnStop=true] Whether or not leaving voice channel after using {@link DisTube#stop|stop()} function.
|
|
35
|
-
* @prop {boolean} [searchSongs=false] Whether or not searching for multiple songs to select manually, DisTube will play the first result if `false`
|
|
36
|
-
* @prop {string} [youtubeCookie=null] `@2.4.0` YouTube cookies. How to get it: {@link https://github.com/fent/node-ytdl-core/blob/784c04eaf9f3cfac0fe0933155adffe0e2e0848a/example/cookies.js#L6-L12|YTDL's Example}
|
|
37
|
-
* @prop {string} [youtubeIdentityToken=null] `@2.4.0` If not given, ytdl-core will try to find it. You can find this by going to a video's watch page, viewing the source, and searching for "ID_TOKEN".
|
|
38
|
-
* @prop {boolean} [youtubeDL=true] `@2.8.0` Whether or not using youtube-dl.
|
|
39
|
-
* @prop {boolean} [updateYouTubeDL=true] `@2.8.0` Whether or not updating youtube-dl automatically.
|
|
40
|
-
* @prop {Object.<string, string>} [customFilters] `@2.7.0` Override or add more ffmpeg filters. Example: `{ "Filter name": "Filter value", "8d": "apulsator=hz=0.075" }`
|
|
41
|
-
*/
|
|
42
|
-
const DisTubeOptions = {
|
|
43
|
-
highWaterMark: 1 << 24,
|
|
44
|
-
emitNewSongOnly: false,
|
|
45
|
-
leaveOnEmpty: true,
|
|
46
|
-
leaveOnFinish: false,
|
|
47
|
-
leaveOnStop: true,
|
|
48
|
-
searchSongs: false,
|
|
49
|
-
youtubeCookie: null,
|
|
50
|
-
youtubeIdentityToken: null,
|
|
51
|
-
youtubeDL: true,
|
|
52
|
-
updateYouTubeDL: true,
|
|
53
|
-
customFilters: {},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* DisTube audio filters.
|
|
58
|
-
* @typedef {("3d"|"bassboost"|"echo"|"karaoke"|"nightcore"|"vaporwave"|"flanger"|"gate"|"haas"|"reverse"|"surround"|"mcompand"|"phaser"|"tremolo"|"earwax"|string)} Filter
|
|
59
|
-
* @prop {string} 3d `@2.0.0`
|
|
60
|
-
* @prop {string} bassboost `@2.0.0`
|
|
61
|
-
* @prop {string} echo `@2.0.0`
|
|
62
|
-
* @prop {string} karaoke `@2.0.0`
|
|
63
|
-
* @prop {string} nightcore `@2.0.0`
|
|
64
|
-
* @prop {string} vaporwave `@2.0.0`
|
|
65
|
-
* @prop {string} flanger `@2.4.0`
|
|
66
|
-
* @prop {string} gate `@2.4.0`
|
|
67
|
-
* @prop {string} haas `@2.4.0`
|
|
68
|
-
* @prop {string} reverse `@2.4.0`
|
|
69
|
-
* @prop {string} surround `@2.7.0`
|
|
70
|
-
* @prop {string} mcompand `@2.7.0`
|
|
71
|
-
* @prop {string} phaser `@2.7.0`
|
|
72
|
-
* @prop {string} tremolo `@2.7.0`
|
|
73
|
-
* @prop {string} earwax `@2.7.0`
|
|
74
|
-
*/
|
|
75
|
-
const ffmpegFilters = {
|
|
76
|
-
"3d": "apulsator=hz=0.125",
|
|
77
|
-
bassboost: "bass=g=10,dynaudnorm=f=150:g=15",
|
|
78
|
-
echo: "aecho=0.8:0.9:1000:0.3",
|
|
79
|
-
flanger: "flanger",
|
|
80
|
-
gate: "agate",
|
|
81
|
-
haas: "haas",
|
|
82
|
-
karaoke: "stereotools=mlev=0.1",
|
|
83
|
-
nightcore: "asetrate=48000*1.25,aresample=48000,bass=g=5",
|
|
84
|
-
reverse: "areverse",
|
|
85
|
-
vaporwave: "asetrate=48000*0.8,aresample=48000,atempo=1.1",
|
|
86
|
-
mcompand: "mcompand",
|
|
87
|
-
phaser: "aphaser",
|
|
88
|
-
tremolo: "tremolo",
|
|
89
|
-
surround: "surround",
|
|
90
|
-
earwax: "earwax",
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Class representing a DisTube.
|
|
95
|
-
* @extends EventEmitter
|
|
96
|
-
*/
|
|
97
|
-
class DisTube extends EventEmitter {
|
|
98
|
-
/**
|
|
99
|
-
* DisTube's current version.
|
|
100
|
-
* @type {string}
|
|
101
|
-
* @ignore
|
|
102
|
-
*/
|
|
103
|
-
get version() { return require("../package.json").version }
|
|
104
|
-
static get version() { return require("../package.json").version }
|
|
105
|
-
/**
|
|
106
|
-
* Create new DisTube.
|
|
107
|
-
* @param {Discord.Client} client Discord.JS client
|
|
108
|
-
* @param {DisTubeOptions} [otp={}] Custom DisTube options
|
|
109
|
-
* @example
|
|
110
|
-
* const Discord = require('discord.js'),
|
|
111
|
-
* DisTube = require('distube'),
|
|
112
|
-
* client = new Discord.Client();
|
|
113
|
-
* // Create a new DisTube
|
|
114
|
-
* const distube = new DisTube(client, { searchSongs: true });
|
|
115
|
-
* // client.DisTube = distube // make it access easily
|
|
116
|
-
* client.login("Your Discord Bot Token")
|
|
117
|
-
*/
|
|
118
|
-
constructor(client, otp = {}) {
|
|
119
|
-
super();
|
|
120
|
-
if (!client) throw new SyntaxError("Invalid Discord.Client");
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Discord.JS client
|
|
124
|
-
* @type {Discord.Client}
|
|
125
|
-
*/
|
|
126
|
-
this.client = client;
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Collection of guild queues
|
|
130
|
-
* @type {Discord.Collection<string, Queue>}
|
|
131
|
-
*/
|
|
132
|
-
this.guildQueues = new Discord.Collection();
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* DisTube options
|
|
136
|
-
* @type {DisTubeOptions}
|
|
137
|
-
*/
|
|
138
|
-
this.options = DisTubeOptions;
|
|
139
|
-
Object.assign(this.options, otp);
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* DisTube filters
|
|
143
|
-
* @type {Filter}
|
|
144
|
-
*/
|
|
145
|
-
this.filters = ffmpegFilters;
|
|
146
|
-
if (typeof otp.customFilters === "object") Object.assign(this.filters, otp.customFilters);
|
|
147
|
-
|
|
148
|
-
this.requestOptions = this.options.youtubeCookie ? { headers: { cookie: this.options.youtubeCookie, "x-youtube-identity-token": this.options.youtubeIdentityToken } } : undefined;
|
|
149
|
-
|
|
150
|
-
client.on("voiceStateUpdate", (oldState, newState) => {
|
|
151
|
-
if (newState && newState.id === client.user.id && !newState.channelID) {
|
|
152
|
-
let queue = this.guildQueues.find(gQueue => gQueue.connection && gQueue.connection.channel.id === oldState.channelID);
|
|
153
|
-
if (!queue) return;
|
|
154
|
-
let guildID = queue.connection.channel.guild.id;
|
|
155
|
-
try { this.stop(guildID) } catch { this._deleteQueue(guildID) }
|
|
156
|
-
}
|
|
157
|
-
if (this.options.leaveOnEmpty && oldState && oldState.channel) {
|
|
158
|
-
let queue = this.guildQueues.find(gQueue => gQueue.connection && gQueue.connection.channel.id === oldState.channelID);
|
|
159
|
-
if (queue && this._isVoiceChannelEmpty(queue)) {
|
|
160
|
-
setTimeout(() => {
|
|
161
|
-
let guildID = queue.connection.channel.guild.id;
|
|
162
|
-
if (this.guildQueues.has(guildID) && this._isVoiceChannelEmpty(queue)) {
|
|
163
|
-
queue.connection.channel.leave();
|
|
164
|
-
this.emit("empty", queue.initMessage);
|
|
165
|
-
this._deleteQueue(queue.initMessage);
|
|
166
|
-
}
|
|
167
|
-
}, 60000)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
if (this.options.updateYouTubeDL) {
|
|
173
|
-
require("@distube/youtube-dl/lib/downloader")()
|
|
174
|
-
.then(message => console.log(`[DisTube] ${message}`))
|
|
175
|
-
.catch(console.error)
|
|
176
|
-
.catch(() => console.log("[DisTube] Unable to update youtube-dl, using default version."));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Resolve a Song
|
|
182
|
-
* @async
|
|
183
|
-
* @param {Discord.Message} message The message from guild channel
|
|
184
|
-
* @param {string|Song} song Youtube url | Search string | {@link Song}
|
|
185
|
-
* @private
|
|
186
|
-
* @ignore
|
|
187
|
-
* @returns {Promise<Song|Song[]>} Resolved Song
|
|
188
|
-
*/
|
|
189
|
-
async _resolveSong(message, song) {
|
|
190
|
-
if (!song) return null;
|
|
191
|
-
if (song instanceof Song) return song;
|
|
192
|
-
if (song instanceof SearchResult) return new Song(await ytdl.getInfo(song.url, { requestOptions: this.requestOptions }), message.author, true);
|
|
193
|
-
if (typeof song === "object") return new Song(song, message.author);
|
|
194
|
-
if (ytdl.validateURL(song)) return new Song(await ytdl.getInfo(song, { requestOptions: this.requestOptions }), message.author, true);
|
|
195
|
-
if (isURL(song)) {
|
|
196
|
-
if (!this.options.youtubeDL) throw new Error("Not Supported URL!");
|
|
197
|
-
let info = await youtube_dl.getInfo(song, youtube_dlOptions).catch(e => { throw new Error(`[youtube-dl] ${e.stderr || e}`) });
|
|
198
|
-
if (Array.isArray(info) && info.length > 0) return info.map(i => new Song(i, message.author));
|
|
199
|
-
return new Song(info, message.author);
|
|
200
|
-
}
|
|
201
|
-
return this._resolveSong(message, await this._searchSong(message, song));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Handle a Song or an array of Song
|
|
206
|
-
* @async
|
|
207
|
-
* @param {Discord.Message} message The message from guild channel
|
|
208
|
-
* @param {Song|SearchResult} song {@link Song} | {@link SearchResult}
|
|
209
|
-
* @private
|
|
210
|
-
* @ignore
|
|
211
|
-
*/
|
|
212
|
-
async _handleSong(message, song, skip = false) {
|
|
213
|
-
if (!song) return;
|
|
214
|
-
if (Array.isArray(song)) this._handlePlaylist(message, song, skip);
|
|
215
|
-
else if (this.getQueue(message)) {
|
|
216
|
-
let queue = this._addToQueue(message, song, skip);
|
|
217
|
-
if (skip) this.skip(message);
|
|
218
|
-
else this.emit("addSong", message, queue, song);
|
|
219
|
-
} else {
|
|
220
|
-
let queue = await this._newQueue(message, song);
|
|
221
|
-
this.emit("playSong", message, queue, song);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Play / add a song or playlist from url. Search and play a song if it is not a valid url.
|
|
227
|
-
* @async
|
|
228
|
-
* @param {Discord.Message} message The message from guild channel
|
|
229
|
-
* @param {string|Song|SearchResult} song Youtube url | Search string | {@link Song} | {@link SearchResult}
|
|
230
|
-
* @example
|
|
231
|
-
* client.on('message', (message) => {
|
|
232
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
233
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
234
|
-
* const command = args.shift();
|
|
235
|
-
* if (command == "play")
|
|
236
|
-
* distube.play(message, args.join(" "));
|
|
237
|
-
* });
|
|
238
|
-
*/
|
|
239
|
-
async play(message, song) {
|
|
240
|
-
if (!song) return;
|
|
241
|
-
try {
|
|
242
|
-
if (ytpl.validateID(song)) await this._handlePlaylist(message, song);
|
|
243
|
-
else await this._handleSong(message, await this._resolveSong(message, song));
|
|
244
|
-
} catch (e) {
|
|
245
|
-
e.message = `play(${song}) encountered:\n${e.message}`;
|
|
246
|
-
this._emitError(message, e);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* `@2.0.0` Skip the playing song and play a song or playlist
|
|
252
|
-
* @async
|
|
253
|
-
* @param {Discord.Message} message The message from guild channel
|
|
254
|
-
* @param {string|Song|SearchResult} song Youtube url | Search string | {@link Song} | {@link SearchResult}
|
|
255
|
-
* @example
|
|
256
|
-
* client.on('message', (message) => {
|
|
257
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
258
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
259
|
-
* const command = args.shift();
|
|
260
|
-
* if (command == "playSkip")
|
|
261
|
-
* distube.playSkip(message, args.join(" "));
|
|
262
|
-
* });
|
|
263
|
-
*/
|
|
264
|
-
async playSkip(message, song) {
|
|
265
|
-
if (!song) return;
|
|
266
|
-
try {
|
|
267
|
-
if (ytpl.validateID(song)) await this._handlePlaylist(message, song, true);
|
|
268
|
-
else await this._handleSong(message, await this._resolveSong(message, song), true);
|
|
269
|
-
} catch (e) {
|
|
270
|
-
e.message = `playSkip(${song}) encountered:\n${e.message}`;
|
|
271
|
-
this._emitError(message, e);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* `@2.1.0` Play or add array of Youtube video urls.
|
|
277
|
-
* {@link DisTube#event:playList} or {@link DisTube#event:addList} will be emitted
|
|
278
|
-
* with `playlist`'s properties include `properties` parameter's properties such as
|
|
279
|
-
* `user`, `songs`, `duration`, `formattedDuration`, `thumbnail` like {@link Playlist}
|
|
280
|
-
* @async
|
|
281
|
-
* @param {Discord.Message} message The message from guild channel
|
|
282
|
-
* @param {string[]} urls Array of Youtube url
|
|
283
|
-
* @param {Object} [properties={}] Additional properties such as `name`
|
|
284
|
-
* @param {boolean} [playSkip=false] Whether or not play this playlist instantly
|
|
285
|
-
* @example
|
|
286
|
-
* let songs = ["https://www.youtube.com/watch?v=xxx", "https://www.youtube.com/watch?v=yyy"];
|
|
287
|
-
* distube.playCustomPlaylist(message, songs, { name: "My playlist name" });
|
|
288
|
-
*/
|
|
289
|
-
async playCustomPlaylist(message, urls, properties = {}, playSkip = false) {
|
|
290
|
-
if (!urls.length) return;
|
|
291
|
-
try {
|
|
292
|
-
let songs = urls.filter(url => isURL(url)).map(url => this._resolveSong(message, url).catch(() => { }));
|
|
293
|
-
songs = (await Promise.all(songs)).filter(song => song);
|
|
294
|
-
let playlist = new Playlist(songs, message.author, properties);
|
|
295
|
-
await this._handlePlaylist(message, playlist, playSkip);
|
|
296
|
-
} catch (e) {
|
|
297
|
-
this._emitError(message, e);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* PLay / add a playlist
|
|
303
|
-
* @async
|
|
304
|
-
* @private
|
|
305
|
-
* @ignore
|
|
306
|
-
* @param {Discord.Message} message The message from guild channel
|
|
307
|
-
* @param {string|Song[]|Playlist} arg2 Youtube playlist url | a Playlist
|
|
308
|
-
* @param {boolean} skip Skip the current song
|
|
309
|
-
*/
|
|
310
|
-
async _handlePlaylist(message, arg2, skip = false) {
|
|
311
|
-
let playlist;
|
|
312
|
-
if (typeof arg2 === "object") playlist = arg2; // Song[] or Playlist
|
|
313
|
-
else if (typeof arg2 === "string") {
|
|
314
|
-
playlist = await ytpl(arg2, { limit: Infinity });
|
|
315
|
-
playlist.items = playlist.items.filter(v => !v.thumbnail.includes("no_thumbnail")).map(v => new Song(v, message.author, true));
|
|
316
|
-
}
|
|
317
|
-
if (!playlist) throw Error("Invalid Playlist");
|
|
318
|
-
if (!(playlist instanceof Playlist)) playlist = new Playlist(playlist, message.author)
|
|
319
|
-
if (!playlist.songs.length) throw Error("No valid video in the playlist");
|
|
320
|
-
let songs = playlist.songs;
|
|
321
|
-
let queue = this.getQueue(message);
|
|
322
|
-
if (queue) {
|
|
323
|
-
this._addSongsToQueue(message, songs, skip);
|
|
324
|
-
if (skip) this.skip(message);
|
|
325
|
-
else this.emit("addList", message, queue, playlist);
|
|
326
|
-
} else {
|
|
327
|
-
let song = songs.shift();
|
|
328
|
-
queue = await this._newQueue(message, song);
|
|
329
|
-
if (songs.length) this._addSongsToQueue(message, songs);
|
|
330
|
-
songs.unshift(song);
|
|
331
|
-
this.emit("playList", message, queue, playlist, queue.songs[0]);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* `@2.0.0` Search for a song. You can customize how user answers instead of send a number.
|
|
337
|
-
* Then use {@link DisTube#play|play(message, aResultFromSearch)} or {@link DisTube#playSkip|playSkip()} to play it.
|
|
338
|
-
* @async
|
|
339
|
-
* @param {string} string The string search for
|
|
340
|
-
* @throws {NotFound} If not found
|
|
341
|
-
* @throws {Error} If an error encountered
|
|
342
|
-
* @returns {Promise<SearchResult[]>} Array of results
|
|
343
|
-
*/
|
|
344
|
-
async search(string, retried = false) {
|
|
345
|
-
try {
|
|
346
|
-
let search = await ytsr(string, { limit: 15 });
|
|
347
|
-
let results = search.items.map(i => new SearchResult(i));
|
|
348
|
-
if (results.length === 0) throw Error("No result!");
|
|
349
|
-
return results;
|
|
350
|
-
} catch (e) {
|
|
351
|
-
if (retried) throw e;
|
|
352
|
-
return this.search(string, true);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Search for a song, fire {@link DisTube#event:error} if not found.
|
|
358
|
-
* @async
|
|
359
|
-
* @private
|
|
360
|
-
* @ignore
|
|
361
|
-
* @param {Discord.Message} message The message from guild channel
|
|
362
|
-
* @param {string} name The string search for
|
|
363
|
-
* @returns {Song} Song info
|
|
364
|
-
*/
|
|
365
|
-
async _searchSong(message, name) {
|
|
366
|
-
let results = await this.search(name);
|
|
367
|
-
let result = results[0];
|
|
368
|
-
if (this.options.searchSongs) {
|
|
369
|
-
this.emit("searchResult", message, results);
|
|
370
|
-
try {
|
|
371
|
-
let answers = await message.channel.awaitMessages(m => m.author.id === message.author.id, {
|
|
372
|
-
max: 1,
|
|
373
|
-
time: 60000,
|
|
374
|
-
errors: ["time"],
|
|
375
|
-
})
|
|
376
|
-
if (!answers.first()) throw new Error();
|
|
377
|
-
let index = parseInt(answers.first().content, 10);
|
|
378
|
-
if (isNaN(index) || index > results.length || index < 1) throw new Error();
|
|
379
|
-
result = results[index - 1];
|
|
380
|
-
} catch {
|
|
381
|
-
this.emit("searchCancel", message);
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return result;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Create a new guild queue
|
|
390
|
-
* @async
|
|
391
|
-
* @private
|
|
392
|
-
* @ignore
|
|
393
|
-
* @param {Discord.Message} message The message from guild channel
|
|
394
|
-
* @param {Song} song Song to play
|
|
395
|
-
* @throws {NotInVoice} if user not in a voice channel
|
|
396
|
-
* @returns {Promise<Queue>}
|
|
397
|
-
*/
|
|
398
|
-
async _newQueue(message, song, retried = false) {
|
|
399
|
-
let voice = message.member.voice.channel;
|
|
400
|
-
if (!voice) throw new Error("User is not in the voice channel.");
|
|
401
|
-
let queue = new Queue(message, song);
|
|
402
|
-
this.emit("initQueue", queue);
|
|
403
|
-
this.guildQueues.set(message.guild.id, queue);
|
|
404
|
-
try {
|
|
405
|
-
queue.connection = await voice.join();
|
|
406
|
-
} catch (e) {
|
|
407
|
-
this._deleteQueue(message);
|
|
408
|
-
e.message = `DisTube cannot join the voice channel!\nReason: ${e.message}`;
|
|
409
|
-
if (retried) throw e;
|
|
410
|
-
return this._newQueue(message, song, true);
|
|
411
|
-
}
|
|
412
|
-
queue.connection.on("error", e => {
|
|
413
|
-
e.message = `There is a problem with Discord Voice Connection.\nPlease try again! Sorry for the interruption!\nReason: ${e.message}`;
|
|
414
|
-
this._emitError(message, e);
|
|
415
|
-
this._deleteQueue(message);
|
|
416
|
-
})
|
|
417
|
-
await this._playSong(message);
|
|
418
|
-
return queue;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Delete a guild queue
|
|
423
|
-
* @private
|
|
424
|
-
* @ignore
|
|
425
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
426
|
-
*/
|
|
427
|
-
_deleteQueue(message) {
|
|
428
|
-
let queue = this.getQueue(message);
|
|
429
|
-
if (!queue) return;
|
|
430
|
-
if (queue.dispatcher) try { queue.dispatcher.destroy() } catch { }
|
|
431
|
-
if (queue.stream) try { queue.stream.destroy() } catch { }
|
|
432
|
-
if (typeof message === "string") this.guildQueues.delete(message);
|
|
433
|
-
else if (message && message.guild) this.guildQueues.delete(message.guild.id);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Get the guild queue
|
|
438
|
-
* @param {Discord.Snowflake|Discord.Message} message The guild ID or message from guild channel.
|
|
439
|
-
* @returns {Queue} The guild queue
|
|
440
|
-
* @example
|
|
441
|
-
* client.on('message', (message) => {
|
|
442
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
443
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
444
|
-
* const command = args.shift();
|
|
445
|
-
* if (command == "queue") {
|
|
446
|
-
* let queue = distube.getQueue(message);
|
|
447
|
-
* message.channel.send('Current queue:\n' + queue.songs.map((song, id) =>
|
|
448
|
-
* `**${id+1}**. [${song.name}](${song.url}) - \`${song.formattedDuration}\``
|
|
449
|
-
* ).join("\n"));
|
|
450
|
-
* }
|
|
451
|
-
* });
|
|
452
|
-
*/
|
|
453
|
-
getQueue(message) {
|
|
454
|
-
if (typeof message === "string") return this.guildQueues.get(message);
|
|
455
|
-
if (!message || !message.guild) throw TypeError("Parameter should be Discord.Message or server ID!");
|
|
456
|
-
return this.guildQueues.get(message.guild.id);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Add a video to queue
|
|
461
|
-
* @private
|
|
462
|
-
* @ignore
|
|
463
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
464
|
-
* @param {Song} song Song to add
|
|
465
|
-
* @param {boolean} [unshift=false] Unshift
|
|
466
|
-
* @throws {NotInVoice} if result is empty
|
|
467
|
-
* @returns {Queue}
|
|
468
|
-
*/
|
|
469
|
-
_addToQueue(message, song, unshift = false) {
|
|
470
|
-
let queue = this.getQueue(message);
|
|
471
|
-
if (!queue) throw new Error("NotPlaying");
|
|
472
|
-
if (!song) throw new Error("NoSong");
|
|
473
|
-
if (unshift) {
|
|
474
|
-
let playing = queue.songs.shift();
|
|
475
|
-
queue.songs.unshift(playing, song);
|
|
476
|
-
} else { queue.songs.push(song); }
|
|
477
|
-
return queue;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Add a array of videos to queue
|
|
482
|
-
* @private
|
|
483
|
-
* @ignore
|
|
484
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
485
|
-
* @param {Song[]} songs Array of song to add
|
|
486
|
-
* @param {boolean} [unshift=false] Unshift
|
|
487
|
-
* @returns {Queue}
|
|
488
|
-
*/
|
|
489
|
-
_addSongsToQueue(message, songs, unshift = false) {
|
|
490
|
-
let queue = this.getQueue(message);
|
|
491
|
-
if (!queue) throw new Error("NotPlaying");
|
|
492
|
-
if (!songs.length) throw new Error("NoSong");
|
|
493
|
-
if (unshift) {
|
|
494
|
-
let playing = queue.songs.shift();
|
|
495
|
-
queue.songs.unshift(playing, ...songs);
|
|
496
|
-
} else { queue.songs.push(...songs); }
|
|
497
|
-
return queue;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Pause the guild stream
|
|
502
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
503
|
-
* @returns {Queue} The guild queue
|
|
504
|
-
* @throws {NotPlaying} No playing queue
|
|
505
|
-
*/
|
|
506
|
-
pause(message) {
|
|
507
|
-
let queue = this.getQueue(message);
|
|
508
|
-
if (!queue) throw new Error("NotPlaying");
|
|
509
|
-
queue.playing = false;
|
|
510
|
-
queue.pause = true;
|
|
511
|
-
queue.dispatcher.pause();
|
|
512
|
-
return queue;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Resume the guild stream
|
|
517
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
518
|
-
* @returns {Queue} The guild queue
|
|
519
|
-
* @throws {NotPlaying} No playing queue
|
|
520
|
-
*/
|
|
521
|
-
resume(message) {
|
|
522
|
-
let queue = this.getQueue(message);
|
|
523
|
-
if (!queue) throw new Error("NotPlaying");
|
|
524
|
-
queue.playing = true;
|
|
525
|
-
queue.pause = false;
|
|
526
|
-
queue.dispatcher.resume();
|
|
527
|
-
return queue;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Stop the guild stream
|
|
532
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
533
|
-
* @throws {NotPlaying} No playing queue
|
|
534
|
-
* @example
|
|
535
|
-
* client.on('message', (message) => {
|
|
536
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
537
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
538
|
-
* const command = args.shift();
|
|
539
|
-
* if (command == "stop") {
|
|
540
|
-
* distube.stop(message);
|
|
541
|
-
* message.channel.send("Stopped the queue!");
|
|
542
|
-
* }
|
|
543
|
-
* });
|
|
544
|
-
*/
|
|
545
|
-
stop(message) {
|
|
546
|
-
let queue = this.getQueue(message);
|
|
547
|
-
if (!queue) throw new Error("NotPlaying");
|
|
548
|
-
queue.stopped = true;
|
|
549
|
-
if (queue.dispatcher) try { queue.dispatcher.end() } catch { }
|
|
550
|
-
if (this.options.leaveOnStop && queue.connection) try { queue.connection.channel.leave() } catch { }
|
|
551
|
-
this._deleteQueue(message);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Set the guild stream's volume
|
|
556
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
557
|
-
* @param {number} percent The percentage of volume you want to set
|
|
558
|
-
* @returns {Queue} The guild queue
|
|
559
|
-
* @throws {NotPlaying} No playing queue
|
|
560
|
-
* @example
|
|
561
|
-
* client.on('message', (message) => {
|
|
562
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
563
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
564
|
-
* const command = args.shift();
|
|
565
|
-
* if (command == "volume")
|
|
566
|
-
* distube.setVolume(message, args[0]);
|
|
567
|
-
* });
|
|
568
|
-
*/
|
|
569
|
-
setVolume(message, percent) {
|
|
570
|
-
let queue = this.getQueue(message);
|
|
571
|
-
if (!queue) throw new Error("NotPlaying");
|
|
572
|
-
queue.volume = percent;
|
|
573
|
-
queue.dispatcher.setVolume(queue.volume / 100);
|
|
574
|
-
return queue
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* Skip the playing song
|
|
579
|
-
*
|
|
580
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
581
|
-
* @returns {Queue} The guild queue
|
|
582
|
-
* @throws {NotPlaying} No playing queue
|
|
583
|
-
* @throws {NoSong} if there is no song in queue
|
|
584
|
-
* @example
|
|
585
|
-
* client.on('message', (message) => {
|
|
586
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
587
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
588
|
-
* const command = args.shift();
|
|
589
|
-
* if (command == "skip")
|
|
590
|
-
* distube.skip(message);
|
|
591
|
-
* });
|
|
592
|
-
*/
|
|
593
|
-
skip(message) {
|
|
594
|
-
let queue = this.getQueue(message);
|
|
595
|
-
if (!queue) throw new Error("NotPlaying");
|
|
596
|
-
if (queue.songs <= 1 && !queue.autoplay) throw new Error("NoSong");
|
|
597
|
-
queue.skipped = true;
|
|
598
|
-
queue.dispatcher.end();
|
|
599
|
-
return queue;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Shuffle the guild queue songs
|
|
604
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
605
|
-
* @returns {Queue} The guild queue
|
|
606
|
-
* @example
|
|
607
|
-
* client.on('message', (message) => {
|
|
608
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
609
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
610
|
-
* const command = args.shift();
|
|
611
|
-
* if (command == "shuffle")
|
|
612
|
-
* distube.shuffle(message);
|
|
613
|
-
* });
|
|
614
|
-
*/
|
|
615
|
-
shuffle(message) {
|
|
616
|
-
let queue = this.getQueue(message);
|
|
617
|
-
if (!queue) throw new Error("NotPlaying");
|
|
618
|
-
let playing = queue.songs.shift();
|
|
619
|
-
for (let i = queue.songs.length - 1; i > 0; i--) {
|
|
620
|
-
let j = Math.floor(Math.random() * (i + 1));
|
|
621
|
-
[queue.songs[i], queue.songs[j]] = [queue.songs[j], queue.songs[i]];
|
|
622
|
-
}
|
|
623
|
-
queue.songs.unshift(playing);
|
|
624
|
-
return queue;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Jump to the song number in the queue.
|
|
629
|
-
* The next one is 1,...
|
|
630
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
631
|
-
* @param {number} num The song number to play
|
|
632
|
-
* @returns {Queue} The guild queue
|
|
633
|
-
* @throws {InvalidSong} if `num` is invalid number (0 < num < {@link Queue#songs}.length)
|
|
634
|
-
* @example
|
|
635
|
-
* client.on('message', (message) => {
|
|
636
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
637
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
638
|
-
* const command = args.shift();
|
|
639
|
-
* if (command == "jump")
|
|
640
|
-
* distube.jump(message, parseInt(args[0]))
|
|
641
|
-
* .catch(err => message.channel.send("Invalid song number."));
|
|
642
|
-
* });
|
|
643
|
-
*/
|
|
644
|
-
jump(message, num) {
|
|
645
|
-
let queue = this.getQueue(message);
|
|
646
|
-
if (!queue) throw new Error("NotPlaying");
|
|
647
|
-
if (num > queue.songs.length || num < 1) throw new Error("InvalidSong");
|
|
648
|
-
queue.songs = queue.songs.splice(num - 1);
|
|
649
|
-
queue.skipped = true;
|
|
650
|
-
if (queue.dispatcher) queue.dispatcher.end();
|
|
651
|
-
return queue;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Set the repeat mode of the guild queue.
|
|
656
|
-
* Turn off if repeat mode is the same value as new mode.
|
|
657
|
-
* Toggle mode: `mode = null` `(0 -> 1 -> 2 -> 0...)`
|
|
658
|
-
*
|
|
659
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
660
|
-
* @param {number} mode The repeat modes `(0: disabled, 1: Repeat a song, 2: Repeat all the queue)`
|
|
661
|
-
* @returns {number} The new repeat mode
|
|
662
|
-
*
|
|
663
|
-
* @example
|
|
664
|
-
* client.on('message', (message) => {
|
|
665
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
666
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
667
|
-
* const command = args.shift();
|
|
668
|
-
* if (command == "repeat") {
|
|
669
|
-
* let mode = distube.setRepeatMode(message, parseInt(args[0]));
|
|
670
|
-
* mode = mode ? mode == 2 ? "Repeat queue" : "Repeat song" : "Off";
|
|
671
|
-
* message.channel.send("Set repeat mode to `" + mode + "`");
|
|
672
|
-
* }
|
|
673
|
-
* });
|
|
674
|
-
*/
|
|
675
|
-
setRepeatMode(message, mode = null) {
|
|
676
|
-
let queue = this.getQueue(message);
|
|
677
|
-
if (!queue) throw new Error("NotPlaying");
|
|
678
|
-
mode = parseInt(mode, 10);
|
|
679
|
-
if (!mode && mode !== 0) queue.repeatMode = (queue.repeatMode + 1) % 3;
|
|
680
|
-
else if (queue.repeatMode === mode) queue.repeatMode = 0;
|
|
681
|
-
else queue.repeatMode = mode;
|
|
682
|
-
return queue.repeatMode;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Toggle autoplay mode
|
|
687
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
688
|
-
* @returns {boolean} Autoplay mode state
|
|
689
|
-
* @throws {NotPlaying} No playing queue
|
|
690
|
-
* @example
|
|
691
|
-
* client.on('message', (message) => {
|
|
692
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
693
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
694
|
-
* const command = args.shift();
|
|
695
|
-
* if (command == "autoplay") {
|
|
696
|
-
* let mode = distube.toggleAutoplay(message);
|
|
697
|
-
* message.channel.send("Set autoplay mode to `" + (mode ? "On" : "Off") + "`");
|
|
698
|
-
* }
|
|
699
|
-
* });
|
|
700
|
-
*/
|
|
701
|
-
toggleAutoplay(message) {
|
|
702
|
-
let queue = this.getQueue(message);
|
|
703
|
-
if (!queue) throw new Error("NotPlaying");
|
|
704
|
-
queue.autoplay = !queue.autoplay;
|
|
705
|
-
return queue.autoplay;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
* Whether or not a guild is playing music.
|
|
710
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel to check
|
|
711
|
-
* @returns {boolean} Whether or not the guild is playing song(s)
|
|
712
|
-
*/
|
|
713
|
-
isPlaying(message) {
|
|
714
|
-
let queue = this.getQueue(message);
|
|
715
|
-
return queue ? queue.playing || !queue.pause : false;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Whether or not the guild queue is paused
|
|
720
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel to check
|
|
721
|
-
* @returns {boolean} Whether or not the guild queue is paused
|
|
722
|
-
*/
|
|
723
|
-
isPaused(message) {
|
|
724
|
-
let queue = this.getQueue(message);
|
|
725
|
-
return queue ? queue.pause : false;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
* Whether or not the queue's voice channel is empty
|
|
730
|
-
* @private
|
|
731
|
-
* @ignore
|
|
732
|
-
* @param {Queue} queue The guild queue
|
|
733
|
-
* @returns {boolean} No user in voice channel return `true`
|
|
734
|
-
*/
|
|
735
|
-
_isVoiceChannelEmpty(queue) {
|
|
736
|
-
let voiceChannel = queue.connection.channel;
|
|
737
|
-
let members = voiceChannel.members.filter(m => !m.user.bot);
|
|
738
|
-
return !members.size;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* TODO: Remove this
|
|
743
|
-
* @deprecated use {@link DisTube#addRelatedVideo} instead
|
|
744
|
-
* @param {DisTube.Message} message Message
|
|
745
|
-
* @returns {Promise<Queue>}
|
|
746
|
-
*/
|
|
747
|
-
runAutoplay(message) {
|
|
748
|
-
console.warn(`\`DisTube#runAutoplay\` is deprecated, use \`DisTube#addRelatedVideo\` instead.`);
|
|
749
|
-
return this.addRelatedVideo(message);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Add related song to the queue
|
|
754
|
-
* @async
|
|
755
|
-
* @param {Discord.Snowflake|Discord.Message} message The message from guild channel
|
|
756
|
-
* @returns {Promise<Queue>} The guild queue
|
|
757
|
-
*/
|
|
758
|
-
async addRelatedVideo(message) {
|
|
759
|
-
let queue = this.getQueue(message);
|
|
760
|
-
if (!queue) throw new Error("NotPlaying");
|
|
761
|
-
let song = queue.songs[0];
|
|
762
|
-
if (!song.youtube) {
|
|
763
|
-
this.emit("noRelated", message);
|
|
764
|
-
return queue;
|
|
765
|
-
}
|
|
766
|
-
let related = song.related;
|
|
767
|
-
if (!Array.isArray(related)) related = (await ytdl.getBasicInfo(song.url, { requestOptions: this.requestOptions })).related_videos;
|
|
768
|
-
if (Array.isArray(related)) {
|
|
769
|
-
const relatedVideo = related.find(s => !queue.previousSongs.includes(s.id));
|
|
770
|
-
if (!relatedVideo && !relatedVideo.id) {
|
|
771
|
-
this.emit("noRelated", message);
|
|
772
|
-
return queue;
|
|
773
|
-
}
|
|
774
|
-
this._addToQueue(message, new Song(await ytdl.getInfo(relatedVideo.id, { requestOptions: this.requestOptions }), this.client.user, true));
|
|
775
|
-
} else this.emit("noRelated", message);
|
|
776
|
-
return queue;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* `@2.0.0` Enable or disable a filter of the queue, replay the playing song.
|
|
781
|
-
* Available filters: {@link Filter}
|
|
782
|
-
*
|
|
783
|
-
* @param {Discord.Message} message The message from guild channel
|
|
784
|
-
* @param {Filter} filter A filter name
|
|
785
|
-
* @returns {string} Current queue's filter name.
|
|
786
|
-
* @example
|
|
787
|
-
* client.on('message', (message) => {
|
|
788
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
789
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
790
|
-
* const command = args.shift();
|
|
791
|
-
* if ([`3d`, `bassboost`, `echo`, `karaoke`, `nightcore`, `vaporwave`].includes(command)) {
|
|
792
|
-
* let filter = distube.setFilter(message, command);
|
|
793
|
-
* message.channel.send("Current queue filter: " + (filter || "Off"));
|
|
794
|
-
* }
|
|
795
|
-
* });
|
|
796
|
-
*/
|
|
797
|
-
setFilter(message, filter) {
|
|
798
|
-
let queue = this.getQueue(message);
|
|
799
|
-
if (!queue) throw new Error("NotPlaying");
|
|
800
|
-
if (!Object.prototype.hasOwnProperty.call(this.filters, filter)) throw new TypeError(`${filter} is not a Filter (https://DisTube.js.org/global.html#Filter).`);
|
|
801
|
-
if (queue.filter === filter) queue.filter = null;
|
|
802
|
-
else queue.filter = filter;
|
|
803
|
-
queue.beginTime = queue.currentTime;
|
|
804
|
-
this._playSong(message);
|
|
805
|
-
return queue.filter;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* `@2.7.0` Set the playing time to another position
|
|
810
|
-
*
|
|
811
|
-
* @param {Discord.Message} message The message from guild channel
|
|
812
|
-
* @param {number} time Time in milliseconds
|
|
813
|
-
* @example
|
|
814
|
-
* client.on('message', message => {
|
|
815
|
-
* if (!message.content.startsWith(config.prefix)) return;
|
|
816
|
-
* const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
|
817
|
-
* const command = args.shift();
|
|
818
|
-
* if (command = 'seek')
|
|
819
|
-
* distube.seek(message, Number(args[0]));
|
|
820
|
-
* });
|
|
821
|
-
*/
|
|
822
|
-
seek(message, time) {
|
|
823
|
-
let queue = this.getQueue(message);
|
|
824
|
-
if (!queue) throw new Error("NotPlaying");
|
|
825
|
-
queue.beginTime = time;
|
|
826
|
-
this._playSong(message);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/**
|
|
830
|
-
* Emit error event
|
|
831
|
-
* @private
|
|
832
|
-
* @ignore
|
|
833
|
-
*/
|
|
834
|
-
_emitError(message, error) {
|
|
835
|
-
if (this.listeners("error").length) this.emit("error", message, error);
|
|
836
|
-
else this.emit("error", error);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* Whether or not emit playSong event
|
|
841
|
-
* @private
|
|
842
|
-
* @ignore
|
|
843
|
-
*/
|
|
844
|
-
_emitPlaySong(queue) {
|
|
845
|
-
if (
|
|
846
|
-
!this.options.emitNewSongOnly ||
|
|
847
|
-
(
|
|
848
|
-
queue.repeatMode !== 1 &&
|
|
849
|
-
(!queue.songs[1] || queue.songs[0].id !== queue.songs[1].id)
|
|
850
|
-
)
|
|
851
|
-
) return true;
|
|
852
|
-
return false;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Create a ytdl stream
|
|
857
|
-
* @private
|
|
858
|
-
* @ignore
|
|
859
|
-
*/
|
|
860
|
-
_createStream(queue) {
|
|
861
|
-
let song = queue.songs[0];
|
|
862
|
-
let encoderArgs = queue.filter ? ["-af", this.filters[queue.filter]] : null;
|
|
863
|
-
let streamOptions = {
|
|
864
|
-
opusEncoded: true,
|
|
865
|
-
filter: song.isLive ? "audioandvideo" : "audioonly",
|
|
866
|
-
quality: "highestaudio",
|
|
867
|
-
highWaterMark: this.options.highWaterMark,
|
|
868
|
-
requestOptions: this.requestOptions,
|
|
869
|
-
encoderArgs,
|
|
870
|
-
seek: queue.beginTime / 1000,
|
|
871
|
-
};
|
|
872
|
-
if (song.youtube) return ytdl(song.info, streamOptions);
|
|
873
|
-
return ytdl.arbitraryStream(song.streamURL, streamOptions);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Play a song on voice connection
|
|
878
|
-
* @private
|
|
879
|
-
* @ignore
|
|
880
|
-
* @param {Discord.Message} message The message from guild channel
|
|
881
|
-
*/
|
|
882
|
-
async _playSong(message) {
|
|
883
|
-
let queue = this.getQueue(message);
|
|
884
|
-
if (!queue) return;
|
|
885
|
-
if (!queue.songs.length) {
|
|
886
|
-
this._deleteQueue(message);
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
let song = queue.songs[0];
|
|
890
|
-
try {
|
|
891
|
-
let errorEmitted = false;
|
|
892
|
-
// Queue.stream.on('info') should works but maybe DisTube#playSong will emit before ytdl#info
|
|
893
|
-
if (song.youtube && !song.info) {
|
|
894
|
-
let { videoDetails } = song.info = await ytdl.getInfo(song.url, { requestOptions: this.requestOptions });
|
|
895
|
-
song.views = parseNumber(videoDetails.viewCount);
|
|
896
|
-
song.likes = parseNumber(videoDetails.likes);
|
|
897
|
-
song.dislikes = parseNumber(videoDetails.dislikes);
|
|
898
|
-
if (song.info.formats.length) {
|
|
899
|
-
song.streamURL = ytdl.chooseFormat(song.info.formats, {
|
|
900
|
-
filter: song.isLive ? "audioandvideo" : "audioonly",
|
|
901
|
-
quality: "highestaudio",
|
|
902
|
-
}).url;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
let stream = this._createStream(queue).on("error", e => {
|
|
906
|
-
errorEmitted = true;
|
|
907
|
-
e.message = `${e.message}\nID: ${song.id}\nName: ${song.name}`;
|
|
908
|
-
this._emitError(message, e);
|
|
909
|
-
});
|
|
910
|
-
queue.dispatcher = queue.connection.play(stream, {
|
|
911
|
-
highWaterMark: 1,
|
|
912
|
-
type: "opus",
|
|
913
|
-
volume: queue.volume / 100,
|
|
914
|
-
bitrate: "auto",
|
|
915
|
-
}).on("finish", () => this._handleSongFinish(message, queue))
|
|
916
|
-
.on("error", e => {
|
|
917
|
-
this._handlePlayingError(message, queue, errorEmitted ? null : e);
|
|
918
|
-
});
|
|
919
|
-
if (queue.stream) queue.stream.destroy();
|
|
920
|
-
queue.stream = stream;
|
|
921
|
-
} catch (e) {
|
|
922
|
-
this._handlePlayingError(message, queue, e);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
/**
|
|
927
|
-
* Handle the queue when a Song finish
|
|
928
|
-
* @private
|
|
929
|
-
* @ignore
|
|
930
|
-
* @param {Discord.Message} message message
|
|
931
|
-
* @param {Queue} queue queue
|
|
932
|
-
*/
|
|
933
|
-
async _handleSongFinish(message, queue) {
|
|
934
|
-
if (queue.stopped) return;
|
|
935
|
-
if (this.options.leaveOnEmpty && this._isVoiceChannelEmpty(queue)) {
|
|
936
|
-
this._deleteQueue(message);
|
|
937
|
-
queue.connection.channel.leave();
|
|
938
|
-
this.emit("empty", message);
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
if (queue.repeatMode === 2 && !queue.skipped) queue.songs.push(queue.songs[0]);
|
|
942
|
-
if (queue.songs.length <= 1 && (queue.skipped || !queue.repeatMode)) {
|
|
943
|
-
if (queue.autoplay) try { await this.addRelatedVideo(message) } catch { this.emit("noRelated", message) }
|
|
944
|
-
if (queue.songs.length <= 1) {
|
|
945
|
-
this._deleteQueue(message);
|
|
946
|
-
if (this.options.leaveOnFinish && !queue.stopped) queue.connection.channel.leave();
|
|
947
|
-
if (!queue.autoplay) this.emit("finish", message);
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
const emitSong = this._emitPlaySong(queue);
|
|
952
|
-
if (queue.repeatMode !== 1 || queue.skipped) {
|
|
953
|
-
const { id } = queue.songs.shift();
|
|
954
|
-
queue.previousSongs.push(id);
|
|
955
|
-
}
|
|
956
|
-
queue.skipped = false;
|
|
957
|
-
queue.beginTime = 0;
|
|
958
|
-
await this._playSong(message);
|
|
959
|
-
if (emitSong) this.emit("playSong", message, queue, queue.songs[0]);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
/**
|
|
963
|
-
* Handle error while playing
|
|
964
|
-
* @private
|
|
965
|
-
* @ignore
|
|
966
|
-
* @param {Discord.Message} message message
|
|
967
|
-
* @param {Queue} queue queue
|
|
968
|
-
* @param {Error} error error
|
|
969
|
-
*/
|
|
970
|
-
_handlePlayingError(message, queue, error = null) {
|
|
971
|
-
let song = queue.songs.shift();
|
|
972
|
-
if (error) {
|
|
973
|
-
error.message = `${error.message}\nID: ${song.id}\nName: ${song.name}`;
|
|
974
|
-
this._emitError(message, error);
|
|
975
|
-
}
|
|
976
|
-
if (queue.songs.length > 0) this._playSong(message).then(() => this.emit("playSong", message, queue, queue.songs[0]));
|
|
977
|
-
else try { this.stop(message) } catch { this._deleteQueue(message) }
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
module.exports = DisTube;
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Emitted after DisTube add playlist to guild queue
|
|
985
|
-
*
|
|
986
|
-
* @event DisTube#addList
|
|
987
|
-
* @param {Discord.Message} message The message from guild channel
|
|
988
|
-
* @param {Queue} queue The guild queue
|
|
989
|
-
* @param {Playlist} playlist Playlist info
|
|
990
|
-
* @since 1.1.0
|
|
991
|
-
* @example
|
|
992
|
-
* const status = (queue) => `Volume: \`${queue.volume}%\` | Loop: \`${queue.repeatMode ? queue.repeatMode == 2 ? "Server Queue" : "This Song" : "Off"}\` | Autoplay: \`${queue.autoplay ? "On" : "Off"}\``;
|
|
993
|
-
* distube.on("addList", (message, queue, playlist) => message.channel.send(
|
|
994
|
-
* `Added \`${playlist.name}\` playlist (${playlist.songs.length} songs) to queue\n${status(queue)}`
|
|
995
|
-
* ));
|
|
996
|
-
*/
|
|
997
|
-
|
|
998
|
-
/**
|
|
999
|
-
* Emitted after DisTube add new song to guild queue
|
|
1000
|
-
*
|
|
1001
|
-
* @event DisTube#addSong
|
|
1002
|
-
* @param {Discord.Message} message The message from guild channel
|
|
1003
|
-
* @param {Queue} queue The guild queue
|
|
1004
|
-
* @param {Song} song Added song
|
|
1005
|
-
* @example
|
|
1006
|
-
* const status = (queue) => `Volume: \`${queue.volume}%\` | Loop: \`${queue.repeatMode ? queue.repeatMode == 2 ? "Server Queue" : "This Song" : "Off"}\` | Autoplay: \`${queue.autoplay ? "On" : "Off"}\``;
|
|
1007
|
-
* distube.on("addSong", (message, queue, song) => message.channel.send(
|
|
1008
|
-
* `Added ${song.name} - \`${song.formattedDuration}\` to the queue by ${song.user}`
|
|
1009
|
-
* ));
|
|
1010
|
-
*/
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* Emitted when there is no user in VoiceChannel and {@link DisTubeOptions}.leaveOnEmpty is `true`.
|
|
1014
|
-
*
|
|
1015
|
-
* @event DisTube#empty
|
|
1016
|
-
* @param {Discord.Message} message The message from guild channel
|
|
1017
|
-
* @example
|
|
1018
|
-
* distube.on("empty", message => message.channel.send("Channel is empty. Leaving the channel"))
|
|
1019
|
-
*/
|
|
1020
|
-
|
|
1021
|
-
/**
|
|
1022
|
-
* Emitted when {@link DisTube} encounters an error.
|
|
1023
|
-
*
|
|
1024
|
-
* @event DisTube#error
|
|
1025
|
-
* @param {Discord.Message} message The message from guild channel
|
|
1026
|
-
* @param {Error} err The error encountered
|
|
1027
|
-
* @example
|
|
1028
|
-
* distube.on("error", (message, err) => message.channel.send(
|
|
1029
|
-
* "An error encountered: " + err
|
|
1030
|
-
* ));
|
|
1031
|
-
*/
|
|
1032
|
-
|
|
1033
|
-
/**
|
|
1034
|
-
* Emitted when there is no more song in the queue and {@link Queue#autoplay} is `false`.
|
|
1035
|
-
* DisTube will leave voice channel if {@link DisTubeOptions}.leaveOnFinish is `true`
|
|
1036
|
-
*
|
|
1037
|
-
* @event DisTube#finish
|
|
1038
|
-
* @param {Discord.Message} message The message from guild channel
|
|
1039
|
-
* @example
|
|
1040
|
-
* distube.on("finish", message => message.channel.send("No more song in queue"));
|
|
1041
|
-
*/
|
|
1042
|
-
|
|
1043
|
-
/**
|
|
1044
|
-
* `@2.3.0` Emitted when DisTube initialize a queue to change queue default properties.
|
|
1045
|
-
*
|
|
1046
|
-
* @event DisTube#initQueue
|
|
1047
|
-
* @param {Queue} queue The guild queue
|
|
1048
|
-
* @example
|
|
1049
|
-
* distube.on("initQueue", queue => {
|
|
1050
|
-
* queue.autoplay = false;
|
|
1051
|
-
* queue.volume = 100;
|
|
1052
|
-
* });
|
|
1053
|
-
*/
|
|
1054
|
-
|
|
1055
|
-
/**
|
|
1056
|
-
* Emitted when {@link Queue#autoplay} is `true`, the {@link Queue#songs} is empty and
|
|
1057
|
-
* DisTube cannot find related songs to play
|
|
1058
|
-
*
|
|
1059
|
-
* @event DisTube#noRelated
|
|
1060
|
-
* @param {Discord.Message} message The message from guild channel
|
|
1061
|
-
* @example
|
|
1062
|
-
* distube.on("noRelated", message => message.channel.send("Can't find related video to play. Stop playing music."));
|
|
1063
|
-
*/
|
|
1064
|
-
|
|
1065
|
-
/**
|
|
1066
|
-
* Emitted after DisTube play the first song of the playlist
|
|
1067
|
-
* and add the rest to the guild queue
|
|
1068
|
-
*
|
|
1069
|
-
* @event DisTube#playList
|
|
1070
|
-
* @param {Discord.Message} message The message from guild channel
|
|
1071
|
-
* @param {Queue} queue The guild queue
|
|
1072
|
-
* @param {Playlist} playlist Playlist info
|
|
1073
|
-
* @param {Song} song Playing song
|
|
1074
|
-
* @example
|
|
1075
|
-
* const status = (queue) => `Volume: \`${queue.volume}%\` | Loop: \`${queue.repeatMode ? queue.repeatMode == 2 ? "Server Queue" : "This Song" : "Off"}\` | Autoplay: \`${queue.autoplay ? "On" : "Off"}\``;
|
|
1076
|
-
* distube.on("playList", (message, queue, playlist, song) => message.channel.send(
|
|
1077
|
-
* `Play \`${playlist.name}\` playlist (${playlist.songs.length} songs).\nRequested by: ${song.user}\nNow playing \`${song.name}\` - \`${song.formattedDuration}\`\n${status(queue)}`
|
|
1078
|
-
* ));
|
|
1079
|
-
*/
|
|
1080
|
-
|
|
1081
|
-
/**
|
|
1082
|
-
* Emitted when DisTube play a song.
|
|
1083
|
-
* If {@link DisTubeOptions}.emitNewSongOnly is `true`, event is not emitted when looping a song or next song is the previous one
|
|
1084
|
-
*
|
|
1085
|
-
* @event DisTube#playSong
|
|
1086
|
-
* @param {Discord.Message} message The message from guild channel
|
|
1087
|
-
* @param {Queue} queue The guild queue
|
|
1088
|
-
* @param {Song} song Playing song
|
|
1089
|
-
* @example
|
|
1090
|
-
* const status = (queue) => `Volume: \`${queue.volume}%\` | Loop: \`${queue.repeatMode ? queue.repeatMode == 2 ? "Server Queue" : "This Song" : "Off"}\` | Autoplay: \`${queue.autoplay ? "On" : "Off"}\``;
|
|
1091
|
-
* distube.on("playSong", (message, queue, song) => message.channel.send(
|
|
1092
|
-
* `Playing \`${song.name}\` - \`${song.formattedDuration}\`\nRequested by: ${song.user}\n${status(queue)}`
|
|
1093
|
-
* ));
|
|
1094
|
-
*/
|
|
1095
|
-
|
|
1096
|
-
/**
|
|
1097
|
-
* Emitted when {@link DisTubeOptions}.searchSongs is `true`.
|
|
1098
|
-
* Search will be canceled if user's next message is invalid number or timeout (60s)
|
|
1099
|
-
*
|
|
1100
|
-
* @event DisTube#searchCancel
|
|
1101
|
-
* @param {Discord.Message} message The message from guild channel
|
|
1102
|
-
* @example
|
|
1103
|
-
* // DisTubeOptions.searchSongs = true
|
|
1104
|
-
* distube.on("searchCancel", (message) => message.channel.send(`Searching canceled`));
|
|
1105
|
-
*/
|
|
1106
|
-
|
|
1107
|
-
/**
|
|
1108
|
-
* Emitted when {@link DisTubeOptions}.searchSongs is `true`.
|
|
1109
|
-
* DisTube will wait for user's next message to choose song manually
|
|
1110
|
-
* if song param of {@link DisTube#play|play()} is invalid url
|
|
1111
|
-
*
|
|
1112
|
-
* @event DisTube#searchResult
|
|
1113
|
-
* @param {Discord.Message} message The message from guild channel
|
|
1114
|
-
* @param {SearchResult[]} result Searched result (max length = 12)
|
|
1115
|
-
* @example
|
|
1116
|
-
* // DisTubeOptions.searchSongs = true
|
|
1117
|
-
* distube.on("searchResult", (message, result) => {
|
|
1118
|
-
* let i = 0;
|
|
1119
|
-
* message.channel.send(`**Choose an option from below**\n${result.map(song => `**${++i}**. ${song.name} - \`${song.formattedDuration}\``).join("\n")}\n*Enter anything else or wait 60 seconds to cancel*`);
|
|
1120
|
-
* });
|
|
1121
|
-
*/
|