feat!: Switched to NekoMelody for music

This commit is contained in:
2024-08-17 21:11:09 +07:00
parent 1dc798d6e8
commit cca1dc67f4
19 changed files with 4097 additions and 1265 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "NekoMelody"]
path = NekoMelody
url = https://github.com/YuzuZensai/NekoMelody.git
Submodule
+1
Submodule NekoMelody added at 45a4a9ff64
+5 -2
View File
@@ -33,6 +33,7 @@
"zlib-sync": "^0.1.9" "zlib-sync": "^0.1.9"
}, },
"devDependencies": { "devDependencies": {
"@discordjs/builders": "^1.8.2",
"@prisma/client": "^5.15.0", "@prisma/client": "^5.15.0",
"@types/bluebird": "^3.5.42", "@types/bluebird": "^3.5.42",
"@types/i18n": "^0.13.12", "@types/i18n": "^0.13.12",
@@ -41,7 +42,8 @@
"@types/validator": "^13.11.10", "@types/validator": "^13.11.10",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"prisma": "^5.15.0", "prisma": "^5.15.0",
"ts-node-dev": "^2.0.0" "ts-node-dev": "^2.0.0",
"tsup": "^8.2.4"
}, },
"scripts": { "scripts": {
"tsc": "tsc", "tsc": "tsc",
@@ -58,5 +60,6 @@
"main": "index.js", "main": "index.js",
"repository": "https://github.com/kirameki-cafe/Yumi.git", "repository": "https://github.com/kirameki-cafe/Yumi.git",
"author": "Kirameki Café <contact@kirameki.cafe>", "author": "Kirameki Café <contact@kirameki.cafe>",
"license": "GPL-3.0" "license": "GPL-3.0",
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a"
} }
+3766
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -11,7 +11,7 @@ import {
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
import { makeInfoEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage'; import { makeInfoEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import Users from '../../services/Users'; import Users from '../../services/Users';
import Cache from '../../providers/Cache'; import Cache from '../../providers/Cache';
-2
View File
@@ -16,9 +16,7 @@ import {
makeSuccessEmbed makeSuccessEmbed
} from '../../utils/DiscordMessage'; } from '../../utils/DiscordMessage';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix';
import Users from '../../services/Users'; import Users from '../../services/Users';
import Cache from '../../providers/Cache';
import VRChatAPI from '../../providers/VRChatAPI'; import VRChatAPI from '../../providers/VRChatAPI';
const EMBEDS = { const EMBEDS = {
+27 -36
View File
@@ -29,12 +29,13 @@ import DiscordMusicPlayer, {
DiscordMusicPlayerInstance, DiscordMusicPlayerInstance,
DiscordMusicPlayerLoopMode, DiscordMusicPlayerLoopMode,
TrackUtils TrackUtils
} from '../../providers/DiscordMusicPlayerTempFix'; } from '../../providers/DiscordMusicPlayer';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
import { SpotifyTrack, YouTubeVideo } from 'play-dl'; import { SpotifyTrack, YouTubeVideo } from 'play-dl';
import { checkBotPermissionsInChannel } from '../../utils/DiscordPermission'; import { checkBotPermissionsInChannel } from '../../utils/DiscordPermission';
import { AudioInformation } from '../../../NekoMelody/src/providers/base';
const EMBEDS = { const EMBEDS = {
VOICECHANNEL_JOINED: (data: HybridInteractionMessage, locale: I18n) => { VOICECHANNEL_JOINED: (data: HybridInteractionMessage, locale: I18n) => {
@@ -79,9 +80,9 @@ const EMBEDS = {
user: data.getUser() user: data.getUser()
}); });
}, },
NOW_PLAYING: async (data: HybridInteractionMessage, locale: I18n, track: ValidTracks) => { NOW_PLAYING: async (data: HybridInteractionMessage, locale: I18n, track: AudioInformation) => {
const title = TrackUtils.getTitle(track); const title = track.metadata.title;
const thumbnails = await TrackUtils.getThumbnails(track); const thumbnails = track.metadata.thumbnail;
const embed = makeInfoEmbed({ const embed = makeInfoEmbed({
title: locale.__('musicplayer.now_playing'), title: locale.__('musicplayer.now_playing'),
@@ -90,14 +91,13 @@ const EMBEDS = {
user: DiscordProvider.client.user user: DiscordProvider.client.user
}); });
if (TrackUtils.getHighestResolutionThumbnail(thumbnails)) if (track.metadata.thumbnail) embed.setImage(track.metadata.thumbnail);
embed.setImage(TrackUtils.getHighestResolutionThumbnail(thumbnails).url);
return embed; return embed;
}, },
NOW_REPEATING: async (data: HybridInteractionMessage, locale: I18n, track: ValidTracks) => { NOW_REPEATING: async (data: HybridInteractionMessage, locale: I18n, track: AudioInformation) => {
const title = TrackUtils.getTitle(track); const title = track.metadata.title;
const thumbnails = await TrackUtils.getThumbnails(track); const thumbnails = track.metadata.thumbnail;
const embed = makeInfoEmbed({ const embed = makeInfoEmbed({
title: locale.__('musicplayer.now_playing_repeating'), title: locale.__('musicplayer.now_playing_repeating'),
@@ -106,8 +106,7 @@ const EMBEDS = {
user: DiscordProvider.client.user user: DiscordProvider.client.user
}); });
if (TrackUtils.getHighestResolutionThumbnail(thumbnails)) if (track.metadata.thumbnail) embed.setImage(track.metadata.thumbnail);
embed.setImage(TrackUtils.getHighestResolutionThumbnail(thumbnails).url);
return embed; return embed;
} }
@@ -230,51 +229,43 @@ export async function joinVoiceChannelProcedure(
// Register Event Listeners // Register Event Listeners
instance.events.on('playing', async (event: PlayerPlayingEvent) => { instance.events.on('playing', async (event: PlayerPlayingEvent) => {
const current = event.instance.nekoPlayer.getCurrentAudioInformation();
if (!current) return;
const locale = await Locale.getGuildLocale(guild.id); const locale = await Locale.getGuildLocale(guild.id);
if (!event.instance.queue || !event.instance.queue.track || event.instance.queue.track.length === 0) return;
previousTrack = event.instance.getPreviousTrack(); previousTrack = event.instance.getPreviousTrack();
if (event.instance.queue.track[0] !== previousTrack) isLoopMessageSent = false; //if (current !== previousTrack) isLoopMessageSent = false;
else if (event.instance.queue.track[0] === previousTrack && isLoopMessageSent) return; //else if (current === previousTrack && isLoopMessageSent) return;
const row = new ActionRowBuilder<ButtonBuilder>(); const row = new ActionRowBuilder<ButtonBuilder>();
if (event.instance.queue.track[0] instanceof SpotifyTrack) // if (event.instance.queue.track[0] instanceof SpotifyTrack)
row.addComponents([ // row.addComponents([
new ButtonBuilder() // new ButtonBuilder()
.setEmoji('🟢') // .setEmoji('🟢')
.setLabel('Open in Spotify') // .setLabel('Open in Spotify')
.setURL(encodeURI(`https://open.spotify.com/track/${event.instance.queue.track[0].id}`)) // .setURL(encodeURI(`https://open.spotify.com/track/${event.instance.queue.track[0].id}`))
.setStyle(ButtonStyle.Link) // .setStyle(ButtonStyle.Link)
]); // ]);
if (
event.instance.queue.track[0] instanceof YouTubeVideo ||
event.instance.queue.track[0] instanceof SpotifyTrack
) {
const actualPlaybackURL = event.instance.getActualPlaybackURL();
if (event.instance.queue.track[0] instanceof SpotifyTrack && !actualPlaybackURL) return;
row.addComponents([ row.addComponents([
new ButtonBuilder() new ButtonBuilder()
.setEmoji('🔴') .setEmoji('🔴')
.setLabel('Open in YouTube') .setLabel('Open in YouTube')
.setURL( .setURL(current.metadata.url)
event.instance.queue.track[0] instanceof YouTubeVideo
? encodeURI(`https://www.youtube.com/watch?v=${event.instance.queue.track[0].id}`)
: encodeURI(event.instance.getActualPlaybackURL()!)
)
.setStyle(ButtonStyle.Link) .setStyle(ButtonStyle.Link)
]); ]);
}
if (event.instance.textChannel) { if (event.instance.textChannel) {
if (event.instance.getLoopMode() === DiscordMusicPlayerLoopMode.Current) { if (event.instance.getLoopMode() === DiscordMusicPlayerLoopMode.Current) {
isLoopMessageSent = true; isLoopMessageSent = true;
await sendMessage(event.instance.textChannel, undefined, { await sendMessage(event.instance.textChannel, undefined, {
embeds: [await EMBEDS.NOW_REPEATING(data, locale, event.instance.queue.track[0])], embeds: [await EMBEDS.NOW_REPEATING(data, locale, current)],
components: [row] components: [row]
}); });
} else { } else {
await sendMessage(event.instance.textChannel, undefined, { await sendMessage(event.instance.textChannel, undefined, {
embeds: [await EMBEDS.NOW_PLAYING(data, locale, event.instance.queue.track[0])], embeds: [await EMBEDS.NOW_PLAYING(data, locale, current)],
components: [row] components: [row]
}); });
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import { I18n } from 'i18n'; import { I18n } from 'i18n';
import { Message, CommandInteraction } from 'discord.js'; import { Message, CommandInteraction } from 'discord.js';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
import { makeSuccessEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage'; import { makeSuccessEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
+2 -2
View File
@@ -1,7 +1,7 @@
import { I18n } from 'i18n'; import { I18n } from 'i18n';
import { Message, CommandInteraction } from 'discord.js'; import { Message, CommandInteraction } from 'discord.js';
import DiscordMusicPlayer, { DiscordMusicPlayerLoopMode } from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer, { DiscordMusicPlayerLoopMode } from '../../providers/DiscordMusicPlayer';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
@@ -105,7 +105,7 @@ export default class Loop extends DiscordModule {
true true
); );
if (instance.queue.track.length === 0) if (!instance.nekoPlayer.getCurrentAudioInformation())
return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)] embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)]
}); });
+21 -25
View File
@@ -3,16 +3,17 @@ import { I18n } from 'i18n';
import { SpotifyTrack, YouTubeVideo } from 'play-dl'; import { SpotifyTrack, YouTubeVideo } from 'play-dl';
import DiscordProvider from '../../providers/Discord'; import DiscordProvider from '../../providers/Discord';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayer';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
import { makeErrorEmbed, makeInfoEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage'; import { makeErrorEmbed, makeInfoEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
import { AudioInformation } from '../../../NekoMelody/src/providers/base';
const EMBEDS = { const EMBEDS = {
NOW_PLAYING: async (data: HybridInteractionMessage, locale: I18n, track: ValidTracks) => { NOW_PLAYING: async (data: HybridInteractionMessage, locale: I18n, track: AudioInformation) => {
const title = TrackUtils.getTitle(track); const title = track.metadata.title;
const thumbnails = await TrackUtils.getThumbnails(track); const thumbnail = track.metadata.thumbnail;
const embed = makeInfoEmbed({ const embed = makeInfoEmbed({
title: locale.__('musicplayer.now_playing'), title: locale.__('musicplayer.now_playing'),
@@ -21,8 +22,7 @@ const EMBEDS = {
user: DiscordProvider.client.user user: DiscordProvider.client.user
}); });
if (TrackUtils.getHighestResolutionThumbnail(thumbnails)) if (thumbnail) embed.setImage(thumbnail);
embed.setImage(TrackUtils.getHighestResolutionThumbnail(thumbnails).url);
return embed; return embed;
}, },
@@ -53,38 +53,34 @@ export default class NowPlaying extends DiscordModule {
const locale = await Locale.getGuildLocale(guild.id); const locale = await Locale.getGuildLocale(guild.id);
const instance = DiscordMusicPlayer.getGuildInstance(guild.id); const instance = DiscordMusicPlayer.getGuildInstance(guild.id);
if (!instance || !instance.queue.track[0]) const current = instance?.nekoPlayer.getCurrentAudioInformation();
if (!instance || !current)
return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)] embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)]
}); });
const row = new ActionRowBuilder<ButtonBuilder>(); const row = new ActionRowBuilder<ButtonBuilder>();
if (instance.queue.track[0] instanceof SpotifyTrack) // if (instance.queue.track[0] instanceof SpotifyTrack)
row.addComponents([ // row.addComponents([
new ButtonBuilder() // new ButtonBuilder()
.setEmoji('🟢') // .setEmoji('🟢')
.setLabel('Open in Spotify') // .setLabel('Open in Spotify')
.setURL(encodeURI(`https://open.spotify.com/track/${instance.queue.track[0].id}`)) // .setURL(encodeURI(`https://open.spotify.com/track/${instance.queue.track[0].id}`))
.setStyle(ButtonStyle.Link) // .setStyle(ButtonStyle.Link)
]); // ]);
if (instance.queue.track[0] instanceof YouTubeVideo || instance.queue.track[0] instanceof SpotifyTrack) {
const actualPlaybackURL = instance.getActualPlaybackURL();
if (instance.queue.track[0] instanceof SpotifyTrack && !actualPlaybackURL) return;
row.addComponents([ row.addComponents([
new ButtonBuilder() new ButtonBuilder()
.setEmoji('🔴') .setEmoji('🔴')
.setLabel('Open in YouTube') .setLabel('Open in YouTube')
.setURL( .setURL(current.metadata.url)
instance.queue.track[0] instanceof YouTubeVideo
? encodeURI(`https://www.youtube.com/watch?v=${instance.queue.track[0].id}`)
: encodeURI(instance.getActualPlaybackURL()!)
)
.setStyle(ButtonStyle.Link) .setStyle(ButtonStyle.Link)
]); ]);
}
return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {
embeds: [await EMBEDS.NOW_PLAYING(data, locale, instance.queue.track[0])], embeds: [await EMBEDS.NOW_PLAYING(data, locale, current)],
components: [row] components: [row]
}); });
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { Message, CommandInteraction, Interaction } from 'discord.js'; import { Message, CommandInteraction, Interaction } from 'discord.js';
import { I18n } from 'i18n'; import { I18n } from 'i18n';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
@@ -87,7 +87,7 @@ export default class Pause extends DiscordModule {
true true
); );
if (instance.queue.track.length === 0) if (!instance.nekoPlayer.getCurrentAudioInformation())
return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)] embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)]
}); });
+50 -64
View File
@@ -15,7 +15,7 @@ import { I18n } from 'i18n';
import { joinVoiceChannelProcedure } from './Join'; import { joinVoiceChannelProcedure } from './Join';
import DiscordProvider from '../../providers/Discord'; import DiscordProvider from '../../providers/Discord';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayer';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
@@ -26,6 +26,8 @@ import {
makeInfoEmbed makeInfoEmbed
} from '../../utils/DiscordMessage'; } from '../../utils/DiscordMessage';
import { checkBotPermissionsInChannel } from '../../utils/DiscordPermission'; import { checkBotPermissionsInChannel } from '../../utils/DiscordPermission';
import { AudioInformation } from '../../../NekoMelody/src/providers/base';
import { COMMON_EMBEDS } from '../Settings';
const EMBEDS = { const EMBEDS = {
PLAY_INFO: (data: HybridInteractionMessage, locale: I18n) => { PLAY_INFO: (data: HybridInteractionMessage, locale: I18n) => {
@@ -41,18 +43,14 @@ const EMBEDS = {
user: data.getUser() user: data.getUser()
}); });
}, },
ADDED_QUEUE: async (data: HybridInteractionMessage, locale: I18n, track: ValidTracks) => { ADDED_QUEUE: async (data: HybridInteractionMessage, locale: I18n, track: AudioInformation) => {
const title = TrackUtils.getTitle(track);
const thumbnails = await TrackUtils.getThumbnails(track);
let embed = makeSuccessEmbed({ let embed = makeSuccessEmbed({
title: locale.__('musicplayer_play.queue_added_song'), title: locale.__('musicplayer_play.queue_added_song'),
description: title, description: track.metadata.title ?? 'No title',
user: data.getUser() user: data.getUser()
}); });
if (TrackUtils.getHighestResolutionThumbnail(thumbnails)) if (track.metadata.thumbnail) embed.setImage(track.metadata.thumbnail);
embed.setImage(TrackUtils.getHighestResolutionThumbnail(thumbnails).url);
return embed; return embed;
}, },
@@ -265,9 +263,7 @@ export default class Play extends DiscordModule {
if (!instance) return; if (!instance) return;
let result = await DiscordMusicPlayer.searchYouTubeByYouTubeLink( let result = await instance.nekoPlayer.enqueue(interaction.values[0]).catch((err) => {
DiscordMusicPlayer.parseYouTubeLink(interaction.values[0])
).catch((err) => {
sendHybridInteractionMessageResponse( sendHybridInteractionMessageResponse(
hybridData, hybridData,
{ embeds: [EMBEDS.LOOKUP_ERROR(hybridData, locale, err)] }, { embeds: [EMBEDS.LOOKUP_ERROR(hybridData, locale, err)] },
@@ -277,7 +273,6 @@ export default class Play extends DiscordModule {
}); });
if (!result) return; if (!result) return;
instance.addTrackToQueue(result);
return await sendHybridInteractionMessageResponse( return await sendHybridInteractionMessageResponse(
hybridData, hybridData,
{ embeds: [await EMBEDS.ADDED_QUEUE(new HybridInteractionMessage(interaction), locale, result)] }, { embeds: [await EMBEDS.ADDED_QUEUE(new HybridInteractionMessage(interaction), locale, result)] },
@@ -335,6 +330,14 @@ export default class Play extends DiscordModule {
if (!instance) return; if (!instance) return;
let placeholder: HybridInteractionMessage | undefined;
let _placeholder = await sendHybridInteractionMessageResponse(data, {
embeds: [COMMON_EMBEDS.PROCESSING(data, locale)]
});
if (_placeholder) placeholder = new HybridInteractionMessage(_placeholder);
if (!placeholder) return;
if (DiscordMusicPlayer.isYouTubeLink(query)) { if (DiscordMusicPlayer.isYouTubeLink(query)) {
let linkData = DiscordMusicPlayer.parseYouTubeLink(query); let linkData = DiscordMusicPlayer.parseYouTubeLink(query);
@@ -345,15 +348,13 @@ export default class Play extends DiscordModule {
for (let song of songs) { for (let song of songs) {
instance.addTrackToQueue(song); instance.addTrackToQueue(song);
} }
return await sendHybridInteractionMessageResponse( return placeholder.getMessage().edit({ embeds: [EMBEDS.ADDED_SONGS_QUEUE(data, locale, songs)] });
data,
{ embeds: [EMBEDS.ADDED_SONGS_QUEUE(data, locale, songs)] },
true
);
} }
let result = await DiscordMusicPlayer.searchYouTubeByYouTubeLink(linkData).catch((err) => { let result = await instance.nekoPlayer
sendHybridInteractionMessageResponse(data, { embeds: [EMBEDS.LOOKUP_ERROR(data, locale, err)] }, true); .enqueue(`https://www.youtube.com/watch?v=${linkData.videoId}`)
.catch((err) => {
placeholder.getMessage().edit({ embeds: [EMBEDS.LOOKUP_ERROR(data, locale, err)] });
return null; return null;
}); });
@@ -371,8 +372,6 @@ export default class Play extends DiscordModule {
data.getMessage().suppressEmbeds(true); data.getMessage().suppressEmbeds(true);
} }
instance.addTrackToQueue(result);
if (linkData.list) { if (linkData.list) {
const row = new ActionRowBuilder<ButtonBuilder>().addComponents([ const row = new ActionRowBuilder<ButtonBuilder>().addComponents([
new ButtonBuilder() new ButtonBuilder()
@@ -388,24 +387,16 @@ export default class Play extends DiscordModule {
.setStyle(ButtonStyle.Primary) .setStyle(ButtonStyle.Primary)
]); ]);
return await sendHybridInteractionMessageResponse(data, { return placeholder
embeds: [await EMBEDS.ADDED_QUEUE(data, locale, result)], .getMessage()
components: [row] .edit({ embeds: [await EMBEDS.ADDED_QUEUE(data, locale, result)], components: [row] });
}); } else return placeholder.getMessage().edit({ embeds: [await EMBEDS.ADDED_QUEUE(data, locale, result)] });
} else
return await sendHybridInteractionMessageResponse(data, {
embeds: [await EMBEDS.ADDED_QUEUE(data, locale, result)]
});
} else if (DiscordMusicPlayer.isSpotifyLink(query)) { } else if (DiscordMusicPlayer.isSpotifyLink(query)) {
let linkData = DiscordMusicPlayer.parseSpotifyLink(query); let linkData = DiscordMusicPlayer.parseSpotifyLink(query);
if (linkData.type === 'playlist' || linkData.type === 'album') { if (linkData.type === 'playlist' || linkData.type === 'album') {
let playlist = await DiscordMusicPlayer.getSpotifySongsInPlayList(query).catch((err) => { let playlist = await DiscordMusicPlayer.getSpotifySongsInPlayList(query).catch((err) => {
sendHybridInteractionMessageResponse( placeholder.getMessage().edit({ embeds: [EMBEDS.LOOKUP_ERROR(data, locale, err)] });
data,
{ embeds: [EMBEDS.LOOKUP_ERROR(data, locale, err)] },
true
);
return null; return null;
}); });
@@ -416,15 +407,11 @@ export default class Play extends DiscordModule {
instance.addTrackToQueue(song); instance.addTrackToQueue(song);
} }
return await sendHybridInteractionMessageResponse( return placeholder.getMessage().edit({ embeds: [EMBEDS.ADDED_SONGS_QUEUE(data, locale, songs)] });
data,
{ embeds: [EMBEDS.ADDED_SONGS_QUEUE(data, locale, songs)] },
true
);
} }
let result = await DiscordMusicPlayer.searchSpotifyBySpotifyLink(linkData).catch((err) => { let result = await DiscordMusicPlayer.searchSpotifyBySpotifyLink(linkData).catch((err) => {
sendHybridInteractionMessageResponse(data, { embeds: [EMBEDS.LOOKUP_ERROR(data, locale, err)] }, true); placeholder.getMessage().edit({ embeds: [EMBEDS.LOOKUP_ERROR(data, locale, err)] });
return null; return null;
}); });
@@ -442,30 +429,29 @@ export default class Play extends DiscordModule {
data.getMessage().suppressEmbeds(true); data.getMessage().suppressEmbeds(true);
} }
instance.addTrackToQueue(result); const youTubeQuery = `${result.name} ${result.artists.map((artist) => artist.name).join(' ')}`;
return await sendHybridInteractionMessageResponse(data, { const youTubeResult = await DiscordMusicPlayer.searchYouTubeByQuery(youTubeQuery);
embeds: [await EMBEDS.ADDED_QUEUE(data, locale, result)]
}); if (!youTubeResult) {
placeholder
.getMessage()
.edit({ embeds: [EMBEDS.LOOKUP_ERROR(data, locale, new Error('Cannot find that song'))] });
return;
}
const finalResult = await instance.addTrackToQueue(youTubeResult[0]);
return placeholder.getMessage().edit({ embeds: [await EMBEDS.ADDED_QUEUE(data, locale, finalResult)] });
} else { } else {
let result = await DiscordMusicPlayer.searchYouTubeByQuery(query); let result = await DiscordMusicPlayer.searchYouTubeByQuery(query);
if (!result) { if (!result) {
// TODO: Send not found embed placeholder
await sendHybridInteractionMessageResponse( .getMessage()
data, .edit({ embeds: [EMBEDS.LOOKUP_ERROR(data, locale, new Error('Cannot find that song'))] });
{ embeds: [EMBEDS.LOOKUP_ERROR(data, locale, new Error('Cannot find that song'))] },
true
);
return; return;
} }
instance.addTrackToQueue(result[0]);
// Max 1 hour
if (result[0].durationInSec > 3600)
return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.TOO_LONG(data, locale)]
});
if (result.length > 1) { if (result.length > 1) {
// TODO: Find a better logic than this // TODO: Find a better logic than this
@@ -492,15 +478,15 @@ export default class Play extends DiscordModule {
.setStyle(ButtonStyle.Primary) .setStyle(ButtonStyle.Primary)
]); ]);
return await sendHybridInteractionMessageResponse(data, { const finalResult = await instance.nekoPlayer.enqueue(result[0].url);
embeds: [await EMBEDS.ADDED_QUEUE(data, locale, result[0])], return placeholder
components: [row] .getMessage()
}); .edit({ embeds: [await EMBEDS.ADDED_QUEUE(data, locale, finalResult)], components: [row] });
} }
return await sendHybridInteractionMessageResponse(data, { const finalResult = await instance.nekoPlayer.enqueue(result[0].url);
embeds: [await EMBEDS.ADDED_QUEUE(data, locale, result[0])]
}); return placeholder.getMessage().edit({ embeds: [await EMBEDS.ADDED_QUEUE(data, locale, finalResult)] });
} }
} }
+24 -19
View File
@@ -1,12 +1,14 @@
import { I18n } from 'i18n'; import { I18n } from 'i18n';
import { Message, CommandInteraction, ActivityType, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; import { Message, CommandInteraction, ActivityType, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayer';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
import { makeSuccessEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage'; import { makeSuccessEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
import { joinVoiceChannelProcedure } from './Join'; import { joinVoiceChannelProcedure } from './Join';
import { AudioInformation } from '../../../NekoMelody/src/providers/base';
import { COMMON_EMBEDS } from '../Settings';
const EMBEDS = { const EMBEDS = {
NOT_DETECTED: (data: HybridInteractionMessage, locale: I18n) => { NOT_DETECTED: (data: HybridInteractionMessage, locale: I18n) => {
@@ -27,9 +29,9 @@ const EMBEDS = {
user: data.getUser() user: data.getUser()
}); });
}, },
ADDED_QUEUE: async (data: HybridInteractionMessage, locale: I18n, track: ValidTracks) => { ADDED_QUEUE: async (data: HybridInteractionMessage, locale: I18n, track: AudioInformation) => {
const title = TrackUtils.getTitle(track); const title = track.metadata.title;
const thumbnails = await TrackUtils.getThumbnails(track); const thumbnail = track.metadata.thumbnail;
let embed = makeSuccessEmbed({ let embed = makeSuccessEmbed({
title: locale.__('musicplayer_play.queue_added_song'), title: locale.__('musicplayer_play.queue_added_song'),
@@ -37,8 +39,7 @@ const EMBEDS = {
user: data.getUser() user: data.getUser()
}); });
if (TrackUtils.getHighestResolutionThumbnail(thumbnails)) if (thumbnail) embed.setImage(thumbnail);
embed.setImage(TrackUtils.getHighestResolutionThumbnail(thumbnails).url);
return embed; return embed;
}, },
@@ -101,6 +102,14 @@ export default class PlayMy extends DiscordModule {
if (!member.presence) return; if (!member.presence) return;
if (!member.presence.activities) return; if (!member.presence.activities) return;
let placeholder: HybridInteractionMessage | undefined;
let _placeholder = await sendHybridInteractionMessageResponse(data, {
embeds: [COMMON_EMBEDS.PROCESSING(data, locale)]
});
if (_placeholder) placeholder = new HybridInteractionMessage(_placeholder);
if (!placeholder) return;
let query; let query;
let found = false; let found = false;
for (const activity of member.presence.activities) { for (const activity of member.presence.activities) {
@@ -113,13 +122,9 @@ export default class PlayMy extends DiscordModule {
const result = await DiscordMusicPlayer.searchYouTubeByQuery(query); const result = await DiscordMusicPlayer.searchYouTubeByQuery(query);
if (!result) continue; if (!result) continue;
instance.addTrackToQueue(result[0]);
// Max 1 hour const finalResult = await instance.nekoPlayer.enqueue(result[0].url);
if (result[0].durationInSec > 3600) if (!finalResult) continue;
return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.TOO_LONG(data, locale)]
});
let row; let row;
if (result.length > 1) { if (result.length > 1) {
@@ -149,19 +154,19 @@ export default class PlayMy extends DiscordModule {
} }
if (!row) if (!row)
await sendHybridInteractionMessageResponse(data, { return placeholder
embeds: [await EMBEDS.ADDED_QUEUE(data, locale, result[0])] .getMessage()
}); .edit({ embeds: [await EMBEDS.ADDED_QUEUE(data, locale, finalResult)] });
else else
await sendHybridInteractionMessageResponse(data, { return placeholder.getMessage().edit({
embeds: [await EMBEDS.ADDED_QUEUE(data, locale, result[0])], embeds: [await EMBEDS.ADDED_QUEUE(data, locale, finalResult)],
components: [row] components: [row]
}); });
} }
} }
if (!found) await sendHybridInteractionMessageResponse(data, { embeds: [EMBEDS.NOT_DETECTED(data, locale)] }); if (!found)
return await sendHybridInteractionMessageResponse(data, { embeds: [EMBEDS.NOT_DETECTED(data, locale)] });
return; return;
} }
} }
+19 -19
View File
@@ -5,7 +5,7 @@ import DiscordMusicPlayer, {
DiscordMusicPlayerInstance, DiscordMusicPlayerInstance,
DiscordMusicPlayerLoopMode, DiscordMusicPlayerLoopMode,
TrackUtils TrackUtils
} from '../../providers/DiscordMusicPlayerTempFix'; } from '../../providers/DiscordMusicPlayer';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
@@ -18,44 +18,45 @@ import {
const EMBEDS = { const EMBEDS = {
QUEUE: (data: HybridInteractionMessage, locale: I18n, instance: DiscordMusicPlayerInstance) => { QUEUE: (data: HybridInteractionMessage, locale: I18n, instance: DiscordMusicPlayerInstance) => {
const queue = instance.queue; const queue = instance.nekoPlayer.getQueue();
const current = instance.nekoPlayer.getCurrentAudioInformation();
if (!queue.track[0]) if (!current)
return makeInfoEmbed({ return makeInfoEmbed({
title: locale.__('musicplayer_queue.title'), title: locale.__('musicplayer_queue.title'),
description: locale.__('musicplayer_queue.empty'), description: locale.__('musicplayer_queue.empty'),
user: data.getUser() user: data.getUser()
}); });
else {
const nowPlayingText = `${ const nowPlayingText = `${
instance.getLoopMode() === DiscordMusicPlayerLoopMode.Current instance.getLoopMode() === DiscordMusicPlayerLoopMode.Current
? ` ${locale.__('musicplayer_queue.looping_current')}` ? ` ${locale.__('musicplayer_queue.looping_current')}`
: '' : ''
}: [${TrackUtils.getTitle(queue.track[0])}](${queue.track[0].url})`; }: [${current.metadata.title}](${current.metadata.url})`;
let description; let description;
if (queue.track.length == 1) { if (queue.length == 0) {
let queueString = `1. [${TrackUtils.getTitle(queue.track[0])}](${queue.track[0].url})`; let queueString = `1. [${current.metadata.title}](${current.metadata.url})`;
description = `${locale.__('musicplayer_queue.now_playing')}${nowPlayingText}\n description = `${locale.__('musicplayer_queue.now_playing')}${nowPlayingText}\n
There are ${queue.track.length} song in the queue!\n${queueString}`; ${locale.__('musicplayer_queue.empty')}`;
} else if (queue.track.length >= 10) { } else if (queue.length >= 10) {
const upcomingText = `[${TrackUtils.getTitle(queue.track[1])}](${queue.track[1].url})`; const upcomingText = `[${queue[0].metadata.title}](${queue[0].metadata.url})`;
const first10 = queue.track.slice(0, 10); const first10 = queue.slice(0, 10);
let queueString = first10 let queueString = first10
.map((track, index) => `${index + 1}. [${TrackUtils.getTitle(track)}](${track.url})`) .map((track, index) => `${index + 1}. [${track.metadata.title}](${track.metadata.url})`)
.join('\n'); .join('\n');
description = `${locale.__('musicplayer_queue.now_playing')}${nowPlayingText} description = `${locale.__('musicplayer_queue.now_playing')}${nowPlayingText}
${locale.__('musicplayer_queue.upcoming_song')} ${upcomingText}\n ${locale.__('musicplayer_queue.upcoming_song')} ${upcomingText}\n
${locale.__('musicplayer_queue.song_x_in_queue', { COUNT: queue.track.length.toString() })} ${locale.__('musicplayer_queue.song_x_in_queue', { COUNT: queue.length.toString() })}
${queueString}\n${queue.track.length > 10 ? `...${queue.track.length - 10} more songs` : ''}`; ${queueString}\n${queue.length > 10 ? `...${queue.length - 10} more songs` : ''}`;
} else { } else {
const upcomingText = `[${TrackUtils.getTitle(queue.track[1])}](${queue.track[1].url})`; const upcomingText = `[${queue[0].metadata.title}](${queue[0].metadata.url})`;
const queueString = queue.track const queueString = queue
.map((track, index) => `${index + 1}. [${TrackUtils.getTitle(track)}](${track.url})`) .map((track, index) => `${index + 1}. [${track.metadata.title}](${track.metadata.url})`)
.join('\n'); .join('\n');
description = `${locale.__('musicplayer_queue.now_playing')}${nowPlayingText} description = `${locale.__('musicplayer_queue.now_playing')}${nowPlayingText}
${locale.__('musicplayer_queue.upcoming_song')} ${upcomingText}\n ${locale.__('musicplayer_queue.upcoming_song')} ${upcomingText}\n
${locale.__('musicplayer_queue.song_x_in_queue', { COUNT: queue.track.length.toString() })} ${locale.__('musicplayer_queue.song_x_in_queue', { COUNT: queue.length.toString() })}
${queueString}`; ${queueString}`;
} }
@@ -70,7 +71,6 @@ const EMBEDS = {
], ],
user: data.getUser() user: data.getUser()
}); });
}
}, },
QUEUE_CLEARED: (data: HybridInteractionMessage, locale: I18n) => { QUEUE_CLEARED: (data: HybridInteractionMessage, locale: I18n) => {
return makeSuccessEmbed({ return makeSuccessEmbed({
+2 -2
View File
@@ -1,7 +1,7 @@
import { Message, CommandInteraction, Interaction } from 'discord.js'; import { Message, CommandInteraction, Interaction } from 'discord.js';
import { I18n } from 'i18n'; import { I18n } from 'i18n';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
import { import {
@@ -87,7 +87,7 @@ export default class Resume extends DiscordModule {
true true
); );
if (instance.queue.track.length === 0) if (!instance.nekoPlayer.getCurrentAudioInformation())
return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)] embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)]
}); });
+1 -1
View File
@@ -10,7 +10,7 @@ import { I18n } from 'i18n';
import { joinVoiceChannelProcedure } from './Join'; import { joinVoiceChannelProcedure } from './Join';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayer';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule'; import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
+3 -3
View File
@@ -3,7 +3,7 @@ import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModu
import { Message, CommandInteraction } from 'discord.js'; import { Message, CommandInteraction } from 'discord.js';
import { makeSuccessEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage'; import { makeSuccessEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix'; import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import { I18n } from 'i18n'; import { I18n } from 'i18n';
import Locale from '../../services/Locale'; import Locale from '../../services/Locale';
@@ -81,14 +81,14 @@ export default class Skip extends DiscordModule {
true true
); );
if (instance.queue.track.length === 0) if (!instance.nekoPlayer.getCurrentAudioInformation())
return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)] embeds: [EMBEDS.NO_MUSIC_PLAYING(data, locale)]
}); });
instance.skipTrack(); instance.skipTrack();
if (instance.queue.track.length === 0) if (!instance.nekoPlayer.getCurrentAudioInformation())
return await sendHybridInteractionMessageResponse(data, { return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.SKIPPED_LASTSONG(data, locale)] embeds: [EMBEDS.SKIPPED_LASTSONG(data, locale)]
}); });
+72 -197
View File
@@ -21,11 +21,13 @@ import {
DiscordGatewayAdapterCreator DiscordGatewayAdapterCreator
} from '@discordjs/voice'; } from '@discordjs/voice';
import playdl, { Spotify, SpotifyPlaylist, SpotifyTrack, YouTubeVideo } from 'play-dl'; import playdl, { Spotify, SpotifyPlaylist, SpotifyTrack, YouTubeVideo } from 'play-dl';
import { EventEmitter } from 'stream'; import NekoMelody, { Player } from '../../NekoMelody/src/index';
import { YtDlpProvider } from '../../NekoMelody/src/providers';
import DiscordProvider from './Discord'; import DiscordProvider from './Discord';
import Environment from './Environment'; import Environment from './Environment';
import Logger from '../libs/Logger'; import Logger from '../libs/Logger';
import EventEmitter from 'events';
import { AudioInformation } from '../../NekoMelody/src/providers/base';
const LOGGING_TAG = '[DiscordMusicPlayer]'; const LOGGING_TAG = '[DiscordMusicPlayer]';
@@ -187,7 +189,8 @@ export enum DiscordMusicPlayerLoopMode {
export class DiscordMusicPlayerInstance { export class DiscordMusicPlayerInstance {
public queue: Queue; public queue: Queue;
public player: AudioPlayer; public discordPlayer: AudioPlayer;
public nekoPlayer: Player;
public textChannel?: BaseGuildTextChannel | BaseGuildVoiceChannel; public textChannel?: BaseGuildTextChannel | BaseGuildVoiceChannel;
public voiceChannel: VoiceChannel | StageChannel; public voiceChannel: VoiceChannel | StageChannel;
public voiceConnection?: VoiceConnection; public voiceConnection?: VoiceConnection;
@@ -196,51 +199,71 @@ export class DiscordMusicPlayerInstance {
public paused: boolean = false; public paused: boolean = false;
public loopMode: DiscordMusicPlayerLoopMode = DiscordMusicPlayerLoopMode.None; public loopMode: DiscordMusicPlayerLoopMode = DiscordMusicPlayerLoopMode.None;
public actualPlaybackURL?: string;
public readonly events: EventEmitter; public readonly events: EventEmitter;
private providers = [new YtDlpProvider()];
constructor({ voiceChannel }: { voiceChannel: VoiceChannel | StageChannel }) { constructor({ voiceChannel }: { voiceChannel: VoiceChannel | StageChannel }) {
this.queue = new Queue(); this.queue = new Queue();
this.player = createAudioPlayer({ this.discordPlayer = createAudioPlayer({
behaviors: { behaviors: {
noSubscriber: NoSubscriberBehavior.Pause, noSubscriber: NoSubscriberBehavior.Pause,
maxMissedFrames: 1000 maxMissedFrames: 1000
} }
}); });
this.nekoPlayer = NekoMelody.createPlayer(this.providers);
this.voiceChannel = voiceChannel; this.voiceChannel = voiceChannel;
this.events = new EventEmitter(); this.events = new EventEmitter();
this.player.on(AudioPlayerStatus.Idle, async (oldStage, newStage) => { // this.player.on(AudioPlayerStatus.Idle, async (oldStage, newStage) => {
//The player stopped // //The player stopped
if (newStage.status === AudioPlayerStatus.Idle && oldStage.status !== AudioPlayerStatus.Idle) { // if (newStage.status === AudioPlayerStatus.Idle && oldStage.status !== AudioPlayerStatus.Idle) {
// Loop mode is set to current song // // Loop mode is set to current song
if (this.loopMode === DiscordMusicPlayerLoopMode.Current) { // if (this.loopMode === DiscordMusicPlayerLoopMode.Current) {
if (this.queue.track.length !== 0) { // if (this.queue.track.length !== 0) {
this.previousTrack = this.queue.track[0]; // this.previousTrack = this.queue.track[0];
this.playTrack(this.queue.track[0]); // this.playTrack(this.queue.track[0]);
} // }
return; // return;
} // }
// There are more songs in the queue, remove finished song and play the next one // // There are more songs in the queue, remove finished song and play the next one
if (this.queue.track.length !== 0) { // if (this.queue.track.length !== 0) {
let previousTrack = this.queue.track.shift(); // let previousTrack = this.queue.track.shift();
if (previousTrack) this.previousTrack = previousTrack; // if (previousTrack) this.previousTrack = previousTrack;
if (this.queue.track.length > 0) { // if (this.queue.track.length > 0) {
this.playTrack(this.queue.track[0]); // this.playTrack(this.queue.track[0]);
} // }
} // }
} // }
// });
// this.player.on(AudioPlayerStatus.Playing, (oldState: any, newState: any) => {
// this.events.emit('playing', new PlayerPlayingEvent(this));
// });
// this.player.on('error', (error: Error) => {
// this.events.emit('error', new PlayerErrorEvent(this, error));
// });
this.nekoPlayer.on('play', (information: AudioInformation) => {
if (!this.nekoPlayer.stream) throw new Error('No input stream');
const resource = createAudioResource(this.nekoPlayer.stream, {
//inlineVolume: true,
}); });
this.player.on(AudioPlayerStatus.Playing, (oldState: any, newState: any) => { this.discordPlayer.play(resource);
this.nekoPlayer.startCurrentStream();
this.events.emit('playing', new PlayerPlayingEvent(this)); this.events.emit('playing', new PlayerPlayingEvent(this));
});
this.player.on('error', (error: Error) => { this.discordPlayer.on('stateChange', (oldState, newState) => {
this.events.emit('error', new PlayerErrorEvent(this, error)); console.log('State change', oldState.status, newState.status);
if (oldState.status === 'playing' && newState.status === 'idle') {
this.nekoPlayer.endCurrentStream();
}
});
}); });
} }
@@ -261,6 +284,8 @@ export class DiscordMusicPlayerInstance {
adapterCreator: this.voiceChannel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator adapterCreator: this.voiceChannel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
}); });
this.voiceConnection.subscribe(this.discordPlayer);
this.voiceConnection.on( this.voiceConnection.on(
VoiceConnectionStatus.Ready, VoiceConnectionStatus.Ready,
async (oldState: VoiceConnectionState, newState: VoiceConnectionState) => { async (oldState: VoiceConnectionState, newState: VoiceConnectionState) => {
@@ -290,7 +315,7 @@ export class DiscordMusicPlayerInstance {
} }
public async leaveVoiceChannel() { public async leaveVoiceChannel() {
if (this.player) this.player.pause(); if (this.discordPlayer) this.discordPlayer.pause();
const guild = DiscordProvider.client.guilds.cache.get(this.voiceChannel.guild.id); const guild = DiscordProvider.client.guilds.cache.get(this.voiceChannel.guild.id);
if (!guild) return; if (!guild) return;
@@ -299,81 +324,28 @@ export class DiscordMusicPlayerInstance {
} }
public async pausePlayer() { public async pausePlayer() {
if (this.paused || !this.player) return; if (this.paused || !this.discordPlayer) return;
if (!this.player.pause(true)) throw new Error('Unable to pause player'); if (!this.discordPlayer.pause(true)) throw new Error('Unable to pause player');
this.paused = true; this.paused = true;
} }
public async resumePlayer() { public async resumePlayer() {
if (!this.paused || !this.player) return; if (!this.paused || !this.discordPlayer) return;
if (!this.player.unpause()) throw new Error('Unable to resume player'); if (!this.discordPlayer.unpause()) throw new Error('Unable to resume player');
this.paused = false; this.paused = false;
} }
public addTrackToQueue(track: ValidTracks) { public async addTrackToQueue(track: ValidTracks) {
if (this.queue.track.length === 0) { return await this.nekoPlayer.enqueue(track.url);
this.queue.track.push(track);
this.playTrack(this.queue.track[0]);
return;
}
this.queue.track.push(track);
}
public async playTrack(track: ValidTracks) {
if (!this.voiceConnection) throw new Error('No voice connection');
try {
let resource;
if (track instanceof YouTubeVideo) {
const stream = await playdl.stream(track.url);
Logger.verbose(
LOGGING_TAG,
`New stream created, type: ${stream.type}, url: ${track.url}, Guild: ${this.voiceChannel.guild.id}, VoiceChannel: ${this.voiceChannel.id}`
);
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);
Logger.verbose(
LOGGING_TAG,
`New stream created, type: ${stream.type}, url: ${track.url}, Guild: ${this.voiceChannel.guild.id}, VoiceChannel: ${this.voiceChannel.id}`
);
resource = createAudioResource(stream.stream, {
inputType: stream.type
});
this.actualPlaybackURL = search.url;
}
this.player.play(resource);
this.voiceConnection.subscribe(this.player);
} catch (error: any) {
this.events.emit('error', new PlayerErrorEvent(this, error));
this.skipTrack();
}
} }
public async skipTrack() { public async skipTrack() {
if (!this.voiceConnection) throw new Error('No voice connection'); if (!this.voiceConnection) throw new Error('No voice connection');
if (this.queue.track.length > 1) { if (this.nekoPlayer.getQueue().length === 0) return;
this.previousTrack = this.queue.track[0];
this.queue.track.shift(); await this.nekoPlayer.skip();
this.playTrack(this.queue.track[0]); if (this.paused) this.paused = false;
} else {
this.previousTrack = this.queue.track[0];
this.queue.track.shift();
this.player.stop();
}
} }
public clearQueue() { public clearQueue() {
@@ -413,8 +385,8 @@ export class DiscordMusicPlayerInstance {
return this.previousTrack; return this.previousTrack;
} }
public getActualPlaybackURL() { public getQueue() {
return this.actualPlaybackURL; return this.nekoPlayer.getQueue();
} }
public isReady() { public isReady() {
@@ -439,16 +411,13 @@ export class DiscordMusicPlayerInstance {
await this.leaveVoiceChannel(); await this.leaveVoiceChannel();
if (this.voiceConnection) { if (this.voiceConnection) {
this.voiceConnection.removeAllListeners();
if (this.voiceConnection.state.status !== 'destroyed') this.voiceConnection.destroy(); if (this.voiceConnection.state.status !== 'destroyed') this.voiceConnection.destroy();
} }
if (this.player) { if (this.discordPlayer) {
this.player.removeAllListeners(); this.discordPlayer.stop(true);
this.player.stop(true);
} }
this.queue.track = [];
this.textChannel = undefined; this.textChannel = undefined;
this.voiceConnection = undefined; this.voiceConnection = undefined;
} }
@@ -456,8 +425,8 @@ export class DiscordMusicPlayerInstance {
public async _fake_error_on_player() { public async _fake_error_on_player() {
const stream = 'https://fakestream:42069/fake/stream/fake/audio/fake.mp3'; const stream = 'https://fakestream:42069/fake/stream/fake/audio/fake.mp3';
const resource = createAudioResource(stream); const resource = createAudioResource(stream);
this.player.play(resource); this.discordPlayer.play(resource);
this.player.emit('error', new AudioPlayerError(new Error('Music player was manually crashed'), null!)); //this.player.emit('error', new AudioPlayerError(new Error('Music player was manually crashed'), null!));
} }
} }
@@ -500,19 +469,6 @@ class DiscordMusicPlayer {
return searched; return searched;
} }
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) { public async searchSpotifyBySpotifyLink(spotifyLink: SpotifyLink) {
if (playdl.is_expired()) { if (playdl.is_expired()) {
Logger.debug(LOGGING_TAG, 'Spotify token expired, refreshing...'); Logger.debug(LOGGING_TAG, 'Spotify token expired, refreshing...');
@@ -538,87 +494,6 @@ class DiscordMusicPlayer {
return track; return track;
} }
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' }
});
Logger.verbose(
LOGGING_TAG,
`Search YouTube by link (Pass 1): ${youtubeLink.videoId}, Total result: ${
searched.length
}, ${JSON.stringify(searched)}`
);
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' }
});
Logger.verbose(
LOGGING_TAG,
`Seach YouTube by video ID (Pass 2): ${youtubeLink.videoId}, Total result: ${
searched2.length
}, ${JSON.stringify(searched2)}`
);
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);
if (videoInfo?.video_details?.title) {
const searched: YouTubeVideo[] = await playdl.search(videoInfo.video_details.title, {
source: { youtube: 'video' }
});
Logger.verbose(
LOGGING_TAG,
`Search YouTube by title (Pass 3): ${videoInfo.video_details.title}, Total result: ${
searched.length
}, ${JSON.stringify(searched)}`
);
for (let video of searched) {
if (video.id === youtubeLink.videoId) return video;
}
}
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,
liveAt: yt_info.video_details.liveAt,
private: yt_info.video_details.private,
tags: yt_info.video_details.tags,
discretionAdvised: yt_info.video_details.discretionAdvised,
music: yt_info.video_details.music
});
}
return null;
}
public async getYouTubeSongsInPlayList(youtubeLink: string) { public async getYouTubeSongsInPlayList(youtubeLink: string) {
const result = await playdl.playlist_info(youtubeLink, { const result = await playdl.playlist_info(youtubeLink, {
incomplete: true incomplete: true
-792
View File
@@ -1,792 +0,0 @@
import {
VoiceChannel,
Snowflake,
StageChannel,
Guild,
PermissionsBitField,
BaseGuildVoiceChannel,
BaseGuildTextChannel
} from 'discord.js';
import {
AudioPlayer,
VoiceConnection,
createAudioPlayer,
joinVoiceChannel,
createAudioResource,
VoiceConnectionStatus,
AudioPlayerStatus,
NoSubscriberBehavior,
VoiceConnectionState,
AudioPlayerError,
DiscordGatewayAdapterCreator
} from '@discordjs/voice';
import playdl, { Spotify, SpotifyPlaylist, SpotifyTrack, YouTubeVideo } from 'play-dl';
import { EventEmitter } from 'stream';
import YTDlpWrap from 'yt-dlp-wrap';
const ytDlpWrap = new YTDlpWrap();
import fs from 'fs';
import DiscordProvider from './Discord';
import Environment from './Environment';
import Logger from '../libs/Logger';
const LOGGING_TAG = '[DiscordMusicPlayer]';
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;
}
interface TokenOptions {
spotify?: {
client_id: string;
client_secret: string;
refresh_token: string;
market: string;
};
soundcloud?: {
client_id: string;
};
youtube?: {
cookie: string;
};
useragent?: string[];
}
let tokenObject: TokenOptions = {};
if (Environment.get().YOUTUBE_COOKIE_BASE64) {
Logger.debug(LOGGING_TAG, 'Setting YouTube cookie');
tokenObject.youtube = {
cookie: Buffer.from(Environment.get().YOUTUBE_COOKIE_BASE64, 'base64').toString()
};
}
if (
Environment.get().SPOTIFY_CLIENT_ID &&
Environment.get().SPOTIFY_CLIENT_SECRET &&
Environment.get().SPOTIFY_REFRESH_TOKEN &&
Environment.get().SPOTIFY_CLIENT_MARKET
) {
Logger.debug(LOGGING_TAG, 'Setting Spotify token');
tokenObject.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
};
}
playdl.setToken(tokenObject);
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
);
}
}
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 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;
}
}
export class PlayerPlayingEvent {
public instance: DiscordMusicPlayerInstance;
constructor(instance: DiscordMusicPlayerInstance) {
this.instance = instance;
}
}
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;
}
}
export enum DiscordMusicPlayerLoopMode {
None = 'none',
Current = 'current'
}
export class DiscordMusicPlayerInstance {
public queue: Queue;
public player: AudioPlayer;
public textChannel?: BaseGuildTextChannel | BaseGuildVoiceChannel;
public voiceChannel: VoiceChannel | StageChannel;
public voiceConnection?: VoiceConnection;
public previousTrack?: ValidTracks;
public paused: boolean = false;
public loopMode: DiscordMusicPlayerLoopMode = DiscordMusicPlayerLoopMode.None;
public actualPlaybackURL?: string;
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) {
// Loop mode is set to current song
if (this.loopMode === DiscordMusicPlayerLoopMode.Current) {
if (this.queue.track.length !== 0) {
this.previousTrack = this.queue.track[0];
this.playTrack(this.queue.track[0]);
}
return;
}
// There are more songs in the queue, remove finished song and play the next one
if (this.queue.track.length !== 0) {
let previousTrack = this.queue.track.shift();
if (previousTrack) this.previousTrack = previousTrack;
if (this.queue.track.length > 0) {
await this.playTrack(this.queue.track[0]);
}
}
}
});
this.player.on(AudioPlayerStatus.Playing, (oldState: any, newState: any) => {
this.events.emit('playing', new PlayerPlayingEvent(this));
});
this.player.on('error', (error: Error) => {
this.events.emit('error', new PlayerErrorEvent(this, error));
});
}
public joinVoiceChannel(
voiceChannel: VoiceChannel | StageChannel,
textChannel?: BaseGuildTextChannel | BaseGuildVoiceChannel
) {
const permissions = voiceChannel.permissionsFor(DiscordProvider.client.user!);
if (!permissions || !voiceChannel.joinable || !permissions.has(PermissionsBitField.Flags.Connect))
throw new Error('No permissions');
if (textChannel) this.textChannel = textChannel;
this.voiceConnection = joinVoiceChannel({
channelId: this.voiceChannel.id,
guildId: this.voiceChannel.guild.id,
adapterCreator: this.voiceChannel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
});
this.voiceConnection.on(
VoiceConnectionStatus.Ready,
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;
}
}
}
);
this.voiceConnection.on(
VoiceConnectionStatus.Disconnected,
(oldState: VoiceConnectionState, newState: VoiceConnectionState) => {
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));
}
}
}, 2000);
}
);
}
public async leaveVoiceChannel() {
if (this.player) this.player.pause();
const guild = DiscordProvider.client.guilds.cache.get(this.voiceChannel.guild.id);
if (!guild) return;
if (guild.members.me?.voice) this.voiceConnection?.disconnect();
}
public async pausePlayer() {
if (this.paused || !this.player) return;
if (!this.player.pause(true)) throw new Error('Unable to pause player');
this.paused = true;
}
public async resumePlayer() {
if (!this.paused || !this.player) return;
if (!this.player.unpause()) throw new Error('Unable to resume player');
this.paused = false;
}
public addTrackToQueue(track: ValidTracks) {
if (this.queue.track.length === 0) {
this.queue.track.push(track);
this.playTrack(this.queue.track[0]);
return;
}
this.queue.track.push(track);
}
public async playTrack(track: ValidTracks) {
if (!this.voiceConnection) throw new Error('No voice connection');
try {
let resource;
if (track instanceof YouTubeVideo) {
await ytDlpWrap.execPromise([
track.url,
'--extractor-args',
'youtube:player_client=ios',
'-f',
'140',
'-o',
`./temp/${track.id}.mp3`
]);
const streamFile = fs.createReadStream(`./temp/${track.id}.mp3`);
//const stream = await playdl.stream(track.url);
const stream = streamFile;
// Logger.verbose(
// LOGGING_TAG,
// `New stream created, type: ${stream.type}, url: ${track.url}, Guild: ${this.voiceChannel.guild.id}, VoiceChannel: ${this.voiceChannel.id}`
// );
resource = createAudioResource(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');
await ytDlpWrap.execPromise([
search.url,
'--extractor-args',
'youtube:player_client=ios',
'-f',
'140',
'-o',
`./temp/${search.id}.mp3`
]);
const streamFile = fs.createReadStream(`./temp/${search.id}.mp3`);
//const stream = await playdl.stream(search.url);
const stream = streamFile;
// Logger.verbose(
// LOGGING_TAG,
// `New stream created, type: ${stream.type}, url: ${track.url}, Guild: ${this.voiceChannel.guild.id}, VoiceChannel: ${this.voiceChannel.id}`
// );
resource = createAudioResource(stream, {
//inputType: stream.type
});
this.actualPlaybackURL = search.url;
}
this.player.play(resource);
this.voiceConnection.subscribe(this.player);
let timeout = 0;
while (!resource.started && timeout < 10000) {
await new Promise((resolve) => setTimeout(resolve, 100));
timeout += 100;
if (timeout >= 10000)
this.events.emit('error', new PlayerErrorEvent(this, new Error('Resource took too long to start')));
}
} catch (error: any) {
this.events.emit('error', new PlayerErrorEvent(this, error));
this.skipTrack();
}
}
public async skipTrack() {
if (!this.voiceConnection) throw new Error('No voice connection');
if (this.queue.track.length > 1) {
this.previousTrack = this.queue.track[0];
this.queue.track.shift();
this.playTrack(this.queue.track[0]);
} else {
this.previousTrack = this.queue.track[0];
this.queue.track.shift();
this.player.stop();
}
}
public clearQueue() {
if (!this.voiceConnection) throw new Error('No voice connection');
if (!this.queue.track || this.queue.track.length === 0) return;
this.queue.track = [this.queue.track[0]];
}
public shuffleQueue() {
if (!this.voiceConnection) throw new Error('No voice connection');
const shuffleFixedFirst = (queue: ValidTracks[]) => {
if (queue.length <= 2) return queue;
const fixedFirst = queue.shift();
if (!fixedFirst) return queue;
queue.sort(() => Math.random() - 0.5);
queue.unshift(fixedFirst);
return queue;
};
if (!this.queue.track || this.queue.track.length === 0) return;
this.queue.track = shuffleFixedFirst(this.queue.track);
}
public setLoopMode(mode: DiscordMusicPlayerLoopMode) {
this.loopMode = mode;
}
public getLoopMode() {
return this.loopMode;
}
public getPreviousTrack(): ValidTracks | undefined {
return this.previousTrack;
}
public getActualPlaybackURL() {
return this.actualPlaybackURL;
}
public isReady() {
if (!this.voiceConnection) return false;
return this.voiceConnection.state.status === VoiceConnectionStatus.Ready;
}
public isConnected() {
if (!this.voiceConnection) return false;
if (this.voiceConnection.state.status === VoiceConnectionStatus.Destroyed) return false;
if (this.voiceConnection.state.status === VoiceConnectionStatus.Disconnected) return false;
return true;
}
public isPaused() {
return this.paused;
}
public async destroy() {
await this.leaveVoiceChannel();
if (this.voiceConnection) {
this.voiceConnection.removeAllListeners();
if (this.voiceConnection.state.status !== 'destroyed') this.voiceConnection.destroy();
}
if (this.player) {
this.player.removeAllListeners();
this.player.stop(true);
}
this.queue.track = [];
this.textChannel = undefined;
this.voiceConnection = undefined;
}
public async _fake_error_on_player() {
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!));
}
}
class DiscordMusicPlayer {
public GuildQueue = new Map();
public getGuildInstance(guildId: Snowflake): DiscordMusicPlayerInstance | null {
if (!this.isGuildInstanceExists(guildId)) return null;
return this.GuildQueue.get(guildId);
}
public isGuildInstanceExists(guildId: Snowflake) {
return this.GuildQueue.has(guildId);
}
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;
if (this.isGuildInstanceExists(guildId)) {
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' }
});
Logger.verbose(
LOGGING_TAG,
`Search YouTube by query: ${query}, Total result: ${searched.length}, ${JSON.stringify(searched)}`
);
if (searched.length == 0) return null;
return searched;
}
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()) {
Logger.debug(LOGGING_TAG, 'Spotify token expired, refreshing...');
await playdl.refreshToken();
}
if (spotifyLink.type != 'track') return;
// Fetch data from spotify
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');
});
Logger.verbose(LOGGING_TAG, `Search Spotify by link: ${spotifyLink.id}, ${JSON.stringify(searched)}`);
if (!(searched instanceof SpotifyTrack)) return;
if (!searched) return null;
const track = searched as unknown as SpotifyTrack;
return track;
}
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' }
});
Logger.verbose(
LOGGING_TAG,
`Search YouTube by link (Pass 1): ${youtubeLink.videoId}, Total result: ${
searched.length
}, ${JSON.stringify(searched)}`
);
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' }
});
Logger.verbose(
LOGGING_TAG,
`Seach YouTube by video ID (Pass 2): ${youtubeLink.videoId}, Total result: ${
searched2.length
}, ${JSON.stringify(searched2)}`
);
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);
if (videoInfo?.video_details?.title) {
const searched: YouTubeVideo[] = await playdl.search(videoInfo.video_details.title, {
source: { youtube: 'video' }
});
Logger.verbose(
LOGGING_TAG,
`Search YouTube by title (Pass 3): ${videoInfo.video_details.title}, Total result: ${
searched.length
}, ${JSON.stringify(searched)}`
);
for (let video of searched) {
if (video.id === youtubeLink.videoId) return video;
}
}
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,
liveAt: yt_info.video_details.liveAt,
private: yt_info.video_details.private,
tags: yt_info.video_details.tags,
discretionAdvised: yt_info.video_details.discretionAdvised,
music: yt_info.video_details.music
});
}
return null;
}
public async getYouTubeSongsInPlayList(youtubeLink: string) {
const result = await playdl.playlist_info(youtubeLink, {
incomplete: true
});
Logger.verbose(LOGGING_TAG, `Get YouTube songs in playlist: ${youtubeLink}, ${JSON.stringify(result)}`);
return result;
}
public async getSpotifySongsInPlayList(spotifyLink: string) {
if (playdl.is_expired()) {
Logger.debug(LOGGING_TAG, 'Spotify token expired, refreshing...');
await playdl.refreshToken();
}
const result = await playdl.spotify(spotifyLink).catch((err) => {
Logger.error(err.message);
throw new Error('Error while searching on Spotify');
});
Logger.verbose(LOGGING_TAG, `Get Spotify songs in playlist: ${spotifyLink}, ${JSON.stringify(result)}`);
if (!(result.type == 'playlist' || result.type == 'album')) throw new Error('Not a spotify playlist');
return result as unknown as SpotifyPlaylist;
}
public isYouTubeLink(link: string): boolean {
try {
this.parseYouTubeLink(link);
return true;
} catch (err) {
return false;
}
}
public isSpotifyLink(link: string): boolean {
try {
this.parseSpotifyLink(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=') ||
query.startsWith('https://music.youtube.com/watch?v=') ||
query.startsWith('https://music.youtube.com/watch?v=')
) {
let data = this.parseURLQuery(query);
if (!data.v) throw new Error('YouTube link is invalid');
return {
videoId: data.v,
list: data.list ? (data.list !== 'RDMM' ? data.list : undefined) : undefined
};
} 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
};
} else if (
query.startsWith('https://www.youtube.com/shorts/') ||
query.startsWith('http://www.youtube.com/shorts/')
) {
let videoId = query.split('/')[4];
if (!videoId) throw new Error('YouTube link is invalid');
if (query.split('/')[5]) throw new Error('YouTube link is invalid');
return {
videoId: videoId
};
} 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
};
} else {
throw new Error('YouTube link is invalid');
}
}
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');
}
}
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;