2022-06-29 15:27:42 +07:00
|
|
|
import {
|
|
|
|
|
VoiceChannel,
|
|
|
|
|
Snowflake,
|
|
|
|
|
StageChannel,
|
|
|
|
|
Guild,
|
|
|
|
|
PermissionsBitField,
|
2022-06-29 18:59:06 +07:00
|
|
|
BaseGuildVoiceChannel,
|
|
|
|
|
BaseGuildTextChannel
|
2022-06-29 15:27:42 +07:00
|
|
|
} from 'discord.js';
|
2022-06-06 14:59:53 +07:00
|
|
|
import {
|
|
|
|
|
AudioPlayer,
|
|
|
|
|
VoiceConnection,
|
|
|
|
|
createAudioPlayer,
|
|
|
|
|
joinVoiceChannel,
|
|
|
|
|
createAudioResource,
|
|
|
|
|
VoiceConnectionStatus,
|
|
|
|
|
AudioPlayerStatus,
|
|
|
|
|
NoSubscriberBehavior,
|
|
|
|
|
VoiceConnectionState,
|
|
|
|
|
AudioPlayerError,
|
|
|
|
|
DiscordGatewayAdapterCreator
|
|
|
|
|
} from '@discordjs/voice';
|
2022-07-01 22:04:37 +07:00
|
|
|
import playdl, { Spotify, SpotifyPlaylist, SpotifyTrack, YouTubeVideo } from 'play-dl';
|
2022-06-06 14:59:53 +07:00
|
|
|
import { EventEmitter } from 'stream';
|
2022-01-30 20:09:54 +07:00
|
|
|
|
2022-06-06 14:59:53 +07:00
|
|
|
import DiscordProvider from './Discord';
|
|
|
|
|
import Environment from './Environment';
|
2022-07-15 14:57:40 +07:00
|
|
|
import Logger from '../libs/Logger';
|
2022-01-30 20:09:54 +07:00
|
|
|
|
2022-07-01 22:04:37 +07:00
|
|
|
export type ValidTracks = YouTubeVideo | SpotifyTrack;
|
|
|
|
|
declare class YouTubeThumbnail {
|
|
|
|
|
url: string;
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
constructor(data: any);
|
|
|
|
|
toJSON(): {
|
|
|
|
|
url: string;
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SpotifyThumbnail {
|
|
|
|
|
height: number;
|
|
|
|
|
width: number;
|
|
|
|
|
url: string;
|
|
|
|
|
}
|
2022-01-30 20:09:54 +07:00
|
|
|
|
2022-07-15 14:57:40 +07:00
|
|
|
// TODO: Make proper check later
|
|
|
|
|
if (Environment.get().YOUTUBE_COOKIE_BASE64 && Environment.get().SPOTIFY_CLIENT_ID) {
|
2022-01-30 21:35:19 +07:00
|
|
|
playdl.setToken({
|
|
|
|
|
youtube: {
|
|
|
|
|
cookie: Buffer.from(Environment.get().YOUTUBE_COOKIE_BASE64, 'base64').toString()
|
2022-07-01 22:04:37 +07:00
|
|
|
},
|
|
|
|
|
spotify: {
|
|
|
|
|
client_id: Environment.get().SPOTIFY_CLIENT_ID,
|
|
|
|
|
client_secret: Environment.get().SPOTIFY_CLIENT_SECRET,
|
|
|
|
|
refresh_token: Environment.get().SPOTIFY_REFRESH_TOKEN,
|
|
|
|
|
market: Environment.get().SPOTIFY_CLIENT_MARKET,
|
2022-01-30 21:35:19 +07:00
|
|
|
}
|
2022-06-06 14:59:53 +07:00
|
|
|
});
|
2022-01-30 21:35:19 +07:00
|
|
|
}
|
2022-07-01 22:04:37 +07:00
|
|
|
|
|
|
|
|
export class TrackUtils {
|
|
|
|
|
public static getTitle(track: ValidTracks) {
|
|
|
|
|
if (track instanceof YouTubeVideo) {
|
|
|
|
|
return track.title;
|
|
|
|
|
} else if (track instanceof SpotifyTrack) {
|
|
|
|
|
let artistNames = "";
|
|
|
|
|
for(let artist of track.artists)
|
|
|
|
|
artistNames += artist.name + " ";
|
|
|
|
|
|
|
|
|
|
return `${artistNames} - ${track.name}`;;
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('Invalid track type');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
public static async getThumbnails(track: ValidTracks) {
|
|
|
|
|
if (track instanceof YouTubeVideo) {
|
|
|
|
|
return track.thumbnails;
|
|
|
|
|
} else if (track instanceof SpotifyTrack) {
|
|
|
|
|
if (track.thumbnail)
|
|
|
|
|
return [track.thumbnail];
|
|
|
|
|
else {
|
|
|
|
|
// Try to find the thumbnail again
|
|
|
|
|
const result = await DiscordMusicPlayer_Instance.searchSpotifyBySpotifyLink(track);
|
|
|
|
|
if(!result) return null;
|
|
|
|
|
if(result.thumbnail)
|
|
|
|
|
return [result.thumbnail];
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('Invalid track type');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
public static getHighestResolutionThumbnail(thumbnails: (YouTubeThumbnail[] | SpotifyThumbnail[] | null)) {
|
|
|
|
|
if(!thumbnails) return null;
|
|
|
|
|
|
|
|
|
|
return (thumbnails as any[]).reduce((prev: any, current: any) =>
|
|
|
|
|
prev.height * prev.width > current.height * current.width ? prev : current
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-30 20:09:54 +07:00
|
|
|
export class Queue {
|
2022-06-06 14:59:53 +07:00
|
|
|
public track: ValidTracks[] = [];
|
2022-01-30 20:09:54 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class YouTubeLink {
|
|
|
|
|
public videoId: string;
|
|
|
|
|
public list?: string;
|
|
|
|
|
|
|
|
|
|
constructor(videoId: string, list: string) {
|
|
|
|
|
this.videoId = videoId;
|
|
|
|
|
this.list = list;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-01 22:04:37 +07:00
|
|
|
export class SpotifyLink {
|
|
|
|
|
public id: string;
|
|
|
|
|
public type: 'track' | 'playlist' | 'album';
|
|
|
|
|
public url: string;
|
|
|
|
|
|
|
|
|
|
constructor(id: string, type: ('track' | 'playlist' | 'album'), url: string) {
|
|
|
|
|
this.id = id;
|
|
|
|
|
this.type = type;
|
|
|
|
|
this.url = url;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-30 20:09:54 +07:00
|
|
|
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 {
|
2022-06-06 14:59:53 +07:00
|
|
|
None = 'none',
|
|
|
|
|
Current = 'current'
|
2022-03-03 23:57:30 +07:00
|
|
|
}
|
2022-01-30 20:09:54 +07:00
|
|
|
export class DiscordMusicPlayerInstance {
|
|
|
|
|
public queue: Queue;
|
|
|
|
|
public player: AudioPlayer;
|
2022-06-29 18:59:06 +07:00
|
|
|
public textChannel?: BaseGuildTextChannel | BaseGuildVoiceChannel;
|
2022-06-06 14:59:53 +07:00
|
|
|
public voiceChannel: VoiceChannel | StageChannel;
|
2022-01-30 20:09:54 +07:00
|
|
|
public voiceConnection?: VoiceConnection;
|
2022-05-01 16:56:37 +07:00
|
|
|
public previousTrack?: ValidTracks;
|
2022-01-30 20:09:54 +07:00
|
|
|
|
2022-06-06 02:18:41 +07:00
|
|
|
public paused: boolean = false;
|
2022-03-03 23:57:30 +07:00
|
|
|
public loopMode: DiscordMusicPlayerLoopMode = DiscordMusicPlayerLoopMode.None;
|
|
|
|
|
|
2022-07-01 22:04:37 +07:00
|
|
|
public actualPlaybackURL?: string;
|
|
|
|
|
|
2022-01-30 20:09:54 +07:00
|
|
|
public readonly events: EventEmitter;
|
|
|
|
|
|
2022-06-06 14:59:53 +07:00
|
|
|
constructor({ voiceChannel }: { voiceChannel: VoiceChannel | StageChannel }) {
|
2022-01-30 20:09:54 +07:00
|
|
|
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
|
2022-06-29 15:27:42 +07:00
|
|
|
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) {
|
2022-05-01 16:56:37 +07:00
|
|
|
if (this.queue.track.length !== 0) {
|
|
|
|
|
this.previousTrack = this.queue.track[0];
|
|
|
|
|
this.playTrack(this.queue.track[0]);
|
|
|
|
|
}
|
2022-03-03 23:57:30 +07:00
|
|
|
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-05-01 16:56:37 +07:00
|
|
|
let previousTrack = this.queue.track.shift();
|
2022-06-06 14:59:53 +07:00
|
|
|
if (previousTrack) this.previousTrack = previousTrack;
|
2022-05-01 16:56:37 +07:00
|
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-29 18:59:06 +07:00
|
|
|
public joinVoiceChannel(voiceChannel: VoiceChannel | StageChannel, textChannel?: BaseGuildTextChannel | BaseGuildVoiceChannel) {
|
2022-01-30 20:09:54 +07:00
|
|
|
const permissions = voiceChannel.permissionsFor(DiscordProvider.client.user!);
|
|
|
|
|
|
2022-06-29 15:27:42 +07:00
|
|
|
if (!permissions || !voiceChannel.joinable || !permissions.has(PermissionsBitField.Flags.Connect))
|
2022-06-06 14:59:53 +07:00
|
|
|
throw new Error('No permissions');
|
2022-01-30 20:09:54 +07:00
|
|
|
|
2022-06-06 14:59:53 +07:00
|
|
|
if (textChannel) this.textChannel = textChannel;
|
2022-01-30 20:09:54 +07:00
|
|
|
|
|
|
|
|
this.voiceConnection = joinVoiceChannel({
|
|
|
|
|
channelId: this.voiceChannel.id,
|
|
|
|
|
guildId: this.voiceChannel.guild.id,
|
2022-06-29 15:27:42 +07:00
|
|
|
adapterCreator: this.voiceChannel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
|
2022-01-30 20:09:54 +07:00
|
|
|
});
|
|
|
|
|
|
2022-06-06 14:59:53 +07:00
|
|
|
this.voiceConnection.on(
|
|
|
|
|
VoiceConnectionStatus.Ready,
|
2022-06-29 15:27:42 +07:00
|
|
|
async (oldState: VoiceConnectionState, newState: VoiceConnectionState) => {
|
|
|
|
|
let guild = DiscordProvider.client.guilds.cache.get(voiceChannel.guild.id);
|
|
|
|
|
if (guild) {
|
|
|
|
|
let currentVC = guild?.members.me?.voice.channel;
|
|
|
|
|
if (currentVC && currentVC.id !== this.voiceChannel.id) {
|
|
|
|
|
this.voiceChannel = currentVC;
|
|
|
|
|
}
|
2022-02-27 17:15:20 +07:00
|
|
|
}
|
2022-06-06 14:59:53 +07:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.voiceConnection.on(
|
|
|
|
|
VoiceConnectionStatus.Disconnected,
|
|
|
|
|
(oldState: VoiceConnectionState, newState: VoiceConnectionState) => {
|
2022-06-29 15:27:42 +07:00
|
|
|
setTimeout(async () => {
|
|
|
|
|
let guild = DiscordProvider.client.guilds.cache.get(voiceChannel.guildId);
|
|
|
|
|
if(guild) {
|
|
|
|
|
if (!guild?.members.me?.voice.channelId) {
|
|
|
|
|
this.events.emit('disconnect', new VoiceDisconnectedEvent(this));
|
|
|
|
|
}
|
2022-06-06 14:59:53 +07:00
|
|
|
}
|
2022-06-28 14:51:05 +07:00
|
|
|
}, 2000);
|
2022-06-06 14:59:53 +07:00
|
|
|
}
|
|
|
|
|
);
|
2022-01-30 20:09:54 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async leaveVoiceChannel() {
|
2022-06-06 14:59:53 +07:00
|
|
|
if (this.player) this.player.pause();
|
2022-01-30 20:09:54 +07:00
|
|
|
|
2022-06-29 15:27:42 +07:00
|
|
|
const guild = DiscordProvider.client.guilds.cache.get(this.voiceChannel.guild.id);
|
|
|
|
|
if (!guild) return;
|
|
|
|
|
|
|
|
|
|
if (guild.members.me?.voice) this.voiceConnection?.disconnect();
|
2022-06-06 02:18:41 +07:00
|
|
|
}
|
2022-01-30 21:35:56 +07:00
|
|
|
|
2022-06-06 02:18:41 +07:00
|
|
|
public async pausePlayer() {
|
2022-06-06 14:59:53 +07:00
|
|
|
if (this.paused || !this.player) return;
|
|
|
|
|
if (!this.player.pause(true)) throw new Error('Unable to pause player');
|
2022-06-06 02:18:41 +07:00
|
|
|
this.paused = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async resumePlayer() {
|
2022-06-06 14:59:53 +07:00
|
|
|
if (!this.paused || !this.player) return;
|
|
|
|
|
if (!this.player.unpause()) throw new Error('Unable to resume player');
|
2022-06-06 02:18:41 +07:00
|
|
|
this.paused = false;
|
2022-01-30 20:09:54 +07:00
|
|
|
}
|
|
|
|
|
|
2022-06-06 14:59:53 +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-06-06 14:59:53 +07:00
|
|
|
if (!this.voiceConnection) throw new Error('No voice connection');
|
|
|
|
|
|
2022-03-03 22:37:41 +07:00
|
|
|
try {
|
2022-06-06 14:59:53 +07:00
|
|
|
|
2022-07-01 22:04:37 +07:00
|
|
|
let resource;
|
|
|
|
|
if(track instanceof YouTubeVideo) {
|
|
|
|
|
const stream = await playdl.stream(track.url);
|
|
|
|
|
resource = createAudioResource(stream.stream, {
|
|
|
|
|
inputType: stream.type
|
|
|
|
|
});
|
|
|
|
|
this.actualPlaybackURL = track.url;
|
|
|
|
|
} else {
|
|
|
|
|
const search = await DiscordMusicPlayer_Instance.searchYouTubeBySpotifyLink(track);
|
|
|
|
|
if(!search)
|
|
|
|
|
throw new Error('Unable to find Spotify track on YouTube');
|
|
|
|
|
|
|
|
|
|
const stream = await playdl.stream(search.url);
|
|
|
|
|
resource = createAudioResource(stream.stream, {
|
|
|
|
|
inputType: stream.type
|
|
|
|
|
});
|
|
|
|
|
this.actualPlaybackURL = search.url;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-06 14:59:53 +07:00
|
|
|
this.player.play(resource);
|
|
|
|
|
this.voiceConnection.subscribe(this.player);
|
2022-03-03 22:37:41 +07:00
|
|
|
} 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-06-06 14:59:53 +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-05-01 16:56:37 +07:00
|
|
|
this.previousTrack = this.queue.track[0];
|
2022-01-30 20:09:54 +07:00
|
|
|
this.queue.track.shift();
|
|
|
|
|
this.playTrack(this.queue.track[0]);
|
2022-06-06 14:59:53 +07:00
|
|
|
} else {
|
2022-05-01 16:56:37 +07:00
|
|
|
this.previousTrack = this.queue.track[0];
|
2022-02-04 22:42:59 +07:00
|
|
|
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-05-01 16:56:37 +07:00
|
|
|
public getPreviousTrack(): ValidTracks | undefined {
|
|
|
|
|
return this.previousTrack;
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-01 22:04:37 +07:00
|
|
|
public getActualPlaybackURL() {
|
|
|
|
|
return this.actualPlaybackURL;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-06 02:18:41 +07:00
|
|
|
public isPaused() {
|
|
|
|
|
return this.paused;
|
|
|
|
|
}
|
|
|
|
|
|
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-06-06 14:59:53 +07:00
|
|
|
if (this.voiceConnection.state.status !== 'destroyed') this.voiceConnection.destroy();
|
2022-01-30 20:09:54 +07:00
|
|
|
}
|
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);
|
2022-06-29 15:27:42 +07:00
|
|
|
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();
|
|
|
|
|
|
2022-06-06 14:59:53 +07:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2022-06-06 14:59:53 +07:00
|
|
|
public createGuildInstance(guildId: Snowflake, voiceChannel: VoiceChannel | StageChannel) {
|
2022-01-30 20:09:54 +07:00
|
|
|
this.GuildQueue.set(guildId, new DiscordMusicPlayerInstance({ voiceChannel }));
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-06 14:59:53 +07:00
|
|
|
public async destoryGuildInstance(guild: Guild | Snowflake) {
|
2022-01-30 20:09:54 +07:00
|
|
|
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) {
|
2022-06-06 14:59:53 +07:00
|
|
|
const searched: YouTubeVideo[] = await playdl.search(query, {
|
|
|
|
|
source: { youtube: 'video' }
|
|
|
|
|
});
|
2022-01-30 20:09:54 +07:00
|
|
|
if (searched.length == 0) return null;
|
|
|
|
|
return searched;
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-01 22:04:37 +07:00
|
|
|
public async searchYouTubeBySpotifyLink(spotifyLink: SpotifyLink) {
|
|
|
|
|
|
|
|
|
|
const track = await this.searchSpotifyBySpotifyLink(spotifyLink);
|
|
|
|
|
if(!track) return;
|
|
|
|
|
|
|
|
|
|
let artistNames = "";
|
|
|
|
|
for(let artist of track.artists)
|
|
|
|
|
artistNames += artist.name + " ";
|
|
|
|
|
|
|
|
|
|
const ytSearchResult = await this.searchYouTubeByQuery(`${artistNames} ${track.name}`);
|
|
|
|
|
if(!ytSearchResult) return null;
|
|
|
|
|
|
|
|
|
|
return ytSearchResult[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async searchSpotifyBySpotifyLink(spotifyLink: SpotifyLink) {
|
|
|
|
|
if(playdl.is_expired())
|
|
|
|
|
await playdl.refreshToken();
|
|
|
|
|
|
|
|
|
|
if(spotifyLink.type != 'track') return;
|
|
|
|
|
|
|
|
|
|
// Fetch data from spotify
|
2022-07-15 14:57:40 +07:00
|
|
|
const searched: Spotify = await playdl.spotify("https://open.spotify.com/track/" + spotifyLink.id).catch((err) => {
|
|
|
|
|
Logger.error(err.message);
|
|
|
|
|
throw new Error('Error while searching on Spotify');
|
|
|
|
|
});
|
2022-07-01 22:04:37 +07:00
|
|
|
if(!(searched instanceof SpotifyTrack)) return;
|
|
|
|
|
|
|
|
|
|
if (!searched) return null;
|
|
|
|
|
const track = searched as unknown as SpotifyTrack;
|
|
|
|
|
return track;
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-30 20:09:54 +07:00
|
|
|
public async searchYouTubeByYouTubeLink(youtubeLink: YouTubeLink) {
|
|
|
|
|
// Search the url
|
2022-06-29 15:27:42 +07:00
|
|
|
const searched: YouTubeVideo[] = await playdl.search('https://www.youtube.com/watch?v=' + youtubeLink.videoId, {
|
|
|
|
|
source: { youtube: 'video' }
|
|
|
|
|
});
|
2022-01-30 20:09:54 +07:00
|
|
|
for (let video of searched) {
|
2022-06-06 14:59:53 +07:00
|
|
|
if (video.id === youtubeLink.videoId) return video;
|
2022-01-30 20:09:54 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Serch the video Id
|
2022-06-06 14:59:53 +07:00
|
|
|
const searched2: YouTubeVideo[] = await playdl.search(youtubeLink.videoId, {
|
|
|
|
|
source: { youtube: 'video' }
|
|
|
|
|
});
|
2022-01-30 20:09:54 +07:00
|
|
|
for (let video of searched2) {
|
2022-06-06 14:59:53 +07:00
|
|
|
if (video.id === youtubeLink.videoId) return video;
|
2022-01-30 20:09:54 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Last resort, search the title
|
2022-06-29 15:27:42 +07:00
|
|
|
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-06-06 14:59:53 +07:00
|
|
|
const searched: YouTubeVideo[] = await playdl.search(videoInfo.video_details.title, {
|
|
|
|
|
source: { youtube: 'video' }
|
|
|
|
|
});
|
2022-01-30 20:09:54 +07:00
|
|
|
for (let video of searched) {
|
2022-06-06 14:59:53 +07:00
|
|
|
if (video.id === youtubeLink.videoId) return video;
|
2022-01-30 20:09:54 +07:00
|
|
|
}
|
|
|
|
|
}
|
2022-01-30 21:35:56 +07:00
|
|
|
|
2022-06-29 15:27:42 +07:00
|
|
|
let yt_info = await playdl.video_info('https://www.youtube.com/watch?v=' + youtubeLink.videoId);
|
2022-06-06 14:59:53 +07:00
|
|
|
if (yt_info) {
|
2022-01-30 21:35:56 +07:00
|
|
|
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,
|
2022-06-06 14:59:53 +07:00
|
|
|
upcoming: yt_info.video_details.upcoming,
|
2022-01-30 21:35:56 +07:00
|
|
|
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,
|
2022-05-01 16:56:37 +07:00
|
|
|
liveAt: yt_info.video_details.liveAt,
|
2022-01-30 21:35:56 +07:00
|
|
|
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-07-01 22:04:37 +07:00
|
|
|
public async getSpotifySongsInPlayList(spotifyLink: string) {
|
2022-07-15 14:57:40 +07:00
|
|
|
if(playdl.is_expired())
|
|
|
|
|
await playdl.refreshToken();
|
|
|
|
|
|
|
|
|
|
const result = await playdl.spotify(spotifyLink).catch((err) => {
|
|
|
|
|
Logger.error(err.message);
|
|
|
|
|
throw new Error('Error while searching on Spotify');
|
|
|
|
|
});
|
2022-07-01 22:04:37 +07:00
|
|
|
|
|
|
|
|
if(!(result.type == 'playlist' || result.type == 'album'))
|
|
|
|
|
throw new Error("Not a spotify playlist");
|
|
|
|
|
|
|
|
|
|
return result as unknown as SpotifyPlaylist;
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-30 20:09:54 +07:00
|
|
|
public isYouTubeLink(link: string): boolean {
|
|
|
|
|
try {
|
|
|
|
|
this.parseYouTubeLink(link);
|
|
|
|
|
return true;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-01 22:04:37 +07:00
|
|
|
public isSpotifyLink(link: string): boolean {
|
|
|
|
|
try {
|
|
|
|
|
this.parseSpotifyLink(link);
|
|
|
|
|
return true;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-30 20:09:54 +07:00
|
|
|
public parseYouTubeLink(query: string): YouTubeLink {
|
2022-06-06 14:59:53 +07:00
|
|
|
if (
|
|
|
|
|
query.startsWith('https://www.youtube.com/watch?v=') ||
|
2022-07-12 12:27:52 +07:00
|
|
|
query.startsWith('http://www.youtube.com/watch?v=') ||
|
|
|
|
|
query.startsWith('https://music.youtube.com/watch?v=') ||
|
|
|
|
|
query.startsWith('https://music.youtube.com/watch?v=')
|
2022-06-06 14:59:53 +07:00
|
|
|
) {
|
2022-01-30 20:09:54 +07:00
|
|
|
let data = this.parseURLQuery(query);
|
2022-06-06 14:59:53 +07:00
|
|
|
if (!data.v) throw new Error('YouTube link is invalid');
|
2022-01-30 20:09:54 +07:00
|
|
|
return {
|
|
|
|
|
videoId: data.v,
|
2022-06-06 14:59:53 +07:00
|
|
|
list: data.list ? (data.list !== 'RDMM' ? data.list : undefined) : undefined
|
|
|
|
|
};
|
|
|
|
|
} else if (query.startsWith('https://youtu.be/') || query.startsWith('http://youtu.be/')) {
|
2022-01-30 20:09:54 +07:00
|
|
|
//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-06-06 14:59:53 +07:00
|
|
|
};
|
|
|
|
|
} else if (
|
|
|
|
|
query.startsWith('https://www.youtube.com/playlist?list=') ||
|
|
|
|
|
query.startsWith('http://www.youtube.com/playlist?list=')
|
|
|
|
|
) {
|
2022-02-27 21:40:59 +07:00
|
|
|
let listId = query.split('?list=')[1];
|
|
|
|
|
return {
|
2022-06-06 14:59:53 +07:00
|
|
|
videoId: '',
|
2022-02-27 21:40:59 +07:00
|
|
|
list: listId
|
2022-06-06 14:59:53 +07:00
|
|
|
};
|
|
|
|
|
} else {
|
2022-01-30 20:09:54 +07:00
|
|
|
throw new Error('YouTube link is invalid');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-01 22:04:37 +07:00
|
|
|
public parseSpotifyLink(query: string): SpotifyLink {
|
|
|
|
|
if (query.startsWith('https://open.spotify.com/track/')) {
|
|
|
|
|
let id = query.split('/')[4].split(/[?#]/)[0];
|
|
|
|
|
return {
|
|
|
|
|
id: id,
|
|
|
|
|
type: 'track',
|
|
|
|
|
url: query.split(/[?#]/)[0]
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
else if (query.startsWith('https://open.spotify.com/album/')) {
|
|
|
|
|
let id = query.split('/')[4].split(/[?#]/)[0];
|
|
|
|
|
return {
|
|
|
|
|
id: id,
|
|
|
|
|
type: 'album',
|
|
|
|
|
url: query.split(/[?#]/)[0]
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
else if (query.startsWith('https://open.spotify.com/playlist/')) {
|
|
|
|
|
let id = query.split('/')[4].split(/[?#]/)[0];
|
|
|
|
|
return {
|
|
|
|
|
id: id,
|
|
|
|
|
type: 'playlist',
|
|
|
|
|
url: query.split(/[?#]/)[0]
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
throw new Error('Spotify link is invalid');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-30 20:09:54 +07:00
|
|
|
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();
|
2022-06-06 14:59:53 +07:00
|
|
|
export default DiscordMusicPlayer_Instance;
|