docs(VoiceCall): Make lib play music

3 hours :v
This commit is contained in:
March 7th 2022-08-16 18:33:40 +07:00
parent a4061d4927
commit bfad5d4187

View File

@ -60,3 +60,558 @@ let i = setInterval(() => {
else console.log('waiting for voice connection'); else console.log('waiting for voice connection');
}, 250); }, 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[];
*/
```