docs(VoiceCall): Make lib play music
3 hours :v
This commit is contained in:
parent
a4061d4927
commit
bfad5d4187
@ -60,3 +60,558 @@ let i = setInterval(() => {
|
||||
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._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;
|
||||
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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
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.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.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._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 => {
|
||||
console.log(`Now playing: ${song.title}`);
|
||||
})
|
||||
.on('addSong', song => {
|
||||
console.log(`Added: ${song.title}`);
|
||||
})
|
||||
.on('addPlaylist', playlist => {
|
||||
console.log(`Added Playlist: ${playlist.title}`);
|
||||
})
|
||||
.on('disconnect', () => {
|
||||
console.log('Disconnected from voice channel.');
|
||||
})
|
||||
.on('finish', () => {
|
||||
console.log('finish.');
|
||||
})
|
||||
.on('empty', () => {
|
||||
console.log('empty voice channel.');
|
||||
})
|
||||
.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[];
|
||||
|
||||
*/
|
||||
```
|
Loading…
Reference in New Issue
Block a user