mirror of
https://github.com/kirameki-cafe/Yumi.git
synced 2026-09-13 10:49:20 +00:00
✨ feat!: Switched to NekoMelody for music
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "NekoMelody"]
|
||||||
|
path = NekoMelody
|
||||||
|
url = https://github.com/YuzuZensai/NekoMelody.git
|
||||||
Submodule
+1
Submodule NekoMelody added at 45a4a9ff64
+63
-60
@@ -1,62 +1,65 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/opus": "^0.9.0",
|
"@discordjs/opus": "^0.9.0",
|
||||||
"@discordjs/rest": "^2.3.0",
|
"@discordjs/rest": "^2.3.0",
|
||||||
"@discordjs/voice": "^0.17.0",
|
"@discordjs/voice": "^0.17.0",
|
||||||
"@types/color-thief-node": "^1.0.4",
|
"@types/color-thief-node": "^1.0.4",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/os-utils": "^0.0.4",
|
"@types/os-utils": "^0.0.4",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"bufferutil": "^4.0.8",
|
"bufferutil": "^4.0.8",
|
||||||
"color-thief-node": "^1.0.4",
|
"color-thief-node": "^1.0.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"country-code-emoji": "^2.3.0",
|
"country-code-emoji": "^2.3.0",
|
||||||
"country-code-lookup": "^0.1.3",
|
"country-code-lookup": "^0.1.3",
|
||||||
"discord-api-types": "^0.37.89",
|
"discord-api-types": "^0.37.89",
|
||||||
"discord.js": "^14.15.3",
|
"discord.js": "^14.15.3",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"i18n": "^0.15.1",
|
"i18n": "^0.15.1",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"node-osu": "^2.2.1",
|
"node-osu": "^2.2.1",
|
||||||
"os-utils": "^0.0.14",
|
"os-utils": "^0.0.14",
|
||||||
"ping": "^0.4.4",
|
"ping": "^0.4.4",
|
||||||
"play-dl": "^1.9.7",
|
"play-dl": "^1.9.7",
|
||||||
"ts-custom-error": "^3.3.1",
|
"ts-custom-error": "^3.3.1",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"utf-8-validate": "^6.0.4",
|
"utf-8-validate": "^6.0.4",
|
||||||
"validator": "^13.12.0",
|
"validator": "^13.12.0",
|
||||||
"vrchat": "^1.17.6",
|
"vrchat": "^1.17.6",
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
"yt-dlp-wrap": "^2.3.12",
|
"yt-dlp-wrap": "^2.3.12",
|
||||||
"zlib-sync": "^0.1.9"
|
"zlib-sync": "^0.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@prisma/client": "^5.15.0",
|
"@discordjs/builders": "^1.8.2",
|
||||||
"@types/bluebird": "^3.5.42",
|
"@prisma/client": "^5.15.0",
|
||||||
"@types/i18n": "^0.13.12",
|
"@types/bluebird": "^3.5.42",
|
||||||
"@types/node": "^20.14.2",
|
"@types/i18n": "^0.13.12",
|
||||||
"@types/ping": "^0.4.4",
|
"@types/node": "^20.14.2",
|
||||||
"@types/validator": "^13.11.10",
|
"@types/ping": "^0.4.4",
|
||||||
"copyfiles": "^2.4.1",
|
"@types/validator": "^13.11.10",
|
||||||
"prisma": "^5.15.0",
|
"copyfiles": "^2.4.1",
|
||||||
"ts-node-dev": "^2.0.0"
|
"prisma": "^5.15.0",
|
||||||
},
|
"ts-node-dev": "^2.0.0",
|
||||||
"scripts": {
|
"tsup": "^8.2.4"
|
||||||
"tsc": "tsc",
|
},
|
||||||
"development": "ts-node-dev ./src/index.ts --respawn --transpileOnly",
|
"scripts": {
|
||||||
"build": "tsc -b && copyfiles -u 1 configs_example/**/*.* dist/configs_example/",
|
"tsc": "tsc",
|
||||||
"docker-build": "docker build . -t ghcr.io/kirameki-cafe/yumi:latest",
|
"development": "ts-node-dev ./src/index.ts --respawn --transpileOnly",
|
||||||
"docker-run": "docker run -d --name yumi --mount type=bind,source=\"$(pwd)\"/configs,target=/home/node/app/configs --network host --env-file .env ghcr.io/kirameki-cafe/yumi:latest",
|
"build": "tsc -b && copyfiles -u 1 configs_example/**/*.* dist/configs_example/",
|
||||||
"docker-push": "docker push ghcr.io/kirameki-cafe/yumi:latest",
|
"docker-build": "docker build . -t ghcr.io/kirameki-cafe/yumi:latest",
|
||||||
"production": "tsc && node ./dist/index.js",
|
"docker-run": "docker run -d --name yumi --mount type=bind,source=\"$(pwd)\"/configs,target=/home/node/app/configs --network host --env-file .env ghcr.io/kirameki-cafe/yumi:latest",
|
||||||
"test": "mocha -r ts-node/register tests/**/*.ts"
|
"docker-push": "docker push ghcr.io/kirameki-cafe/yumi:latest",
|
||||||
},
|
"production": "tsc && node ./dist/index.js",
|
||||||
"name": "yumi",
|
"test": "mocha -r ts-node/register tests/**/*.ts"
|
||||||
"version": "1.0.0",
|
},
|
||||||
"main": "index.js",
|
"name": "yumi",
|
||||||
"repository": "https://github.com/kirameki-cafe/Yumi.git",
|
"version": "1.0.0",
|
||||||
"author": "Kirameki Café <contact@kirameki.cafe>",
|
"main": "index.js",
|
||||||
"license": "GPL-3.0"
|
"repository": "https://github.com/kirameki-cafe/Yumi.git",
|
||||||
|
"author": "Kirameki Café <contact@kirameki.cafe>",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3766
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 ||
|
row.addComponents([
|
||||||
event.instance.queue.track[0] instanceof SpotifyTrack
|
new ButtonBuilder()
|
||||||
) {
|
.setEmoji('🔴')
|
||||||
const actualPlaybackURL = event.instance.getActualPlaybackURL();
|
.setLabel(' Open in YouTube')
|
||||||
if (event.instance.queue.track[0] instanceof SpotifyTrack && !actualPlaybackURL) return;
|
.setURL(current.metadata.url)
|
||||||
row.addComponents([
|
.setStyle(ButtonStyle.Link)
|
||||||
new ButtonBuilder()
|
]);
|
||||||
.setEmoji('🔴')
|
|
||||||
.setLabel(' Open in YouTube')
|
|
||||||
.setURL(
|
|
||||||
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)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,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';
|
||||||
|
|||||||
@@ -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)]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
row.addComponents([
|
||||||
if (instance.queue.track[0] instanceof SpotifyTrack && !actualPlaybackURL) return;
|
new ButtonBuilder()
|
||||||
row.addComponents([
|
.setEmoji('🔴')
|
||||||
new ButtonBuilder()
|
.setLabel(' Open in YouTube')
|
||||||
.setEmoji('🔴')
|
.setURL(current.metadata.url)
|
||||||
.setLabel(' Open in YouTube')
|
.setStyle(ButtonStyle.Link)
|
||||||
.setURL(
|
]);
|
||||||
instance.queue.track[0] instanceof YouTubeVideo
|
|
||||||
? encodeURI(`https://www.youtube.com/watch?v=${instance.queue.track[0].id}`)
|
|
||||||
: encodeURI(instance.getActualPlaybackURL()!)
|
|
||||||
)
|
|
||||||
.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]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,17 +348,15 @@ 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}`)
|
||||||
return null;
|
.catch((err) => {
|
||||||
});
|
placeholder.getMessage().edit({ embeds: [EMBEDS.LOOKUP_ERROR(data, locale, err)] });
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
@@ -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)] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,59 +18,59 @@ 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 = `${
|
|
||||||
instance.getLoopMode() === DiscordMusicPlayerLoopMode.Current
|
|
||||||
? ` ${locale.__('musicplayer_queue.looping_current')}`
|
|
||||||
: ''
|
|
||||||
}: [${TrackUtils.getTitle(queue.track[0])}](${queue.track[0].url})`;
|
|
||||||
|
|
||||||
let description;
|
const nowPlayingText = `${
|
||||||
if (queue.track.length == 1) {
|
instance.getLoopMode() === DiscordMusicPlayerLoopMode.Current
|
||||||
let queueString = `1. [${TrackUtils.getTitle(queue.track[0])}](${queue.track[0].url})`;
|
? ` ${locale.__('musicplayer_queue.looping_current')}`
|
||||||
description = `${locale.__('musicplayer_queue.now_playing')}${nowPlayingText}\n
|
: ''
|
||||||
There are ${queue.track.length} song in the queue!\n${queueString}`;
|
}: [${current.metadata.title}](${current.metadata.url})`;
|
||||||
} else if (queue.track.length >= 10) {
|
|
||||||
const upcomingText = `[${TrackUtils.getTitle(queue.track[1])}](${queue.track[1].url})`;
|
let description;
|
||||||
const first10 = queue.track.slice(0, 10);
|
if (queue.length == 0) {
|
||||||
let queueString = first10
|
let queueString = `1. [${current.metadata.title}](${current.metadata.url})`;
|
||||||
.map((track, index) => `${index + 1}. [${TrackUtils.getTitle(track)}](${track.url})`)
|
description = `${locale.__('musicplayer_queue.now_playing')}${nowPlayingText}\n
|
||||||
.join('\n');
|
${locale.__('musicplayer_queue.empty')}`;
|
||||||
description = `${locale.__('musicplayer_queue.now_playing')}${nowPlayingText}
|
} else if (queue.length >= 10) {
|
||||||
|
const upcomingText = `[${queue[0].metadata.title}](${queue[0].metadata.url})`;
|
||||||
|
const first10 = queue.slice(0, 10);
|
||||||
|
let queueString = first10
|
||||||
|
.map((track, index) => `${index + 1}. [${track.metadata.title}](${track.metadata.url})`)
|
||||||
|
.join('\n');
|
||||||
|
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}`;
|
||||||
}
|
|
||||||
|
|
||||||
return makeInfoEmbed({
|
|
||||||
title: locale.__('musicplayer_queue.title'),
|
|
||||||
description: description,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: locale.__('common.available_args'),
|
|
||||||
value: locale.__('musicplayer_queue.valid_args')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
user: data.getUser()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return makeInfoEmbed({
|
||||||
|
title: locale.__('musicplayer_queue.title'),
|
||||||
|
description: description,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: locale.__('common.available_args'),
|
||||||
|
value: locale.__('musicplayer_queue.valid_args')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
user: data.getUser()
|
||||||
|
});
|
||||||
},
|
},
|
||||||
QUEUE_CLEARED: (data: HybridInteractionMessage, locale: I18n) => {
|
QUEUE_CLEARED: (data: HybridInteractionMessage, locale: I18n) => {
|
||||||
return makeSuccessEmbed({
|
return makeSuccessEmbed({
|
||||||
|
|||||||
@@ -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)]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,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)]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.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.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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
Reference in New Issue
Block a user