🐛 fix: Temp patch broken music player

This commit is contained in:
2024-06-16 00:34:31 +07:00
parent a47a1d0c27
commit 34d5ce00d3
19 changed files with 1021 additions and 199 deletions
+1
View File
@@ -2,6 +2,7 @@
/build
/configs
/temp
# compiled output
/dist
+186 -185
View File
@@ -1,60 +1,60 @@
{
"common": {
"available_args": "Available arguments",
"member_no_permissions": "Access denied (O_O;)",
"member_no_permissions_description": "You need ``{{PERMISSIONS}}`` permissions on this guild",
"bot_no_permissions": "I don't have permission (O_O;)",
"bot_no_permissions_description": "I need ``{{PERMISSIONS}}`` permissions",
"processing": "Please wait a moment~",
"processing_description": "Getting it done right now! (งᓀ‸ᓂ)ง",
"early_access_features": "Early access features",
"early_access_features_description": "Hey there! So, this new feature we've got is still in its early access phase. We're working hard to test and make it even better. But, just a heads up, early access means there might be some surprises like unexpected stuff, security things, or features that are still a work in progress.\n\nIf you're up for it, guild administrators can jump in and try it out by opting in through the settings"
},
"early_access_warning": {
"title": "Early access warning",
"description": "Experience the newest features first with early access! Things might not be perfect yet, but your feedback helps us make it amazing. Contact us with the support command if you find anything\n\n You can disable early access features anytime in the settings"
},
"stats": {
"title": "Stats",
"info": "Statistical data of me",
"users": "Users",
"x_servers": "{{COUNT}} servers",
"x_users": "{{COUNT}} users",
"uptime_since": "Uptime since",
"server_uptime_x": "Server | {{{TIME}}}",
"process_uptime_x": "Process | {{{TIME}}}"
},
"userinfo": {
"title": "User Information",
"info": "Show information of a specific user, such as status, activities, and more",
"valid_args": "(``@Mention or User ID``)",
"user_not_found": "I cannot find that user, is your information correct?",
"online": "Online",
"idle": "Idle",
"dnd": "Do not disturb",
"offline": "Recently Offline",
"unknown": "Offline / Unknown",
"user_guild_info": "Information on this guild",
"join_date_unknown": "Unknown join date",
"join_date": "Joined {{{TIME}}}",
"guild_owner": "Owner of this guild",
"custom_status": "Custom Status",
"playing_x": "Playing {{NAME}}",
"streaming_x": "Streaming {{NAME}}",
"listening_x": "Listening to {{NAME}}",
"watching_x": "Watching {{NAME}}",
"competing_x": "Competing {{NAME}}",
"since": "Since {{{TIME}}}",
"end": "Ending {{{TIME}}}"
},
"say": {
"title": "Say",
"info": "Make me say something that you want~",
"valid_args": "``(Text message)``",
"success": "Done~, I posted a message with your content!",
"error": "An error occurred while trying to post the message with your content!"
},
"purge": {
"common": {
"available_args": "Available arguments",
"member_no_permissions": "Access denied (O_O;)",
"member_no_permissions_description": "You need ``{{PERMISSIONS}}`` permissions on this guild",
"bot_no_permissions": "I don't have permission (O_O;)",
"bot_no_permissions_description": "I need ``{{PERMISSIONS}}`` permissions",
"processing": "Please wait a moment~",
"processing_description": "Getting it done right now! (งᓀ‸ᓂ)ง",
"early_access_features": "Early access features",
"early_access_features_description": "Hey there! So, this new feature we've got is still in its early access phase. We're working hard to test and make it even better. But, just a heads up, early access means there might be some surprises like unexpected stuff, security things, or features that are still a work in progress.\n\nIf you're up for it, guild administrators can jump in and try it out by opting in through the settings"
},
"early_access_warning": {
"title": "Early access warning",
"description": "Experience the newest features first with early access! Things might not be perfect yet, but your feedback helps us make it amazing. Contact us with the support command if you find anything\n\n You can disable early access features anytime in the settings"
},
"stats": {
"title": "Stats",
"info": "Statistical data of me",
"users": "Users",
"x_servers": "{{COUNT}} servers",
"x_users": "{{COUNT}} users",
"uptime_since": "Uptime since",
"server_uptime_x": "Server | {{{TIME}}}",
"process_uptime_x": "Process | {{{TIME}}}"
},
"userinfo": {
"title": "User Information",
"info": "Show information of a specific user, such as status, activities, and more",
"valid_args": "(``@Mention or User ID``)",
"user_not_found": "I cannot find that user, is your information correct?",
"online": "Online",
"idle": "Idle",
"dnd": "Do not disturb",
"offline": "Recently Offline",
"unknown": "Offline / Unknown",
"user_guild_info": "Information on this guild",
"join_date_unknown": "Unknown join date",
"join_date": "Joined {{{TIME}}}",
"guild_owner": "Owner of this guild",
"custom_status": "Custom Status",
"playing_x": "Playing {{NAME}}",
"streaming_x": "Streaming {{NAME}}",
"listening_x": "Listening to {{NAME}}",
"watching_x": "Watching {{NAME}}",
"competing_x": "Competing {{NAME}}",
"since": "Since {{{TIME}}}",
"end": "Ending {{{TIME}}}"
},
"say": {
"title": "Say",
"info": "Make me say something that you want~",
"valid_args": "``(Text message)``",
"success": "Done~, I posted a message with your content!",
"error": "An error occurred while trying to post the message with your content!"
},
"purge": {
"title": "Purge",
"info": "Delete a certain amount of messages",
"valid_args": "``(Number of messages)``",
@@ -70,133 +70,134 @@
"purging_step_normal_delete_done": "Deleted {{COUNT}} messages",
"error": "An error occurred while purging messages"
},
"invite": {
"title": "Invite",
"info": "[Invite {{BOT_NAME}} to your server!]({{{LINK}}})",
"private": "{{BOT_NAME}}'s invite is currently private. Only the developers can add me to another server"
},
"help": {
"title": "Help - {{BOT_NAME}}",
"prefix_title": "Prefix",
"prefix_description": "You can call me using ``{{{PREFIX}}}``, {{{PREFIX_MENTION}}} or ``/slash command``",
"available_commands": "Available commands",
"general": "General",
"music": "Music",
"games": "Games",
"admin": "Admin",
"developer": "Developer",
"footer": "Made with 💖 and [open source]"
},
"musicplayer": {
"now_playing": "Now playing",
"now_playing_repeating": "Now playing (Repeating)",
"no_music_playing": "There are no music playing (ᴗ˳ᴗ);",
"not_in_voice": "You need to be in the voice channel first~",
"different_voice_channel": "You are not in the same voice channel as me!",
"track_error": "Error while playing music, skipping this track"
},
"musicplayer_join": {
"success": "Successfully joined the voice channel, I'm here now~",
"error": "Unable to join the voice channel",
"disconnected": "Disconnected",
"disconnected_reason_disconnected": "I got disconnected from the voice channel",
"in_use": "I can't join mutiple voice channel on the same guild.\n There are also somebody listening to the music in the voice channel I'm currently in.",
"in_use_wait": "Wait until I finish playing there or I'm alone there. Better yet, join them!",
"can_force_move": "\n\n**Or... just (ab)use your admin permissions and move me to where you want!**",
"already_joined_title": "I'm already here~ (* ^ ω ^)",
"already_joined_description": "Already in the voice channel you are currently connected to"
},
"musicplayer_leave": {
"success": "Successfully left the voice channel, see ya!"
},
"musicplayer_loop": {
"info": "Current loop mode is {{MODE}}",
"valid_args": "Available loop mode: ``None`` ``Current``",
"invalid": "Invalid loop mode",
"loop_set": "Loop mode is now set to {{MODE}}"
},
"musicplayer_pause": {
"paused": "Paused",
"already_paused": "Already paused (´。• ω •。`)"
},
"musicplayer_resume": {
"resumed": "Resumed",
"not_paused": "The music is already playing (´。• ᵕ •。`)"
},
"musicplayer_play": {
"title": "Play",
"info": "Play a song",
"valid_args": "``(Search query or Link)``",
"queue_added_song": "Song added to queue! ヽ(>∀<☆)",
"queue_added_songs": "Songs added to queue! ヽ(>∀<☆)",
"queue_added_songs_description": "Added {{COUNT}} songs to the queue",
"error": "Error while looking up the song"
},
"musicplayer_playmy": {
"not_detected": "You don't seem to be listening to any music currently according to your activity"
},
"musicplayer_queue": {
"title": "Queue",
"valid_args": "``Clear`` ``Shuffle``",
"empty": "The queue is empty! (っ˘ω˘ς )",
"now_playing": "Now playing",
"upcoming_song": "Upcoming song",
"looping_current": "(Looping current)",
"song_x_in_queue": "There are {{COUNT}} song in the queue!",
"cleared": "Queue cleared~ (。•ˇ‸ˇ•。)",
"shuffled": "Queue shuffled"
},
"musicplayer_search": {
"title": "Search",
"info": "Search for a song",
"valid_args": "``(Search query)``",
"title_result": "Search result",
"x_results_found": "{{COUNT}} results found"
},
"musicplayer_skip": {
"skipped": "Skipped",
"skipped_last_song": "Skipped, that was the last song"
},
"settings": {
"title": "Settings",
"description": "Change how {{BOT_NAME}} behave on **{{{GUILD_NAME}}}**",
"valid_args": "``Prefix`` ``Language`` ``EnableServiceAnnouncement`` ``ServiceAnnouncementChannel`` ``EarlyAccess`` ``PurgeLimit``"
},
"settings_prefix": {
"info": "Current prefix is {{{PREFIX}}}",
"info_description": "Provide a new prefix to change",
"prefix_too_long": "Prefix too long",
"prefix_too_long_description": "Can you even remember that? 😆",
"prefix_is_mention": "Prefix cannot be @Mention of me",
"prefix_is_mention_description": "You can already call me by mentioning me. I got that covered, don't worry",
"prefix_updated": "Prefix updated",
"prefix_updated_description": "From now onwards, I shall be called by using ``{{{PREFIX}}}``"
},
"settings_purge_limit": {
"info": "Current purge limit is {{LIMIT}}",
"info_description": "Provide a new purge limit to change",
"purge_limit_too_big": "Purge limit too big, maximum is {{LIMIT}}",
"purge_limit_zero": "Purge limit can't be zero",
"purge_limit_nan": "Purge limit is not a number",
"purge_limit_updated": "Purge limit updated to {{LIMIT}}"
},
"settings_language": {
"info": "Current language is {{LANGUAGE}}",
"info_description": "Provide a new language to change\n**Available languages**: ``{{LANGUAGES}}``",
"language_invalid": "Invalid language",
"language_invalid_description": "The language code is invalid or not supported yet",
"language_updated": "Language updated",
"language_updated_description": "I will be using {{LANGUAGE}} from now on"
},
"ping": {
"title": "Network performance",
"title_processing": "Measuring network performance ( ◕▿◕ )",
"running_on": "Running on {{HOST}}",
"development_environment": "Development Environment"
},
"support": {
"title": "Support",
"info": "[Join {{BOT_NAME}}'s support server]({{{LINK}}})",
"not_available": "{{BOT_NAME}}'s support server is currently unavailable"
}
"invite": {
"title": "Invite",
"info": "[Invite {{BOT_NAME}} to your server!]({{{LINK}}})",
"private": "{{BOT_NAME}}'s invite is currently private. Only the developers can add me to another server"
},
"help": {
"title": "Help - {{BOT_NAME}}",
"prefix_title": "Prefix",
"prefix_description": "You can call me using ``{{{PREFIX}}}``, {{{PREFIX_MENTION}}} or ``/slash command``",
"available_commands": "Available commands",
"general": "General",
"music": "Music",
"games": "Games",
"admin": "Admin",
"developer": "Developer",
"footer": "Made with 💖 and [open source]"
},
"musicplayer": {
"now_playing": "Now playing",
"now_playing_repeating": "Now playing (Repeating)",
"no_music_playing": "There are no music playing (ᴗ˳ᴗ);",
"not_in_voice": "You need to be in the voice channel first~",
"different_voice_channel": "You are not in the same voice channel as me!",
"track_error": "Error while playing music, skipping this track"
},
"musicplayer_join": {
"success": "Successfully joined the voice channel, I'm here now~",
"error": "Unable to join the voice channel",
"disconnected": "Disconnected",
"disconnected_reason_disconnected": "I got disconnected from the voice channel",
"in_use": "I can't join mutiple voice channel on the same guild.\n There are also somebody listening to the music in the voice channel I'm currently in.",
"in_use_wait": "Wait until I finish playing there or I'm alone there. Better yet, join them!",
"can_force_move": "\n\n**Or... just (ab)use your admin permissions and move me to where you want!**",
"already_joined_title": "I'm already here~ (* ^ ω ^)",
"already_joined_description": "Already in the voice channel you are currently connected to"
},
"musicplayer_leave": {
"success": "Successfully left the voice channel, see ya!"
},
"musicplayer_loop": {
"info": "Current loop mode is {{MODE}}",
"valid_args": "Available loop mode: ``None`` ``Current``",
"invalid": "Invalid loop mode",
"loop_set": "Loop mode is now set to {{MODE}}"
},
"musicplayer_pause": {
"paused": "Paused",
"already_paused": "Already paused (´。• ω •。`)"
},
"musicplayer_resume": {
"resumed": "Resumed",
"not_paused": "The music is already playing (´。• ᵕ •。`)"
},
"musicplayer_play": {
"title": "Play",
"info": "Play a song",
"valid_args": "``(Search query or Link)``",
"queue_added_song": "Song added to queue! ヽ(>∀<☆)",
"queue_added_songs": "Songs added to queue! ヽ(>∀<☆)",
"queue_added_songs_description": "Added {{COUNT}} songs to the queue",
"error": "Error while looking up the song"
},
"musicplayer_playmy": {
"not_detected": "You don't seem to be listening to any music currently according to your activity",
"too_long": "musicplayer_playmy.too_long"
},
"musicplayer_queue": {
"title": "Queue",
"valid_args": "``Clear`` ``Shuffle``",
"empty": "The queue is empty! (っ˘ω˘ς )",
"now_playing": "Now playing",
"upcoming_song": "Upcoming song",
"looping_current": "(Looping current)",
"song_x_in_queue": "There are {{COUNT}} song in the queue!",
"cleared": "Queue cleared~ (。•ˇ‸ˇ•。)",
"shuffled": "Queue shuffled"
},
"musicplayer_search": {
"title": "Search",
"info": "Search for a song",
"valid_args": "``(Search query)``",
"title_result": "Search result",
"x_results_found": "{{COUNT}} results found"
},
"musicplayer_skip": {
"skipped": "Skipped",
"skipped_last_song": "Skipped, that was the last song"
},
"settings": {
"title": "Settings",
"description": "Change how {{BOT_NAME}} behave on **{{{GUILD_NAME}}}**",
"valid_args": "``Prefix`` ``Language`` ``EnableServiceAnnouncement`` ``ServiceAnnouncementChannel`` ``EarlyAccess`` ``PurgeLimit``"
},
"settings_prefix": {
"info": "Current prefix is {{{PREFIX}}}",
"info_description": "Provide a new prefix to change",
"prefix_too_long": "Prefix too long",
"prefix_too_long_description": "Can you even remember that? 😆",
"prefix_is_mention": "Prefix cannot be @Mention of me",
"prefix_is_mention_description": "You can already call me by mentioning me. I got that covered, don't worry",
"prefix_updated": "Prefix updated",
"prefix_updated_description": "From now onwards, I shall be called by using ``{{{PREFIX}}}``"
},
"settings_purge_limit": {
"info": "Current purge limit is {{LIMIT}}",
"info_description": "Provide a new purge limit to change",
"purge_limit_too_big": "Purge limit too big, maximum is {{LIMIT}}",
"purge_limit_zero": "Purge limit can't be zero",
"purge_limit_nan": "Purge limit is not a number",
"purge_limit_updated": "Purge limit updated to {{LIMIT}}"
},
"settings_language": {
"info": "Current language is {{LANGUAGE}}",
"info_description": "Provide a new language to change\n**Available languages**: ``{{LANGUAGES}}``",
"language_invalid": "Invalid language",
"language_invalid_description": "The language code is invalid or not supported yet",
"language_updated": "Language updated",
"language_updated_description": "I will be using {{LANGUAGE}} from now on"
},
"ping": {
"title": "Network performance",
"title_processing": "Measuring network performance ( ◕▿◕ )",
"running_on": "Running on {{HOST}}",
"development_environment": "Development Environment"
},
"support": {
"title": "Support",
"info": "[Join {{BOT_NAME}}'s support server]({{{LINK}}})",
"not_available": "{{BOT_NAME}}'s support server is currently unavailable"
}
}
+1
View File
@@ -29,6 +29,7 @@
"validator": "^13.12.0",
"vrchat": "^1.17.6",
"winston": "^3.13.0",
"yt-dlp-wrap": "^2.3.12",
"zlib-sync": "^0.1.9"
},
"devDependencies": {
+1 -1
View File
@@ -11,7 +11,7 @@ import {
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
import { makeInfoEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix';
import Users from '../../services/Users';
import Cache from '../../providers/Cache';
+1 -1
View File
@@ -16,7 +16,7 @@ import {
makeSuccessEmbed
} from '../../utils/DiscordMessage';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix';
import Users from '../../services/Users';
import Cache from '../../providers/Cache';
import VRChatAPI from '../../providers/VRChatAPI';
+1 -1
View File
@@ -29,7 +29,7 @@ import DiscordMusicPlayer, {
DiscordMusicPlayerInstance,
DiscordMusicPlayerLoopMode,
TrackUtils
} from '../../providers/DiscordMusicPlayer';
} from '../../providers/DiscordMusicPlayerTempFix';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
import Locale from '../../services/Locale';
+1 -1
View File
@@ -1,7 +1,7 @@
import { I18n } from 'i18n';
import { Message, CommandInteraction } from 'discord.js';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix';
import Locale from '../../services/Locale';
import { makeSuccessEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
+1 -1
View File
@@ -1,7 +1,7 @@
import { I18n } from 'i18n';
import { Message, CommandInteraction } from 'discord.js';
import DiscordMusicPlayer, { DiscordMusicPlayerLoopMode } from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer, { DiscordMusicPlayerLoopMode } from '../../providers/DiscordMusicPlayerTempFix';
import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
+1 -1
View File
@@ -3,7 +3,7 @@ import { I18n } from 'i18n';
import { SpotifyTrack, YouTubeVideo } from 'play-dl';
import DiscordProvider from '../../providers/Discord';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayerTempFix';
import Locale from '../../services/Locale';
import { makeErrorEmbed, makeInfoEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
+1 -1
View File
@@ -1,7 +1,7 @@
import { Message, CommandInteraction, Interaction } from 'discord.js';
import { I18n } from 'i18n';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix';
import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
+13 -1
View File
@@ -15,7 +15,7 @@ import { I18n } from 'i18n';
import { joinVoiceChannelProcedure } from './Join';
import DiscordProvider from '../../providers/Discord';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayerTempFix';
import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
@@ -92,6 +92,12 @@ const EMBEDS = {
description: errorMessage,
user: data.getUser()
});
},
TOO_LONG: (data: HybridInteractionMessage, locale: I18n) => {
return makeErrorEmbed({
title: locale.__('musicplayer_play.too_long'),
user: data.getUser()
});
}
};
export default class Play extends DiscordModule {
@@ -455,6 +461,12 @@ export default class Play extends DiscordModule {
}
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) {
// TODO: Find a better logic than this
// If the first result title is exact match with the query or first title contains half the space of the query, it's probably a sentence
+13 -1
View File
@@ -1,7 +1,7 @@
import { I18n } from 'i18n';
import { Message, CommandInteraction, ActivityType, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayerTempFix';
import Locale from '../../services/Locale';
import { makeSuccessEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
@@ -41,6 +41,12 @@ const EMBEDS = {
embed.setImage(TrackUtils.getHighestResolutionThumbnail(thumbnails).url);
return embed;
},
TOO_LONG: (data: HybridInteractionMessage, locale: I18n) => {
return makeErrorEmbed({
title: locale.__('musicplayer_playmy.too_long'),
user: data.getUser()
});
}
};
@@ -109,6 +115,12 @@ export default class PlayMy extends DiscordModule {
if (!result) continue;
instance.addTrackToQueue(result[0]);
// Max 1 hour
if (result[0].durationInSec > 3600)
return await sendHybridInteractionMessageResponse(data, {
embeds: [EMBEDS.TOO_LONG(data, locale)]
});
let row;
if (result.length > 1) {
// TODO: Find a better logic than this
+1 -1
View File
@@ -5,7 +5,7 @@ import DiscordMusicPlayer, {
DiscordMusicPlayerInstance,
DiscordMusicPlayerLoopMode,
TrackUtils
} from '../../providers/DiscordMusicPlayer';
} from '../../providers/DiscordMusicPlayerTempFix';
import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
+1 -1
View File
@@ -1,7 +1,7 @@
import { Message, CommandInteraction, Interaction } from 'discord.js';
import { I18n } from 'i18n';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
import {
+1 -1
View File
@@ -10,7 +10,7 @@ import { I18n } from 'i18n';
import { joinVoiceChannelProcedure } from './Join';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer, { TrackUtils, ValidTracks } from '../../providers/DiscordMusicPlayerTempFix';
import Locale from '../../services/Locale';
import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModule';
+1 -1
View File
@@ -3,7 +3,7 @@ import DiscordModule, { HybridInteractionMessage } from '../../utils/DiscordModu
import { Message, CommandInteraction } from 'discord.js';
import { makeSuccessEmbed, makeErrorEmbed, sendHybridInteractionMessageResponse } from '../../utils/DiscordMessage';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayer';
import DiscordMusicPlayer from '../../providers/DiscordMusicPlayerTempFix';
import { I18n } from 'i18n';
import Locale from '../../services/Locale';
+7
View File
@@ -8,6 +8,13 @@ class Configuration {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
// TODO: Temp folder
const tempdir = path.join(process.cwd(), 'temp/');
if (!fs.existsSync(tempdir)) {
fs.mkdirSync(tempdir);
}
this.copyExampleIfNotExists('Ping.json');
this.copyExampleIfNotExists('ServiceAnnouncement.json');
+783
View File
@@ -0,0 +1,783 @@
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) {
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);
stream.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.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);
stream.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.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() {
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;
+5
View File
@@ -2256,6 +2256,11 @@ yn@3.1.1:
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
yt-dlp-wrap@^2.3.12:
version "2.3.12"
resolved "https://registry.yarnpkg.com/yt-dlp-wrap/-/yt-dlp-wrap-2.3.12.tgz#35601d4e4fad12c35a6c25041a6d6404cf5066d5"
integrity sha512-P8fJ+6M1YjukyJENCTviNLiZ8mokxprR54ho3DsSKPWDcac489OjRiStGEARJr6un6ETS6goTn4CWl/b/rM3aA==
zlib-sync@^0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/zlib-sync/-/zlib-sync-0.1.9.tgz#7075cc257e4551f5d9fc74e24cf2c11e39c7a0d1"