620 lines
21 KiB
Markdown
620 lines
21 KiB
Markdown
# Setup
|
|
- Before you use it, properly initialize the module (`@discordjs/voice` patch)
|
|
|
|
```js
|
|
new Client({
|
|
patchVoice: true, // Enable default
|
|
})
|
|
```
|
|
|
|
# Usage: Call DM / Group DM
|
|
|
|
```js
|
|
const dmChannel = client.channels.cache.get('id');
|
|
/* or
|
|
const dmChannel = User.dmChannel || await User.createDM();
|
|
*/
|
|
const connection = await dmChannel.call();
|
|
/* Return @discordjs/voice VoiceConnection */
|
|
```
|
|
|
|
# Play Music using `play-dl`
|
|
|
|
```js
|
|
const play = require('play-dl');
|
|
const {
|
|
createAudioPlayer,
|
|
createAudioResource,
|
|
NoSubscriberBehavior,
|
|
} = require('@discordjs/voice');
|
|
const channel = (await (message.member.user.dmChannel || message.member.user.createDM()));
|
|
const connection = channel.voiceConnection || await channel.call();
|
|
let stream;
|
|
if (!args[0]) {
|
|
return message.channel.send('Enter something to search for.');
|
|
} else if (args[0].startsWith('https://www.youtube.com/watch?v=')) {
|
|
stream = await play.stream(args.join(' '));
|
|
} else {
|
|
const yt_info = await play.search(args, {
|
|
limit: 1
|
|
});
|
|
stream = await play.stream(yt_info[0].url);
|
|
}
|
|
const resource = createAudioResource(stream.stream, {
|
|
inputType: stream.type,
|
|
inlineVolume: true,
|
|
});
|
|
resource.volume.setVolume(0.25);
|
|
const player = createAudioPlayer({
|
|
behaviors: {
|
|
noSubscriber: NoSubscriberBehavior.Play,
|
|
},
|
|
});
|
|
let i = setInterval(() => {
|
|
const m = channel.voiceUsers.get(message.author.id);
|
|
if (m) {
|
|
player.play(resource);
|
|
connection.subscribe(player);
|
|
clearInterval(i);
|
|
}
|
|
else console.log('waiting for voice connection');
|
|
}, 250);
|
|
```
|
|
|
|
# Play Music with module (support Play, Pause, Search, Skip, Previous, Volume, Loop)
|
|
|
|
```js
|
|
/* Copyright aiko-chan-ai @2022. All rights reserved. */
|
|
const DjsVoice = require('@discordjs/voice');
|
|
const Discord = require('discord.js-selfbot-v13');
|
|
const playDL = require('play-dl');
|
|
const EventEmitter = require('events');
|
|
const Event = {
|
|
READY: 'ready',
|
|
NO_SEARCH_RESULT: 'searchNoResult',
|
|
SEARCH_RESULT: 'searchResult',
|
|
PLAY_SONG: 'playSong',
|
|
ADD_SONG: 'addSong',
|
|
ADD_PLAYLIST: 'addPlaylist',
|
|
LEAVE_VC: 'disconnect',
|
|
FINISH: 'finish',
|
|
EMPTY: 'empty',
|
|
ERROR: 'error',
|
|
}
|
|
class Stack {
|
|
constructor() {
|
|
this.data = [];
|
|
}
|
|
|
|
push(item) {
|
|
return this.data.push(item);
|
|
}
|
|
|
|
pop() {
|
|
return this.data.pop();
|
|
}
|
|
|
|
peek() {
|
|
return this.data[this.length - 1];
|
|
}
|
|
|
|
get length() {
|
|
return this.data.length;
|
|
}
|
|
|
|
isEmpty() {
|
|
return this.length === 0;
|
|
}
|
|
|
|
reset () {
|
|
this.data = [];
|
|
return this;
|
|
}
|
|
}
|
|
|
|
class Queue {
|
|
constructor() {
|
|
this.data = [];
|
|
}
|
|
|
|
enqueue(item) {
|
|
return this.data.unshift(item);
|
|
}
|
|
|
|
dequeue() {
|
|
return this.data.pop();
|
|
}
|
|
|
|
peek() {
|
|
return this.data[this.length - 1];
|
|
}
|
|
|
|
get length() {
|
|
return this.data.length;
|
|
}
|
|
|
|
isEmpty() {
|
|
return this.data.length === 0;
|
|
}
|
|
|
|
reset() {
|
|
this.data = [];
|
|
return this;
|
|
}
|
|
}
|
|
class Player extends EventEmitter {
|
|
constructor(client, options = {}) {
|
|
super();
|
|
if (!client || !client instanceof Discord.Client) throw new Error('Invalid Discord Client (Selfbot)');
|
|
Object.defineProperty(this, 'client', { value: client });
|
|
this._playDl = playDL;
|
|
this._queue = new Queue();
|
|
this._previousSongs = new Stack();
|
|
this.song = null;
|
|
this.guild = undefined;
|
|
this.channel = undefined;
|
|
this._currentResourceAudio = undefined;
|
|
this._currentTime = 0;
|
|
this._playingTime = null;
|
|
this.isPlaying = false;
|
|
this.volume = 100;
|
|
this.loopMode = 0;
|
|
this.message = undefined;
|
|
this._timeoutEmpty = undefined;
|
|
this._player = DjsVoice.createAudioPlayer({
|
|
behaviors: {
|
|
noSubscriber: DjsVoice.NoSubscriberBehavior.Play,
|
|
},
|
|
});
|
|
this._playerEvent();
|
|
this._validateOptions(options);
|
|
this._discordEvent();
|
|
this._privateEvent();
|
|
}
|
|
get currentTime() {
|
|
return this._currentTime || Date.now() - this._playingTime;
|
|
}
|
|
get currentConnection() {
|
|
return DjsVoice.getVoiceConnection(this.guild?.id || null);
|
|
}
|
|
get queue() {
|
|
return this._queue.data;
|
|
}
|
|
get previousSongs() {
|
|
return this._previousSongs.data;
|
|
}
|
|
authorization() {
|
|
this._playDl.authorization();
|
|
}
|
|
_validateOptions(options) {
|
|
if (typeof options !== 'object') throw new Error(`Invalid options type (Required: object, got: ${typeof options})`);
|
|
this.options = {
|
|
directLink: true,
|
|
joinNewVoiceChannel: true,
|
|
waitingUserToPlayInDMs: true,
|
|
nsfw: false,
|
|
leaveOnEmpty: true,
|
|
leaveOnFinish: true,
|
|
leaveOnStop: true,
|
|
savePreviousSongs: true,
|
|
emptyCooldown: 10_000,
|
|
}
|
|
if (typeof options.directLink === 'boolean') {
|
|
this.options.directLink = options.directLink;
|
|
}
|
|
if (typeof options.joinNewVoiceChannel === 'boolean') {
|
|
this.options.joinNewVoiceChannel = options.joinNewVoiceChannel;
|
|
}
|
|
if (typeof options.waitingUserToPlayInDMs === 'boolean') {
|
|
this.options.waitingUserToPlayInDMs = options.waitingUserToPlayInDMs;
|
|
}
|
|
if (typeof options.nsfw === 'boolean') {
|
|
this.options.nsfw = options.nsfw;
|
|
}
|
|
if (typeof options.leaveOnEmpty === 'boolean') {
|
|
if (typeof options.emptyCooldown === 'number') {
|
|
this.options.leaveOnEmpty = options.leaveOnEmpty;
|
|
this.options.emptyCooldown = options.emptyCooldown;
|
|
} else {
|
|
this.options.leaveOnEmpty = false;
|
|
}
|
|
}
|
|
if (typeof options.leaveOnFinish === 'boolean') {
|
|
this.options.leaveOnFinish = options.leaveOnFinish;
|
|
}
|
|
if (typeof options.leaveOnStop === 'boolean') {
|
|
this.options.leaveOnStop = options.leaveOnStop;
|
|
}
|
|
if (typeof options.savePreviousSongs === 'boolean') {
|
|
this.options.savePreviousSongs = options.savePreviousSongs;
|
|
}
|
|
}
|
|
async play(options = {}) {
|
|
const {
|
|
message,
|
|
channel,
|
|
query,
|
|
} = options;
|
|
if (!(message instanceof Discord.Message)) throw new Error(`Invalid message type (Required: Message, got: ${typeof message})`);
|
|
if (channel &&
|
|
(
|
|
channel instanceof Discord.DMChannel ||
|
|
channel instanceof Discord.PartialGroupDMChannel ||
|
|
channel instanceof Discord.VoiceChannel ||
|
|
channel instanceof Discord.StageChannel
|
|
)
|
|
) {
|
|
let checkChangeVC = false;
|
|
if (!this.channel) this.channel = channel;
|
|
else {
|
|
if (this.options.joinNewVoiceChannel) {
|
|
if (this.channel.id !== channel.id) checkChangeVC = true;
|
|
this.channel = channel;
|
|
}
|
|
}
|
|
this.guild = channel.guild;
|
|
this.message = message;
|
|
if (typeof query !== 'string') throw new Error(`Invalid query type (Required: string, got: ${typeof query})`);
|
|
const result = await this.search(message, query);
|
|
if (result.length < 1) {
|
|
throw new Error('No search result with the given query: ' + query);
|
|
} else {
|
|
for (let i = 0; i < result.length; i++) {
|
|
this._queue.enqueue(result[i]);
|
|
}
|
|
if (!this.isPlaying) {
|
|
this._skip(checkChangeVC);
|
|
} else if (!result[0].playlist) {
|
|
this.emit(Event.ADD_SONG, result[0]);
|
|
}
|
|
}
|
|
} else {
|
|
throw new Error(`Invalid channel. Make sure the channel is a DMChannel | PartialGroupDMChannel | VoiceChannel | StageChannel.`);
|
|
}
|
|
}
|
|
async search(message, query, limit = 1) {
|
|
if (!(message instanceof Discord.Message)) throw new Error(`Invalid message type (Required: Message, got: ${typeof message})`);
|
|
if (typeof query !== 'string') throw new Error(`Invalid query type (Required: string, got: ${typeof query})`);
|
|
if (typeof limit !== 'number') throw new Error(`Invalid limit type (Required: number, got: ${typeof limit})`);
|
|
if (limit < 1) {
|
|
limit = 1;
|
|
process.emitWarning(`Invalid limit value (Required: 1 or more, got: ${limit})`);
|
|
};
|
|
if (limit > 10) {
|
|
limit = 10;
|
|
process.emitWarning(`Invalid limit value (Required: 10 or less, got: ${limit})`);
|
|
};
|
|
if (/^(https?\:\/\/)?(www\.youtube\.com|youtu\.be)\/.+$/.test(query)) {
|
|
const validateData = this._playDl.yt_validate(query);
|
|
if (validateData == 'video') {
|
|
const result = await this._playDl.video_info(query);
|
|
return [result.video_details];
|
|
} else if (validateData == 'playlist') {
|
|
const result = await this._playDl.playlist_info(query);
|
|
this.emit(Event.ADD_PLAYLIST, result);
|
|
const allVideo = await result.all_videos();
|
|
return allVideo.map(video => {
|
|
Object.defineProperty(video, 'playlist', { value: result });
|
|
return video;
|
|
});
|
|
} else {
|
|
return this.emit(Event.ERROR, new Error('Invalid YouTube URL: ' + query));
|
|
}
|
|
} else {
|
|
const result = await this._playDl.search(query, {
|
|
limit,
|
|
unblurNSFWThumbnails: this.options.nsfw,
|
|
});
|
|
if (result.length < 1) {
|
|
this.emit(Event.NO_SEARCH_RESULT, message, query, limit);
|
|
return [];
|
|
} else {
|
|
this.emit(Event.SEARCH_RESULT, message, result, query, limit);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
setLoopMode(mode) {
|
|
if ([0, 1, 2].includes(mode)) {
|
|
this._loopMode = mode;
|
|
} else {
|
|
throw new Error(`Invalid mode value (Required: 0 [No loop], 1 [Loop song], 2 [Loop queue], got: ${mode})`);
|
|
}
|
|
}
|
|
async createStream(url) {
|
|
const stream = await this._playDl.stream(url);
|
|
const resource = DjsVoice.createAudioResource(stream.stream, {
|
|
inputType: stream.type,
|
|
inlineVolume: true,
|
|
});
|
|
this._currentResourceAudio = resource;
|
|
}
|
|
_play() {
|
|
this._player.play(this._currentResourceAudio);
|
|
}
|
|
setVolume(volume) {
|
|
if (!this._currentResourceAudio) throw new Error('No current resource audio');
|
|
if (typeof volume !== 'number') throw new Error(`Invalid volume type (Required: number, got: ${typeof volume})`);
|
|
if (volume < 0) {
|
|
volume = 0;
|
|
process.emitWarning(`Invalid volume value (Required: 0 or more, got: ${volume})`);
|
|
} else if (volume > 100) {
|
|
volume = 100;
|
|
process.emitWarning(`Invalid volume value (Required: 100 or less, got: ${volume})`);
|
|
}
|
|
this._currentResourceAudio.volume.setVolume((volume / 100).toFixed(2));
|
|
this.volume = (volume / 100).toFixed(2) * 100;
|
|
return (volume / 100).toFixed(2) * 100;
|
|
}
|
|
pause() {
|
|
if (!this._currentResourceAudio) throw new Error('No current resource audio');
|
|
this._player.pause();
|
|
}
|
|
resume() {
|
|
if (!this._currentResourceAudio) throw new Error('No current resource audio');
|
|
this._player.unpause();
|
|
}
|
|
stop() {
|
|
if (!this._currentResourceAudio) throw new Error('No current resource audio');
|
|
this._stop(false, this.options.leaveOnStop);
|
|
}
|
|
_stop(finish = false, force = false) {
|
|
if (!this._currentResourceAudio) return;
|
|
this._queue.reset();
|
|
this._previousSongs.reset();
|
|
this._timeoutEmpty = undefined;
|
|
this._player.stop();
|
|
this._currentTime = 0;
|
|
this._currentResourceAudio = null;
|
|
this._playingTime = 0;
|
|
this.isPlaying = false;
|
|
this.channel = null;
|
|
this.guild = null;
|
|
this.song = null;
|
|
this.volume = 100;
|
|
if (force || !finish && this.options.leaveOnStop || finish && this.options.leaveOnFinish) this.currentConnection?.destroy();
|
|
this.message = null;
|
|
}
|
|
skip() {
|
|
this._skip();
|
|
}
|
|
async _skip(checkChangeVC = false) {
|
|
if (!this._queue.length) throw new Error('No song in the queue');
|
|
const currentSong = this.song;
|
|
if (this.loopMode == 0) {
|
|
if (this.options.savePreviousSongs) this._previousSongs.push(currentSong);
|
|
const nextSong = this._queue.dequeue();
|
|
this.song = nextSong;
|
|
} else if (this.loopMode == 1) {
|
|
this.song = currentSong;
|
|
} else if (this.loopMode == 2) {
|
|
this._queue.enqueue(currentSong);
|
|
const nextSong = this._queue.dequeue();
|
|
this.song = nextSong;
|
|
}
|
|
await this.createStream(this.song.url);
|
|
await this.joinVC(checkChangeVC);
|
|
this.emit(Event.PLAY_SONG, this.song);
|
|
if (!this.guild?.id) await _awaitDM();
|
|
this._play();
|
|
this._playingTime = Date.now();
|
|
}
|
|
async previous() {
|
|
if (!this._previousSongs.length) throw new Error('No previous songs');
|
|
const previousSong = this._previousSongs.pop();
|
|
this.song = previousSong;
|
|
await createStream(this.song.url);
|
|
await this.joinVC();
|
|
this._play();
|
|
this.emit(Event.PLAY_SONG, this.song, this.queue);
|
|
}
|
|
async joinVC(changeVC = false) {
|
|
if (this.currentConnection && !changeVC) {
|
|
this.currentConnection.subscribe(this._player);
|
|
} else if (this.channel instanceof Discord.VoiceChannel) {
|
|
const connection = DjsVoice.joinVoiceChannel({
|
|
channelId: this.channel.id,
|
|
guildId: this.guild.id,
|
|
adapterCreator: this.guild.voiceAdapterCreator,
|
|
});
|
|
await DjsVoice.entersState(
|
|
connection,
|
|
DjsVoice.VoiceConnectionStatus.Ready,
|
|
10_000,
|
|
);
|
|
connection.subscribe(this._player);
|
|
} else if (this.channel instanceof Discord.StageChannel) {
|
|
const connection = DjsVoice.joinVoiceChannel({
|
|
channelId: this.channel.id,
|
|
guildId: this.guild.id,
|
|
adapterCreator: this.guild.voiceAdapterCreator,
|
|
});
|
|
await DjsVoice.entersState(
|
|
connection,
|
|
DjsVoice.VoiceConnectionStatus.Ready,
|
|
10_000,
|
|
);
|
|
connection.subscribe(this._player);
|
|
await this.channel.guild.me.voice
|
|
.setSuppressed(false)
|
|
.catch(async () => {
|
|
return await this.channel.guild.me.voice
|
|
.setRequestToSpeak(true);
|
|
});
|
|
} else {
|
|
const connection = this.channel.voiceConnection || await this.channel.call();
|
|
connection.subscribe(this._player);
|
|
}
|
|
}
|
|
_discordEvent() {
|
|
// Event sus .-.
|
|
this.client.on('voiceStateUpdate', (oldState, newState) => {
|
|
if (!this._currentResourceAudio) return;
|
|
if (newState.guild?.id == this.guild?.id) {
|
|
if (oldState.channel?.id !== newState.channel?.id && oldState.channel?.id && newState.channel?.id && newState.id == this.client.user.id) {
|
|
// change vc
|
|
}
|
|
if (newState.id == this.client.user.id && oldState.channel?.members?.has(this.client.user.id) && !newState.channel?.members?.has(this.client.user.id)) {
|
|
this._stop();
|
|
this.emit(Event.LEAVE_VC, this.message);
|
|
}
|
|
if (newState.channel?.members?.has(this.client.user.id) && !newState.channel?.members?.filter(m => m.id != this.client.user.id && !m.bot).size) {
|
|
// empty
|
|
if (this.options.leaveOnEmpty && !this._timeoutEmpty) {
|
|
this._timeoutEmpty = setTimeout(() => {
|
|
this._stop(false, true);
|
|
this.emit(Event.EMPTY, this.message);
|
|
}, this.options.emptyCooldown);
|
|
}
|
|
}
|
|
if (newState.channel?.members?.has(this.client.user.id) && newState.channel?.members?.filter(m => m.id != this.client.user.id && !m.bot).size > 0) {
|
|
// not empty
|
|
if (this._timeoutEmpty) clearTimeout(this._timeoutEmpty);
|
|
this._timeoutEmpty = undefined;
|
|
}
|
|
} else if (!this.guild?.id && !newState.guild?.id) {
|
|
// DM channels
|
|
if (!newState.channel?.voiceUsers?.filter(m => m.id != this.client.user.id).size) {
|
|
// empty
|
|
if (this.options.leaveOnEmpty && !this._timeoutEmpty) {
|
|
this._timeoutEmpty = setTimeout(() => {
|
|
this._stop(false, true);
|
|
this.emit(Event.EMPTY, this.message);
|
|
}, this.options.emptyCooldown);
|
|
}
|
|
}
|
|
if (newState.channel?.voiceUsers?.filter(m => m.id != this.client.user.id).size > 0) {
|
|
// not empty
|
|
if (this._timeoutEmpty) clearTimeout(this._timeoutEmpty);
|
|
this._timeoutEmpty = undefined;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
_awaitDM () {
|
|
if (!this.options.waitingUserToPlayInDMs) return true;
|
|
return new Promise(resolve => {
|
|
let i = setInterval(() => {
|
|
const m = this.channel.voiceUsers.get(this.client.user.id);
|
|
if (m) {
|
|
clearInterval(i);
|
|
resolve(true);
|
|
}
|
|
}, 250);
|
|
})
|
|
}
|
|
_privateEvent() {
|
|
this.on('next_song', async () => {
|
|
await this._skip().catch(() => {
|
|
this.emit(Event.FINISH, this.message);
|
|
this._stop(true);
|
|
});
|
|
});
|
|
}
|
|
_playerEvent() {
|
|
const player = this._player;
|
|
player.on('stateChange', async (oldState, newState) => {
|
|
// idle -> playing
|
|
// idle -> buffering
|
|
// buffering -> playing
|
|
// playing -> idle
|
|
if (newState.status.toLowerCase() == 'idle') {
|
|
this.isPlaying = false;
|
|
} else if (newState.status.toLowerCase() == 'paused' || newState.status.toLowerCase() == 'autopaused') {
|
|
this.isPlaying = false;
|
|
} else {
|
|
this.isPlaying = true;
|
|
}
|
|
this._currentTime = newState.playbackDuration;
|
|
//
|
|
if (oldState.status == 'playing' && newState.status == 'idle') {
|
|
this.emit('next_song');
|
|
}
|
|
});
|
|
player.on('error', (e) => {
|
|
this.emit(Event.ERROR, e);
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = Player;
|
|
|
|
/* Example
|
|
const player = new Player(client, options);
|
|
|
|
player
|
|
.on('playSong', song => {
|
|
player.message.channel.send(`Now playing: ${song.title}`);
|
|
})
|
|
.on('addSong', song => {
|
|
player.message.channel.send(`Added: ${song.title}`);
|
|
})
|
|
.on('addPlaylist', playlist => {
|
|
player.message.channel.send(`Added Playlist: ${playlist.title}`);
|
|
})
|
|
.on('disconnect', (message) => {
|
|
message.channel.send('Disconnected from voice channel.');
|
|
})
|
|
.on('finish', (message) => {
|
|
message.channel.send('Finished playing.');
|
|
})
|
|
.on('empty', (message) => {
|
|
message.channel.send('The queue is empty.');
|
|
})
|
|
.on('error', error => {
|
|
console.log('Music error', error);
|
|
})
|
|
|
|
client.player = player;
|
|
|
|
// Method
|
|
|
|
client.player.play({
|
|
message,
|
|
channel: message.member.voice.channel, // VoiceChannel | DMChannel | StageChannel | GroupDMChannel
|
|
query: string,
|
|
});
|
|
|
|
client.player.skip();
|
|
|
|
client.player.previous();
|
|
|
|
client.player.pause();
|
|
|
|
client.player.resume();
|
|
|
|
client.player.setVolume(50); // 50%
|
|
|
|
client.player.setLoopMode(1); // 0: none, 1: song, 2: queue;
|
|
|
|
client.player.stop();
|
|
|
|
// Options
|
|
|
|
options = {
|
|
directLink: true, // Whether or not play direct link of the song (not support)
|
|
joinNewVoiceChannel: true, // Whether or not joining the new voice channel when using #play method
|
|
waitingUserToPlayInDMs: true, // Waiting User join Call to play in DM channels
|
|
nsfw: false, // Whether or not play NSFW
|
|
leaveOnEmpty: true, // Whether or not leave voice channel when empty (not working)
|
|
leaveOnFinish: true, // Whether or not leave voice channel when finish
|
|
leaveOnStop: true, // Whether or not leave voice channel when stop
|
|
savePreviousSongs: true, // Whether or not save previous songs
|
|
emptyCooldown: 10_000, // Cooldown when empty voice channel
|
|
}
|
|
|
|
// Properties
|
|
|
|
song = Song;
|
|
guild = Discord.Guild;
|
|
channel = Channel;
|
|
client = Discord.Client;
|
|
isPlaying = false;
|
|
volume = 100;
|
|
currentTime = Unix timestamp miliseconds;
|
|
currentConnection = VoiceConnection;
|
|
queue: Song[];
|
|
previousSongs: Song[];
|
|
|
|
*/
|
|
``` |