Files
Yumi/src/providers/DiscordMusicPlayer.ts
T

402 lines
14 KiB
TypeScript
Raw Normal View History

2022-02-28 11:50:47 +07:00
import playdl, { YouTubeVideo } from "play-dl";
import { VoiceChannel, Snowflake, TextChannel, StageChannel, Guild } from "discord.js";
import { AudioPlayer, VoiceConnection, createAudioPlayer, joinVoiceChannel, createAudioResource, VoiceConnectionStatus, AudioPlayerStatus, NoSubscriberBehavior, VoiceConnectionState, AudioPlayerError } from "@discordjs/voice";
2022-01-30 20:09:54 +07:00
import { EventEmitter } from "stream";
import DiscordProvider from "./Discord";
2022-01-30 21:35:19 +07:00
import Environment from "./Environment";
2022-01-30 20:09:54 +07:00
export type ValidTracks = YouTubeVideo;
2022-01-30 21:35:19 +07:00
if (Environment.get().YOUTUBE_COOKIE_BASE64) {
playdl.setToken({
youtube: {
cookie: Buffer.from(Environment.get().YOUTUBE_COOKIE_BASE64, 'base64').toString()
}
})
}
2022-01-30 20:09:54 +07:00
export class Queue {
public track: (ValidTracks)[] = [];
}
export class YouTubeLink {
public videoId: string;
public list?: string;
constructor(videoId: string, list: string) {
this.videoId = videoId;
this.list = list;
}
}
export class PlayerPlayingEvent {
2022-01-30 21:35:56 +07:00
public instance: DiscordMusicPlayerInstance;
2022-01-30 20:09:54 +07:00
constructor(instance: DiscordMusicPlayerInstance) {
this.instance = instance;
}
}
2022-02-27 17:15:20 +07:00
export class PlayerErrorEvent {
public instance: DiscordMusicPlayerInstance;
public error: Error;
constructor(instance: DiscordMusicPlayerInstance, error: Error) {
this.instance = instance;
this.error = error;
}
}
export class VoiceDisconnectedEvent {
public instance: DiscordMusicPlayerInstance;
constructor(instance: DiscordMusicPlayerInstance) {
this.instance = instance;
}
}
2022-03-03 23:57:30 +07:00
export enum DiscordMusicPlayerLoopMode {
None = "none",
Current = "current"
}
2022-01-30 20:09:54 +07:00
export class DiscordMusicPlayerInstance {
public queue: Queue;
public player: AudioPlayer;
public textChannel?: TextChannel;
public voiceChannel: (VoiceChannel | StageChannel);
public voiceConnection?: VoiceConnection;
2022-03-03 23:57:30 +07:00
public loopMode: DiscordMusicPlayerLoopMode = DiscordMusicPlayerLoopMode.None;
2022-01-30 20:09:54 +07:00
public readonly events: EventEmitter;
constructor({ voiceChannel }: { voiceChannel: (VoiceChannel | StageChannel) }) {
this.queue = new Queue();
this.player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Pause,
maxMissedFrames: 1000
}
});
this.voiceChannel = voiceChannel;
this.events = new EventEmitter();
this.player.on(AudioPlayerStatus.Idle, async (oldStage, newStage) => {
//The player stopped
if (newStage.status === AudioPlayerStatus.Idle && oldStage.status !== AudioPlayerStatus.Idle) {
2022-03-03 23:57:30 +07:00
// Loop mode is set to current song
if (this.loopMode === DiscordMusicPlayerLoopMode.Current) {
this.playTrack(this.queue.track[0]);
return;
}
// There are more songs in the queue, remove finished song and play the next one
2022-01-30 21:35:56 +07:00
if (this.queue.track.length !== 0) {
2022-01-30 20:09:54 +07:00
this.queue.track.shift();
2022-01-30 21:35:56 +07:00
if (this.queue.track.length > 0) {
2022-01-30 20:09:54 +07:00
this.playTrack(this.queue.track[0]);
}
}
2022-03-03 23:57:30 +07:00
2022-01-30 20:09:54 +07:00
}
});
this.player.on(AudioPlayerStatus.Playing, (oldState: any, newState: any) => {
this.events.emit('playing', new PlayerPlayingEvent(this));
});
2022-02-27 17:15:20 +07:00
this.player.on('error', (error: Error) => {
this.events.emit('error', new PlayerErrorEvent(this, error));
});
2022-01-30 20:09:54 +07:00
}
public isReady() {
if (!this.voiceConnection) return false;
return this.voiceConnection.state.status === VoiceConnectionStatus.Ready;
}
public isConnected() {
if (!this.voiceConnection) return false;
2022-01-30 21:35:56 +07:00
if (this.voiceConnection.state.status === VoiceConnectionStatus.Destroyed) return false;
if (this.voiceConnection.state.status === VoiceConnectionStatus.Disconnected) return false;
2022-01-30 20:09:54 +07:00
return true;
}
public joinVoiceChannel(voiceChannel: (VoiceChannel | StageChannel), textChannel?: TextChannel) {
const permissions = voiceChannel.permissionsFor(DiscordProvider.client.user!);
if (!permissions)
throw new Error("No permissions");
if (!voiceChannel.joinable || !permissions.has("CONNECT"))
throw new Error("No permissions");
2022-01-30 21:35:56 +07:00
if (textChannel)
2022-01-30 20:09:54 +07:00
this.textChannel = textChannel;
this.voiceConnection = joinVoiceChannel({
channelId: this.voiceChannel.id,
guildId: this.voiceChannel.guild.id,
adapterCreator: this.voiceChannel.guild.voiceAdapterCreator
});
this.voiceConnection.on(VoiceConnectionStatus.Ready, (oldState: VoiceConnectionState, newState: VoiceConnectionState) => {
let currentVC = DiscordProvider.client.guilds.cache.get(voiceChannel.guild.id)?.me?.voice.channel;
2022-01-30 21:35:56 +07:00
if (currentVC && currentVC.id !== this.voiceChannel.id) {
2022-01-30 20:09:54 +07:00
this.voiceChannel = currentVC;
}
});
2022-02-05 00:32:02 +07:00
this.voiceConnection.on(VoiceConnectionStatus.Disconnected, (oldState: VoiceConnectionState, newState: VoiceConnectionState) => {
2022-02-27 17:15:20 +07:00
setTimeout(() => {
if (!DiscordProvider.client.guilds.cache.get(voiceChannel.guildId!)!.me!.voice.channelId) {
this.events.emit('disconnect', new VoiceDisconnectedEvent(this));
}
}, 1000);
2022-02-05 00:32:02 +07:00
});
2022-01-30 20:09:54 +07:00
}
public async leaveVoiceChannel() {
2022-01-30 21:35:56 +07:00
if (this.player)
this.player.pause();
2022-01-30 20:09:54 +07:00
2022-01-30 21:35:56 +07:00
if (DiscordProvider.client.guilds.cache.get(this.voiceChannel.guild.id)?.me?.voice) {
2022-01-30 20:09:54 +07:00
await DiscordProvider.client.guilds.cache.get(this.voiceChannel.guild.id)?.me?.voice.disconnect();
}
2022-01-30 21:35:56 +07:00
2022-01-30 20:09:54 +07:00
}
public addTrackToQueue(track: (ValidTracks)) {
2022-01-30 21:35:56 +07:00
if (this.queue.track.length === 0) {
2022-01-30 20:09:54 +07:00
this.queue.track.push(track);
this.playTrack(this.queue.track[0]);
return;
}
this.queue.track.push(track);
}
public async playTrack(track: ValidTracks) {
2022-01-30 21:35:56 +07:00
if (!this.voiceConnection) throw new Error("No voice connection");
try {
const stream = await playdl.stream(track.url);
const resource = createAudioResource(stream.stream, {
inputType: stream.type
});
this.player.play(resource)
this.voiceConnection.subscribe(this.player)
} catch (error: any) {
this.events.emit('error', new PlayerErrorEvent(this, error));
this.skipTrack();
}
2022-01-30 20:09:54 +07:00
}
public async skipTrack() {
2022-01-30 21:35:56 +07:00
if (!this.voiceConnection) throw new Error("No voice connection");
2022-01-30 20:09:54 +07:00
2022-01-30 21:35:56 +07:00
if (this.queue.track.length > 1) {
2022-01-30 20:09:54 +07:00
this.queue.track.shift();
this.playTrack(this.queue.track[0]);
}
2022-02-04 22:42:59 +07:00
else {
this.queue.track.shift();
this.player.stop();
}
2022-01-30 20:09:54 +07:00
}
2022-03-03 23:57:30 +07:00
public setLoopMode(mode: DiscordMusicPlayerLoopMode) {
this.loopMode = mode;
}
public getLoopMode() {
return this.loopMode;
}
2022-01-30 20:09:54 +07:00
public async destroy() {
await this.leaveVoiceChannel();
2022-01-30 21:35:56 +07:00
if (this.voiceConnection) {
2022-01-30 20:09:54 +07:00
this.voiceConnection.removeAllListeners();
2022-01-30 21:35:56 +07:00
if (this.voiceConnection.state.status !== 'destroyed')
2022-01-30 20:09:54 +07:00
this.voiceConnection.destroy();
}
2022-01-30 21:35:56 +07:00
if (this.player) {
2022-01-30 20:09:54 +07:00
this.player.removeAllListeners();
this.player.stop(true);
}
this.queue.track = [];
this.textChannel = undefined;
this.voiceConnection = undefined;
}
2022-02-27 17:15:20 +07:00
public async _fake_error_on_player() {
2022-03-03 22:18:34 +07:00
const stream = 'https://fakestream:42069/fake/stream/fake/audio/fake.mp3';
const resource = createAudioResource(stream);
this.player.play(resource);
this.player.emit('error', new AudioPlayerError(new Error("Music player was manually crashed"), null!));
2022-02-27 17:15:20 +07:00
}
2022-01-30 20:09:54 +07:00
}
class DiscordMusicPlayer {
public GuildQueue = new Map();
public getGuildInstance(guildId: Snowflake): (DiscordMusicPlayerInstance | null) {
2022-01-30 21:35:56 +07:00
if (!this.isGuildInstanceExists(guildId)) return null;
2022-01-30 20:09:54 +07:00
return this.GuildQueue.get(guildId);
}
public isGuildInstanceExists(guildId: Snowflake) {
2022-01-30 21:35:56 +07:00
return this.GuildQueue.has(guildId);
2022-01-30 20:09:54 +07:00
}
public createGuildInstance(guildId: Snowflake, voiceChannel: (VoiceChannel | StageChannel)) {
this.GuildQueue.set(guildId, new DiscordMusicPlayerInstance({ voiceChannel }));
}
public async destoryGuildInstance(guild: (Guild | Snowflake)) {
let guildId: Snowflake = guild instanceof Guild ? guild.id : guild;
2022-01-30 21:35:56 +07:00
if (this.isGuildInstanceExists(guildId)) {
2022-01-30 20:09:54 +07:00
await this.GuildQueue.get(guildId).destroy();
this.GuildQueue.delete(guildId);
}
}
public async searchYouTubeByQuery(query: string) {
const searched: YouTubeVideo[] = await playdl.search(query, { source: { youtube: "video" } });
if (searched.length == 0) return null;
return searched;
}
public async searchYouTubeByYouTubeLink(youtubeLink: YouTubeLink) {
// Search the url
const searched: YouTubeVideo[] = await playdl.search("https://www.youtube.com/watch?v=" + youtubeLink.videoId, { source: { youtube: "video" } });
for (let video of searched) {
if (video.id === youtubeLink.videoId)
return video;
}
// Serch the video Id
const searched2: YouTubeVideo[] = await playdl.search(youtubeLink.videoId, { source: { youtube: "video" } });
for (let video of searched2) {
if (video.id === youtubeLink.videoId)
return video;
}
// Last resort, search the title
const videoInfo = await playdl.video_basic_info("https://www.youtube.com/watch?v=" + youtubeLink.videoId);
2022-01-30 21:35:56 +07:00
if (videoInfo?.video_details?.title) {
2022-01-30 20:09:54 +07:00
const searched: YouTubeVideo[] = await playdl.search(videoInfo.video_details.title, { source: { youtube: "video" } });
for (let video of searched) {
if (video.id === youtubeLink.videoId)
return video;
}
}
2022-01-30 21:35:56 +07:00
let yt_info = await playdl.video_info("https://www.youtube.com/watch?v=" + youtubeLink.videoId);
if(yt_info) {
return new YouTubeVideo({
id: yt_info.video_details.id,
url: yt_info.video_details.url,
type: yt_info.video_details.type,
title: yt_info.video_details.title,
description: yt_info.video_details.description,
durationRaw: yt_info.video_details.durationRaw,
durationInSec: yt_info.video_details.durationInSec,
uploadedAt: yt_info.video_details.uploadedAt,
upcoming: yt_info.video_details.upcoming,
views: yt_info.video_details.views,
thumbnails: yt_info.video_details.thumbnails,
channel: yt_info.video_details.channel,
likes: yt_info.video_details.likes,
live: yt_info.video_details.live,
private: yt_info.video_details.private,
tags: yt_info.video_details.tags,
discretionAdvised: yt_info.video_details.discretionAdvised,
music: yt_info.video_details.music
});
}
2022-01-30 20:09:54 +07:00
return null;
}
2022-02-27 21:05:45 +07:00
public async getYouTubeSongsInPlayList(youtubeLink: string) {
2022-02-27 21:40:59 +07:00
return await playdl.playlist_info(youtubeLink, {
incomplete: true
});
2022-02-27 21:05:45 +07:00
}
2022-01-30 20:09:54 +07:00
public isYouTubeLink(link: string): boolean {
try {
this.parseYouTubeLink(link);
return true;
} catch (err) {
return false;
}
}
public parseYouTubeLink(query: string): YouTubeLink {
if (query.startsWith('https://www.youtube.com/watch?v=') || query.startsWith('http://www.youtube.com/watch?v=')) {
let data = this.parseURLQuery(query);
if (!data.v)
throw new Error('YouTube link is invalid');
return {
videoId: data.v,
2022-02-27 21:05:45 +07:00
list: (data.list ? (data.list !== "RDMM" ? data.list : undefined) : undefined)
2022-01-30 20:09:54 +07:00
}
}
else if (query.startsWith('https://youtu.be/') || query.startsWith('http://youtu.be/')) {
//Get youtube video id after the url
let videoId = query.split('/')[3];
if (!videoId) throw new Error('YouTube link is invalid');
if (query.split('/')[4]) throw new Error('YouTube link is invalid');
return {
videoId: videoId
}
}
2022-02-27 21:40:59 +07:00
else if (query.startsWith('https://www.youtube.com/playlist?list=') || query.startsWith('http://www.youtube.com/playlist?list=')) {
let listId = query.split('?list=')[1];
return {
videoId: "",
list: listId
}
}
2022-01-30 20:09:54 +07:00
else {
throw new Error('YouTube link is invalid');
}
}
private parseURLQuery(query: string) {
let queryObject: any = {};
if (query.indexOf('?') >= 0) {
let queryString = query.split('?')[1];
let queryArray = queryString.split('&');
for (let i = 0; i < queryArray.length; i++) {
let queryPair = queryArray[i].split('=');
queryObject[queryPair[0]] = queryPair[1];
}
}
return queryObject;
}
}
const DiscordMusicPlayer_Instance = new DiscordMusicPlayer();
export default DiscordMusicPlayer_Instance;