distube 3.0.0-beta.9 → 3.0.3
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/LICENSE +21 -21
- package/README.md +64 -51
- 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 +95 -0
- package/dist/core/DisTubeHandler.d.ts.map +1 -0
- package/dist/core/DisTubeHandler.js +337 -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 +85 -0
- package/dist/core/voice/DisTubeVoice.d.ts.map +1 -0
- package/dist/core/voice/DisTubeVoice.js +246 -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 +56 -0
- package/dist/struct/DisTubeError.d.ts.map +1 -0
- package/dist/struct/DisTubeError.js +75 -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 +481 -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 +88 -62
- package/src/DisTube.js +0 -851
- package/src/DisTubeBase.js +0 -39
- package/src/DisTubeHandler.js +0 -440
- package/src/DisTubeOptions.js +0 -82
- package/src/Filter.js +0 -36
- package/src/Playlist.js +0 -75
- package/src/Plugin/CustomPlugin.js +0 -26
- package/src/Plugin/ExtractorPlugin.js +0 -25
- package/src/Plugin/Plugin.js +0 -36
- package/src/Plugin/http.js +0 -27
- package/src/Plugin/https.js +0 -27
- package/src/Queue.js +0 -340
- package/src/SearchResult.js +0 -57
- package/src/Song.js +0 -169
- package/src/util.js +0 -65
- package/typings/DisTube.d.ts +0 -553
- package/typings/DisTubeBase.d.ts +0 -31
- package/typings/DisTubeHandler.d.ts +0 -130
- package/typings/DisTubeOptions.d.ts +0 -5
- package/typings/Filter.d.ts +0 -83
- package/typings/Playlist.d.ts +0 -58
- package/typings/Plugin/CustomPlugin.d.ts +0 -21
- package/typings/Plugin/ExtractorPlugin.d.ts +0 -20
- package/typings/Plugin/Plugin.d.ts +0 -31
- package/typings/Plugin/http.d.ts +0 -4
- package/typings/Plugin/https.d.ts +0 -4
- package/typings/Queue.d.ts +0 -227
- package/typings/SearchResult.d.ts +0 -51
- package/typings/Song.d.ts +0 -153
- package/typings/util.d.ts +0 -6
package/src/DisTubeBase.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// eslint-disable-next-line no-unused-vars
|
|
2
|
-
const DisTube = require("./DisTube");
|
|
3
|
-
// eslint-disable-next-line no-unused-vars
|
|
4
|
-
const Discord = require("discord.js");
|
|
5
|
-
|
|
6
|
-
/** @private */
|
|
7
|
-
class DisTubeBase {
|
|
8
|
-
/** @param {DisTube} distube distube */
|
|
9
|
-
constructor(distube) {
|
|
10
|
-
/**
|
|
11
|
-
* DisTube
|
|
12
|
-
* @type {DisTube}
|
|
13
|
-
* @private
|
|
14
|
-
*/
|
|
15
|
-
this.distube = distube;
|
|
16
|
-
/**
|
|
17
|
-
* DisTube options
|
|
18
|
-
* @type {DisTube.DisTubeOptions}
|
|
19
|
-
* @private
|
|
20
|
-
*/
|
|
21
|
-
this.options = this.distube.options;
|
|
22
|
-
/**
|
|
23
|
-
* Discord.js client
|
|
24
|
-
* @type {Discord.Client}
|
|
25
|
-
* @private
|
|
26
|
-
*/
|
|
27
|
-
this.client = this.distube.client;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Redirect emitter
|
|
31
|
-
* @private
|
|
32
|
-
* @param {...any} args arguments
|
|
33
|
-
*/
|
|
34
|
-
emit(...args) {
|
|
35
|
-
this.distube.emit(...args);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
module.exports = DisTubeBase;
|
package/src/DisTubeHandler.js
DELETED
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
const ytdl = require("@distube/ytdl"),
|
|
2
|
-
ytpl = require("@distube/ytpl"),
|
|
3
|
-
Song = require("./Song"),
|
|
4
|
-
SearchResult = require("./SearchResult"),
|
|
5
|
-
Playlist = require("./Playlist"),
|
|
6
|
-
{ parseNumber, isURL } = require("./util"),
|
|
7
|
-
youtube_dl = require("@distube/youtube-dl"),
|
|
8
|
-
DisTubeBase = require("./DisTubeBase"),
|
|
9
|
-
// eslint-disable-next-line no-unused-vars
|
|
10
|
-
Queue = require("./Queue"),
|
|
11
|
-
// eslint-disable-next-line no-unused-vars
|
|
12
|
-
{ opus } = require("prism-media"),
|
|
13
|
-
// eslint-disable-next-line no-unused-vars
|
|
14
|
-
Discord = require("discord.js");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* DisTube's Handler
|
|
19
|
-
* @extends DisTubeBase
|
|
20
|
-
* @private
|
|
21
|
-
*/
|
|
22
|
-
class DisTubeHandler extends DisTubeBase {
|
|
23
|
-
constructor(distube) {
|
|
24
|
-
super(distube);
|
|
25
|
-
const requestOptions = this.options.youtubeCookie ? { headers: { cookie: this.options.youtubeCookie, "x-youtube-identity-token": this.options.youtubeIdentityToken } } : undefined;
|
|
26
|
-
this.ytdlOptions = Object.assign(this.options.ytdlOptions, { requestOptions });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Emit error event
|
|
31
|
-
* @param {Discord.TextChannel} channel Text channel where the error is encountered.
|
|
32
|
-
* @param {Error} error error
|
|
33
|
-
* @private
|
|
34
|
-
*/
|
|
35
|
-
emitError(channel, error) {
|
|
36
|
-
this.distube.emitError(channel, error);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Delete a guild queue
|
|
41
|
-
* @param {Discord.Snowflake|Discord.Message|Queue} queue A message from guild channel | Queue
|
|
42
|
-
*/
|
|
43
|
-
deleteQueue(queue) {
|
|
44
|
-
this.distube._deleteQueue(queue);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @param {string} url url
|
|
49
|
-
* @returns {Promise<ytdl.videoInfo>}
|
|
50
|
-
*/
|
|
51
|
-
getYouTubeInfo(url) {
|
|
52
|
-
return ytdl.getInfo(url, this.ytdlOptions);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Resolve a Song
|
|
57
|
-
* @param {Discord.Message|Discord.GuildMember} message A message from guild channel | A guild member
|
|
58
|
-
* @param {string|Song|SearchResult|Playlist} song YouTube url | Search string | {@link Song}
|
|
59
|
-
* @returns {Promise<Song|Array<Song>|Playlist>} Resolved Song
|
|
60
|
-
*/
|
|
61
|
-
async resolveSong(message, song) {
|
|
62
|
-
if (!song) return null;
|
|
63
|
-
const member = message?.member || message;
|
|
64
|
-
if (song instanceof Song || song instanceof Playlist) return song;
|
|
65
|
-
if (song instanceof SearchResult) {
|
|
66
|
-
if (song.type === "video") return new Song(await this.getYouTubeInfo(song.url), member);
|
|
67
|
-
else if (song.type === "playlist") return this.resolvePlaylist(message, song.url);
|
|
68
|
-
throw new Error("Invalid SearchResult");
|
|
69
|
-
}
|
|
70
|
-
if (typeof song === "object") return new Song(song, member);
|
|
71
|
-
if (ytdl.validateURL(song)) return new Song(await this.getYouTubeInfo(song), member);
|
|
72
|
-
if (isURL(song)) {
|
|
73
|
-
for (const plugin of this.distube.extractorPlugins) if (await plugin.validate(song)) return plugin.resolve(song, member);
|
|
74
|
-
if (!this.options.youtubeDL) throw new Error("Not Supported URL!");
|
|
75
|
-
const info = await youtube_dl(song, {
|
|
76
|
-
dumpJson: true,
|
|
77
|
-
noWarnings: true,
|
|
78
|
-
}).catch(e => { throw new Error(`[youtube-dl] ${e.stderr || e}`) });
|
|
79
|
-
if (Array.isArray(info) && info.length > 0) return this.resolvePlaylist(message, info.map(i => new Song(i, member, i.extractor)));
|
|
80
|
-
return new Song(info, member, info.extractor);
|
|
81
|
-
}
|
|
82
|
-
if (typeof song !== "string") throw new TypeError("song is not a valid type");
|
|
83
|
-
if (message instanceof Discord.GuildMember) song = (await this.distube.search(song, { limit: 1 }))[0];
|
|
84
|
-
else song = await this.searchSong(message, song);
|
|
85
|
-
return this.resolveSong(message, song);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Resole Song[] or url to a Playlist
|
|
90
|
-
* @param {Discord.Message|Discord.GuildMember} message A message from guild channel | A guild member
|
|
91
|
-
* @param {Array<Song>|string} playlist Resolvable playlist
|
|
92
|
-
* @returns {Promise<Playlist>}
|
|
93
|
-
*/
|
|
94
|
-
async resolvePlaylist(message, playlist) {
|
|
95
|
-
const member = message?.member || message;
|
|
96
|
-
if (typeof playlist === "string") {
|
|
97
|
-
playlist = await ytpl(playlist, { limit: Infinity });
|
|
98
|
-
playlist.items = playlist.items.filter(v => !v.thumbnail.includes("no_thumbnail")).map(v => new Song(v, member));
|
|
99
|
-
}
|
|
100
|
-
if (!(playlist instanceof Playlist)) playlist = new Playlist(playlist, member);
|
|
101
|
-
return playlist;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Create a custom playlist
|
|
106
|
-
* @returns {Promise<Playlist>}
|
|
107
|
-
* @param {Discord.Message|Discord.GuildMember} message A message from guild channel | A guild member
|
|
108
|
-
* @param {Array<string|Song|SearchResult>} songs Array of url, Song or SearchResult
|
|
109
|
-
* @param {Object} [properties={}] Additional properties such as `name`
|
|
110
|
-
* @param {boolean} [parallel=true] Whether or not fetch the songs in parallel
|
|
111
|
-
*/
|
|
112
|
-
async createCustomPlaylist(message, songs, properties = {}, parallel = true) {
|
|
113
|
-
const member = message?.member || message;
|
|
114
|
-
if (!Array.isArray(songs)) throw new TypeError("songs must be an array of url");
|
|
115
|
-
if (!songs.length) throw new Error("songs is an empty array");
|
|
116
|
-
songs = songs.filter(song => song instanceof Song || song instanceof SearchResult || isURL(song));
|
|
117
|
-
if (!songs.length) throw new Error("songs does not have any valid Song, SearchResult or url");
|
|
118
|
-
if (parallel) {
|
|
119
|
-
songs = songs.map(song => this.resolveSong(member, song).catch(() => undefined));
|
|
120
|
-
songs = await Promise.all(songs);
|
|
121
|
-
} else {
|
|
122
|
-
const resolved = [];
|
|
123
|
-
for (const song of songs) resolved.push(await this.resolveSong(member, song).catch(() => undefined));
|
|
124
|
-
songs = resolved;
|
|
125
|
-
}
|
|
126
|
-
songs = songs.filter(song => song);
|
|
127
|
-
return new Playlist(songs, member, properties);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Play / add a playlist
|
|
132
|
-
* @returns {Promise<void>}
|
|
133
|
-
* @param {Discord.Message|Discord.VoiceChannel|Discord.StageChannel} message A message from guild channel | a voice channel
|
|
134
|
-
* @param {Playlist|string} playlist A YouTube playlist url | a Playlist
|
|
135
|
-
* @param {boolean} [textChannel] The default text channel of the queue
|
|
136
|
-
* @param {boolean} [skip=false] Skip the current song
|
|
137
|
-
*/
|
|
138
|
-
async handlePlaylist(message, playlist, textChannel = false, skip = false) {
|
|
139
|
-
if (typeof textChannel === "boolean") {
|
|
140
|
-
skip = textChannel;
|
|
141
|
-
textChannel = message.channel;
|
|
142
|
-
}
|
|
143
|
-
if (!playlist || !(playlist instanceof Playlist)) throw Error("Invalid Playlist");
|
|
144
|
-
if (this.options.nsfw && !textChannel?.nsfw) {
|
|
145
|
-
playlist.songs = playlist.songs.filter(s => !s.age_restricted);
|
|
146
|
-
}
|
|
147
|
-
if (!playlist.songs.length) {
|
|
148
|
-
if (this.options.nsfw && !textChannel?.nsfw) {
|
|
149
|
-
throw new Error("No valid video in the playlist.\nMaybe age-restricted contents is filtered because you are in non-NSFW channel.");
|
|
150
|
-
}
|
|
151
|
-
throw Error("No valid video in the playlist");
|
|
152
|
-
}
|
|
153
|
-
const songs = playlist.songs;
|
|
154
|
-
let queue = this.distube.getQueue(message);
|
|
155
|
-
if (queue) {
|
|
156
|
-
queue.addToQueue(songs, skip);
|
|
157
|
-
if (skip) queue.skip();
|
|
158
|
-
else this.emit("addList", queue, playlist);
|
|
159
|
-
} else {
|
|
160
|
-
queue = await this.distube._newQueue(message, songs, textChannel);
|
|
161
|
-
if (queue !== true) this.emit("playSong", queue, queue.songs[0]);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Search for a song, fire {@link DisTube#event:error} if not found.
|
|
167
|
-
* @param {Discord.Message} message A message from guild channel
|
|
168
|
-
* @param {string} query The query string
|
|
169
|
-
* @returns {Promise<Song?>} Song info
|
|
170
|
-
*/
|
|
171
|
-
async searchSong(message, query) {
|
|
172
|
-
const results = await this.distube.search(query, {
|
|
173
|
-
limit: this.options.searchSongs || 1,
|
|
174
|
-
safeSearch: this.options.nsfw ? false : !message.channel?.nsfw,
|
|
175
|
-
}).catch(() => undefined);
|
|
176
|
-
if (!results?.length) {
|
|
177
|
-
this.emit("searchNoResult", message, query);
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
let result = results[0];
|
|
181
|
-
if (this.options.searchSongs && this.options.searchSongs > 1) {
|
|
182
|
-
this.emit("searchResult", message, results, query);
|
|
183
|
-
const answers = await message.channel.awaitMessages(m => m.author.id === message.author.id, {
|
|
184
|
-
max: 1,
|
|
185
|
-
time: this.options.searchCooldown * 1000,
|
|
186
|
-
errors: ["time"],
|
|
187
|
-
}).catch(() => undefined);
|
|
188
|
-
const ans = answers?.first();
|
|
189
|
-
if (!ans) {
|
|
190
|
-
this.emit("searchCancel", message, query);
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
const index = parseInt(ans.content, 10);
|
|
194
|
-
if (isNaN(index) || index > results.length || index < 1) {
|
|
195
|
-
this.emit("searchCancel", message, query);
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
this.emit("searchDone", message, ans, query);
|
|
199
|
-
result = results[index - 1];
|
|
200
|
-
}
|
|
201
|
-
return result;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Join the voice channel
|
|
206
|
-
* @param {Queue} queue A message from guild channel
|
|
207
|
-
* @param {Discord.VoiceChannel|Discord.StageChannel} voice The string search for
|
|
208
|
-
* @param {boolean} retried retried?
|
|
209
|
-
* @throws {Error}
|
|
210
|
-
* @returns {Promise<Queue|true>} `true` if queue is not generated
|
|
211
|
-
*/
|
|
212
|
-
async joinVoiceChannel(queue, voice, retried = false) {
|
|
213
|
-
try {
|
|
214
|
-
queue.connection = await voice.join();
|
|
215
|
-
this.emit("connect", queue);
|
|
216
|
-
queue.connection.on("disconnect", () => {
|
|
217
|
-
this.emit("disconnect", queue);
|
|
218
|
-
try { queue.stop() } catch { this.deleteQueue(queue) }
|
|
219
|
-
}).on("error", e => {
|
|
220
|
-
e.name = "VoiceConnection";
|
|
221
|
-
this.emitError(queue.textChannel, e);
|
|
222
|
-
try { queue.stop() } catch { this.deleteQueue(queue) }
|
|
223
|
-
});
|
|
224
|
-
const err = await this.playSong(queue);
|
|
225
|
-
return err || queue;
|
|
226
|
-
} catch (e) {
|
|
227
|
-
this.deleteQueue(queue);
|
|
228
|
-
e.name = "JoinVoiceChannel";
|
|
229
|
-
if (retried) throw e;
|
|
230
|
-
return this.joinVoiceChannel(queue, voice, true);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Get related songs
|
|
236
|
-
* @param {Song} song song
|
|
237
|
-
* @returns {Array<ytdl.relatedVideo>} Related videos
|
|
238
|
-
* @throws {NoRelated}
|
|
239
|
-
*/
|
|
240
|
-
async getRelatedVideo(song) {
|
|
241
|
-
if (song.source !== "youtube") throw new Error("NoRelated");
|
|
242
|
-
let related = song.related;
|
|
243
|
-
if (!related) related = (await ytdl.getBasicInfo(song.url, this.ytdlOptions)).related_videos;
|
|
244
|
-
if (!related || !related.length) throw new Error("NoRelated");
|
|
245
|
-
return related;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Create a ytdl stream
|
|
250
|
-
* @param {Queue} queue Queue
|
|
251
|
-
* @returns {opus.Encoder}
|
|
252
|
-
*/
|
|
253
|
-
createStream(queue) {
|
|
254
|
-
const song = queue.songs[0];
|
|
255
|
-
const filterArgs = [];
|
|
256
|
-
queue.filters.forEach(filter => filterArgs.push(this.distube.filters[filter]));
|
|
257
|
-
const encoderArgs = queue.filters?.length ? ["-af", filterArgs.join(",")] : null;
|
|
258
|
-
const seek = song.duration ? queue.beginTime : undefined;
|
|
259
|
-
const streamOptions = {
|
|
260
|
-
opusEncoded: true,
|
|
261
|
-
filter: song.isLive ? "audioandvideo" : "audioonly",
|
|
262
|
-
quality: "highestaudio",
|
|
263
|
-
encoderArgs,
|
|
264
|
-
seek,
|
|
265
|
-
};
|
|
266
|
-
Object.assign(streamOptions, this.ytdlOptions);
|
|
267
|
-
if (song.source === "youtube") return ytdl(song.info, streamOptions);
|
|
268
|
-
return ytdl.arbitraryStream(song.streamURL, streamOptions);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async checkYouTubeInfo(song) {
|
|
272
|
-
if (!song.info) {
|
|
273
|
-
const { videoDetails } = song.info = await this.getYouTubeInfo(song.url);
|
|
274
|
-
song.views = parseNumber(videoDetails.viewCount);
|
|
275
|
-
song.likes = parseNumber(videoDetails.likes);
|
|
276
|
-
song.dislikes = parseNumber(videoDetails.dislikes);
|
|
277
|
-
if (song.info.formats.length) {
|
|
278
|
-
song.streamURL = ytdl.chooseFormat(song.info.formats, {
|
|
279
|
-
filter: song.isLive ? "audioandvideo" : "audioonly",
|
|
280
|
-
quality: "highestaudio",
|
|
281
|
-
}).url;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const err = require("ytdl-core/lib/utils").playError(song.info.player_response, ["UNPLAYABLE", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"]);
|
|
285
|
-
if (err) throw err;
|
|
286
|
-
if (!song.info.formats.length) throw new Error("This video is unavailable");
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Whether or not emit playSong event
|
|
291
|
-
* @param {Queue} queue Queue
|
|
292
|
-
* @private
|
|
293
|
-
* @returns {boolean}
|
|
294
|
-
*/
|
|
295
|
-
_emitPlaySong(queue) {
|
|
296
|
-
if (
|
|
297
|
-
!this.options.emitNewSongOnly ||
|
|
298
|
-
(
|
|
299
|
-
queue.repeatMode !== 1 &&
|
|
300
|
-
queue.songs[0]?.id !== queue.songs[1]?.id
|
|
301
|
-
)
|
|
302
|
-
) return true;
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Play a song on voice connection
|
|
308
|
-
* @param {Queue} queue The guild queue
|
|
309
|
-
* @returns {Promise<boolean>} error?
|
|
310
|
-
*/
|
|
311
|
-
async playSong(queue) {
|
|
312
|
-
if (!queue) return true;
|
|
313
|
-
if (!queue.songs.length) {
|
|
314
|
-
this.deleteQueue(queue);
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
const song = queue.songs[0];
|
|
318
|
-
try {
|
|
319
|
-
let errorEmitted = false;
|
|
320
|
-
if (song.source === "youtube") await this.checkYouTubeInfo(song);
|
|
321
|
-
const stream = this.createStream(queue).on("error", e => {
|
|
322
|
-
errorEmitted = true;
|
|
323
|
-
e.name = "Stream";
|
|
324
|
-
e.message = `${e.message}\nID: ${song.id}\nName: ${song.name}`;
|
|
325
|
-
this.emitError(queue.textChannel, e);
|
|
326
|
-
});
|
|
327
|
-
queue.dispatcher = queue.connection.play(stream, {
|
|
328
|
-
highWaterMark: 1,
|
|
329
|
-
type: "opus",
|
|
330
|
-
volume: queue.volume / 100,
|
|
331
|
-
bitrate: "auto",
|
|
332
|
-
}).on("finish", () => { this._handleSongFinish(queue) })
|
|
333
|
-
.on("error", e => { this._handlePlayingError(queue, errorEmitted ? null : e) });
|
|
334
|
-
if (queue.stream) queue.stream.destroy();
|
|
335
|
-
queue.stream = stream;
|
|
336
|
-
return false;
|
|
337
|
-
} catch (e) {
|
|
338
|
-
this._handlePlayingError(queue, e);
|
|
339
|
-
return true;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Handle the queue when a Song finish
|
|
345
|
-
* @private
|
|
346
|
-
* @param {Queue} queue queue
|
|
347
|
-
* @returns {Promise<void>}
|
|
348
|
-
*/
|
|
349
|
-
async _handleSongFinish(queue) {
|
|
350
|
-
this.emit("finishSong", queue, queue.songs[0]);
|
|
351
|
-
if (queue.stopped) {
|
|
352
|
-
this.deleteQueue(queue);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
if (queue.repeatMode === 2 && !queue.prev) queue.songs.push(queue.songs[0]);
|
|
356
|
-
if (queue.prev) {
|
|
357
|
-
if (queue.repeatMode === 2) queue.songs.unshift(queue.songs.pop());
|
|
358
|
-
else queue.songs.unshift(queue.previousSongs.pop());
|
|
359
|
-
}
|
|
360
|
-
if (queue.songs.length <= 1 && (queue.next || !queue.repeatMode)) {
|
|
361
|
-
if (queue.autoplay) try { await queue.addRelatedVideo() } catch { this.emit("noRelated", queue) }
|
|
362
|
-
if (queue.songs.length <= 1) {
|
|
363
|
-
if (this.options.leaveOnFinish) queue.connection.channel.leave();
|
|
364
|
-
if (!queue.autoplay) this.emit("finish", queue);
|
|
365
|
-
this.deleteQueue(queue);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
const emitPlaySong = this._emitPlaySong(queue);
|
|
370
|
-
if (!queue.prev && (queue.repeatMode !== 1 || queue.next)) {
|
|
371
|
-
const prev = queue.songs.shift();
|
|
372
|
-
if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
|
|
373
|
-
}
|
|
374
|
-
queue.next = queue.prev = false;
|
|
375
|
-
queue.beginTime = 0;
|
|
376
|
-
const err = await this.playSong(queue);
|
|
377
|
-
if (!err && emitPlaySong) this.emit("playSong", queue, queue.songs[0]);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Handle error while playing
|
|
382
|
-
* @private
|
|
383
|
-
* @param {Queue} queue queue
|
|
384
|
-
* @param {Error} error error
|
|
385
|
-
*/
|
|
386
|
-
_handlePlayingError(queue, error = null) {
|
|
387
|
-
const song = queue.songs.shift();
|
|
388
|
-
if (error) {
|
|
389
|
-
error.name = "Playing";
|
|
390
|
-
error.message = `${error.message}\nID: ${song.id}\nName: ${song.name}`;
|
|
391
|
-
this.emitError(queue.textChannel, error);
|
|
392
|
-
}
|
|
393
|
-
if (queue.songs.length > 0) {
|
|
394
|
-
this.playSong(queue).then(e => {
|
|
395
|
-
if (!e) this.emit("playSong", queue, queue.songs[0]);
|
|
396
|
-
});
|
|
397
|
-
} else try { queue.stop() } catch { this.deleteQueue(queue) }
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Play a song from url without creating a {@link Queue}
|
|
402
|
-
* @param {Discord.VoiceChannel|Discord.StageChannel} voiceChannel The voice channel will be joined
|
|
403
|
-
* @param {string|Song|SearchResult} song YouTube url | {@link Song} | {@link SearchResult}
|
|
404
|
-
* @returns {Promise<Discord.StreamDispatcher>}
|
|
405
|
-
*/
|
|
406
|
-
async playWithoutQueue(voiceChannel, song) {
|
|
407
|
-
if (!["voice", "stage"].includes(voiceChannel?.type)) {
|
|
408
|
-
throw new TypeError("voiceChannel is not a Discord.VoiceChannel or a Discord.StageChannel.");
|
|
409
|
-
}
|
|
410
|
-
try {
|
|
411
|
-
if (ytpl.validateID(song)) throw new Error("Cannot play a playlist with this method.");
|
|
412
|
-
song = await this.resolveSong(voiceChannel.guild.me, song);
|
|
413
|
-
if (!song) throw new Error("Cannot resolve this song.");
|
|
414
|
-
if (song instanceof Playlist || Array.isArray(song)) throw new Error("Cannot play a playlist with this method.");
|
|
415
|
-
const connection = await voiceChannel.join();
|
|
416
|
-
if (song.source === "youtube") await this.checkYouTubeInfo(song);
|
|
417
|
-
const streamOptions = {
|
|
418
|
-
opusEncoded: true,
|
|
419
|
-
filter: song.isLive ? "audioandvideo" : "audioonly",
|
|
420
|
-
quality: "highestaudio",
|
|
421
|
-
};
|
|
422
|
-
Object.assign(streamOptions, this.ytdlOptions);
|
|
423
|
-
let stream;
|
|
424
|
-
if (song.source === "youtube") stream = ytdl(song.info, streamOptions);
|
|
425
|
-
else stream = ytdl.arbitraryStream(song.streamURL, streamOptions);
|
|
426
|
-
const dispatcher = connection.play(stream, {
|
|
427
|
-
highWaterMark: 1,
|
|
428
|
-
type: "opus",
|
|
429
|
-
bitrate: "auto",
|
|
430
|
-
}).on("finish", () => { try { stream.destroy() } catch { } });
|
|
431
|
-
return dispatcher;
|
|
432
|
-
} catch (e) {
|
|
433
|
-
e.name = "playWithoutQueue";
|
|
434
|
-
e.message = `${song?.url || song}\n${e.message}`;
|
|
435
|
-
throw e;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
module.exports = DisTubeHandler;
|
package/src/DisTubeOptions.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
const { mergeObject } = require("./util");
|
|
2
|
-
const defaultOptions = {
|
|
3
|
-
emitNewSongOnly: false,
|
|
4
|
-
leaveOnEmpty: true,
|
|
5
|
-
leaveOnFinish: false,
|
|
6
|
-
leaveOnStop: true,
|
|
7
|
-
savePreviousSongs: true,
|
|
8
|
-
youtubeDL: true,
|
|
9
|
-
updateYouTubeDL: true,
|
|
10
|
-
searchSongs: 0,
|
|
11
|
-
youtubeCookie: null,
|
|
12
|
-
youtubeIdentityToken: null,
|
|
13
|
-
customFilters: {},
|
|
14
|
-
ytdlOptions: {
|
|
15
|
-
highWaterMark: 1 << 24,
|
|
16
|
-
},
|
|
17
|
-
searchCooldown: 60,
|
|
18
|
-
emptyCooldown: 60,
|
|
19
|
-
plugins: [],
|
|
20
|
-
nsfw: false,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
module.exports = class DisTubeOptions {
|
|
24
|
-
constructor(options) {
|
|
25
|
-
const opt = mergeObject(defaultOptions, options);
|
|
26
|
-
for (const key in opt) {
|
|
27
|
-
this[key] = opt[key];
|
|
28
|
-
}
|
|
29
|
-
this._validateOptions();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
_validateOptions(options = this) {
|
|
33
|
-
if (typeof options.emitNewSongOnly !== "boolean") {
|
|
34
|
-
throw new TypeError("DisTubeOptions.emitNewSongOnly must be a boolean");
|
|
35
|
-
}
|
|
36
|
-
if (typeof options.leaveOnEmpty !== "boolean") {
|
|
37
|
-
throw new TypeError("DisTubeOptions.leaveOnEmpty must be a boolean");
|
|
38
|
-
}
|
|
39
|
-
if (typeof options.leaveOnFinish !== "boolean") {
|
|
40
|
-
throw new TypeError("DisTubeOptions.leaveOnFinish must be a boolean");
|
|
41
|
-
}
|
|
42
|
-
if (typeof options.leaveOnStop !== "boolean") {
|
|
43
|
-
throw new TypeError("DisTubeOptions.leaveOnStop must be a boolean");
|
|
44
|
-
}
|
|
45
|
-
if (typeof options.savePreviousSongs !== "boolean") {
|
|
46
|
-
throw new TypeError("DisTubeOptions.savePreviousSongs must be a boolean");
|
|
47
|
-
}
|
|
48
|
-
if (typeof options.youtubeDL !== "boolean") {
|
|
49
|
-
throw new TypeError("DisTubeOptions.youtubeDL must be a boolean");
|
|
50
|
-
}
|
|
51
|
-
if (typeof options.updateYouTubeDL !== "boolean") {
|
|
52
|
-
throw new TypeError("DisTubeOptions.updateYouTubeDL must be a boolean");
|
|
53
|
-
}
|
|
54
|
-
if (options.youtubeCookie !== null && typeof options.youtubeCookie !== "string") {
|
|
55
|
-
throw new TypeError("DisTubeOptions.youtubeCookie must be a string");
|
|
56
|
-
}
|
|
57
|
-
if (options.youtubeIdentityToken !== null && typeof options.youtubeIdentityToken !== "string") {
|
|
58
|
-
throw new TypeError("DisTubeOptions.youtubeIdentityToken must be a string");
|
|
59
|
-
}
|
|
60
|
-
if (typeof options.customFilters !== "object" || Array.isArray(options.customFilters)) {
|
|
61
|
-
throw new TypeError("DisTubeOptions.customFilters must be an object");
|
|
62
|
-
}
|
|
63
|
-
if (typeof options.ytdlOptions !== "object" || Array.isArray(options.ytdlOptions)) {
|
|
64
|
-
throw new TypeError("DisTubeOptions.customFilters must be an object");
|
|
65
|
-
}
|
|
66
|
-
if (typeof options.searchCooldown !== "number" || isNaN(options.searchCooldown)) {
|
|
67
|
-
throw new TypeError("DisTubeOptions.searchCooldown must be a number");
|
|
68
|
-
}
|
|
69
|
-
if (typeof options.emptyCooldown !== "number" || isNaN(options.emptyCooldown)) {
|
|
70
|
-
throw new TypeError("DisTubeOptions.emptyCooldown must be a number");
|
|
71
|
-
}
|
|
72
|
-
if (typeof options.searchSongs !== "number" || isNaN(options.emptyCooldown)) {
|
|
73
|
-
throw new TypeError("DisTubeOptions.searchSongs must be a number");
|
|
74
|
-
}
|
|
75
|
-
if (!Array.isArray(options.plugins)) {
|
|
76
|
-
throw new TypeError("DisTubeOptions.plugins must be an array of Plugin.");
|
|
77
|
-
}
|
|
78
|
-
if (typeof options.nsfw !== "boolean") {
|
|
79
|
-
throw new TypeError("DisTubeOptions.nsfw must be a boolean");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
};
|
package/src/Filter.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default DisTube audio filters.
|
|
3
|
-
* @typedef {Object} DefaultFilters
|
|
4
|
-
* @prop {string} 3d 3d
|
|
5
|
-
* @prop {string} bassboost bassboost
|
|
6
|
-
* @prop {string} echo echo
|
|
7
|
-
* @prop {string} karaoke karaoke
|
|
8
|
-
* @prop {string} nightcore nightcore
|
|
9
|
-
* @prop {string} vaporwave vaporwave
|
|
10
|
-
* @prop {string} flanger flanger
|
|
11
|
-
* @prop {string} gate gate
|
|
12
|
-
* @prop {string} haas haas
|
|
13
|
-
* @prop {string} reverse reverse
|
|
14
|
-
* @prop {string} surround surround
|
|
15
|
-
* @prop {string} mcompand mcompand
|
|
16
|
-
* @prop {string} phaser phaser
|
|
17
|
-
* @prop {string} tremolo tremolo
|
|
18
|
-
* @prop {string} earwax earwax
|
|
19
|
-
*/
|
|
20
|
-
module.exports = {
|
|
21
|
-
"3d": "apulsator=hz=0.125",
|
|
22
|
-
bassboost: "bass=g=10,dynaudnorm=f=150:g=15",
|
|
23
|
-
echo: "aecho=0.8:0.9:1000:0.3",
|
|
24
|
-
flanger: "flanger",
|
|
25
|
-
gate: "agate",
|
|
26
|
-
haas: "haas",
|
|
27
|
-
karaoke: "stereotools=mlev=0.1",
|
|
28
|
-
nightcore: "asetrate=48000*1.25,aresample=48000,bass=g=5",
|
|
29
|
-
reverse: "areverse",
|
|
30
|
-
vaporwave: "asetrate=48000*0.8,aresample=48000,atempo=1.1",
|
|
31
|
-
mcompand: "mcompand",
|
|
32
|
-
phaser: "aphaser",
|
|
33
|
-
tremolo: "tremolo",
|
|
34
|
-
surround: "surround",
|
|
35
|
-
earwax: "earwax",
|
|
36
|
-
};
|
package/src/Playlist.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
const { formatDuration } = require("./util"),
|
|
2
|
-
// eslint-disable-next-line no-unused-vars
|
|
3
|
-
Song = require("./Song"),
|
|
4
|
-
// eslint-disable-next-line no-unused-vars
|
|
5
|
-
Discord = require("discord.js");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/** Class representing a playlist. */
|
|
9
|
-
class Playlist {
|
|
10
|
-
/**
|
|
11
|
-
* Create a playlist
|
|
12
|
-
* @param {Array<Song>|Object} playlist Playlist
|
|
13
|
-
* @param {Discord.GuildMember} member Requested user
|
|
14
|
-
* @param {Object} properties Custom properties
|
|
15
|
-
*/
|
|
16
|
-
constructor(playlist, member, properties = {}) {
|
|
17
|
-
if (typeof properties !== "object") throw new TypeError("Custom properties must be an object");
|
|
18
|
-
/**
|
|
19
|
-
* The source of the playlist
|
|
20
|
-
* @type {string}
|
|
21
|
-
*/
|
|
22
|
-
this.source = playlist.source || properties.source || "youtube";
|
|
23
|
-
/**
|
|
24
|
-
* User requested.
|
|
25
|
-
* @type {Discord.GuildMember}
|
|
26
|
-
*/
|
|
27
|
-
this.member = member || playlist.member;
|
|
28
|
-
/**
|
|
29
|
-
* User requested.
|
|
30
|
-
* @type {Discord.User}
|
|
31
|
-
*/
|
|
32
|
-
this.user = this.member?.user;
|
|
33
|
-
/**
|
|
34
|
-
* Playlist songs.
|
|
35
|
-
* @type {Array<Song>}
|
|
36
|
-
*/
|
|
37
|
-
this.songs = Array.isArray(playlist) ? playlist : playlist.items || playlist.songs;
|
|
38
|
-
if (!Array.isArray(this.songs) || !this.songs.length) throw new Error("Playlist is empty!");
|
|
39
|
-
this.songs.map(s => s.constructor.name === "Song" && s._patchPlaylist(this, this.member));
|
|
40
|
-
/**
|
|
41
|
-
* Playlist name.
|
|
42
|
-
* @type {string}
|
|
43
|
-
*/
|
|
44
|
-
this.name = playlist.name || playlist.title || this.songs[0].playlist_title || `${this.songs[0].name} and ${this.songs.length - 1} more songs.`;
|
|
45
|
-
/**
|
|
46
|
-
* Playlist URL.
|
|
47
|
-
* @type {string}
|
|
48
|
-
*/
|
|
49
|
-
this.url = playlist.url;
|
|
50
|
-
/**
|
|
51
|
-
* Playlist thumbnail.
|
|
52
|
-
* @type {string}
|
|
53
|
-
*/
|
|
54
|
-
this.thumbnail = playlist.thumbnail || this.songs[0].thumbnail;
|
|
55
|
-
for (const [key, value] of Object.entries(properties)) this[key] = value;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Playlist duration in second.
|
|
60
|
-
* @type {number}
|
|
61
|
-
*/
|
|
62
|
-
get duration() {
|
|
63
|
-
return this.songs?.reduce((prev, next) => prev + (next.duration || 0), 0) || 0;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Formatted duration string `hh:mm:ss`.
|
|
68
|
-
* @type {string}
|
|
69
|
-
*/
|
|
70
|
-
get formattedDuration() {
|
|
71
|
-
return formatDuration(this.duration);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
module.exports = Playlist;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
const Plugin = require("./Plugin"),
|
|
3
|
-
Discord = require("discord.js");
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Custom Plugin
|
|
7
|
-
* @extends Plugin
|
|
8
|
-
*/
|
|
9
|
-
class CustomPlugin extends Plugin {
|
|
10
|
-
/** Create a custom plugin */
|
|
11
|
-
constructor() {
|
|
12
|
-
super("custom");
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Execute if the url is validated
|
|
16
|
-
* @param {Discord.VoiceChannel|Discord.StageChannel} voiceChannel The voice channel will be joined
|
|
17
|
-
* @param {string} url Validated url
|
|
18
|
-
* @param {Discord.GuildMember} member Requested user
|
|
19
|
-
* @param {Discord.TextChannel?} textChannel Default {@link Queue#textChannel}
|
|
20
|
-
* @param {boolean} skip Skip the playing song (if exists)
|
|
21
|
-
* @returns {Promise<void>}
|
|
22
|
-
*/
|
|
23
|
-
async play(voiceChannel, url, member, textChannel, skip) { }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
module.exports = CustomPlugin;
|