diff --git a/.gitignore b/.gitignore index 34268b3..31b4a69 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /build /configs +/temp # compiled output /dist diff --git a/locales/en.json b/locales/en.json index ea33c8b..58d7e1f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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" + } +} \ No newline at end of file diff --git a/package.json b/package.json index fcc331b..e1c5ddf 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/discord/Developer/Debug.ts b/src/discord/Developer/Debug.ts index 9529346..9bc8e43 100644 --- a/src/discord/Developer/Debug.ts +++ b/src/discord/Developer/Debug.ts @@ -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'; diff --git a/src/discord/Developer/VRChatLogin.ts b/src/discord/Developer/VRChatLogin.ts index 682f0a2..83799ed 100644 --- a/src/discord/Developer/VRChatLogin.ts +++ b/src/discord/Developer/VRChatLogin.ts @@ -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'; diff --git a/src/discord/MusicPlayer/Join.ts b/src/discord/MusicPlayer/Join.ts index 45fa94e..1d12394 100644 --- a/src/discord/MusicPlayer/Join.ts +++ b/src/discord/MusicPlayer/Join.ts @@ -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'; diff --git a/src/discord/MusicPlayer/Leave.ts b/src/discord/MusicPlayer/Leave.ts index b70f856..567205f 100644 --- a/src/discord/MusicPlayer/Leave.ts +++ b/src/discord/MusicPlayer/Leave.ts @@ -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'; diff --git a/src/discord/MusicPlayer/Loop.ts b/src/discord/MusicPlayer/Loop.ts index 2c1f0f8..fc981b2 100644 --- a/src/discord/MusicPlayer/Loop.ts +++ b/src/discord/MusicPlayer/Loop.ts @@ -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'; diff --git a/src/discord/MusicPlayer/NowPlaying.ts b/src/discord/MusicPlayer/NowPlaying.ts index c97e74d..ebebd2d 100644 --- a/src/discord/MusicPlayer/NowPlaying.ts +++ b/src/discord/MusicPlayer/NowPlaying.ts @@ -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'; diff --git a/src/discord/MusicPlayer/Pause.ts b/src/discord/MusicPlayer/Pause.ts index 0576cd4..fcb8ccf 100644 --- a/src/discord/MusicPlayer/Pause.ts +++ b/src/discord/MusicPlayer/Pause.ts @@ -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'; diff --git a/src/discord/MusicPlayer/Play.ts b/src/discord/MusicPlayer/Play.ts index fe29e81..a3719ac 100644 --- a/src/discord/MusicPlayer/Play.ts +++ b/src/discord/MusicPlayer/Play.ts @@ -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 diff --git a/src/discord/MusicPlayer/PlayMy.ts b/src/discord/MusicPlayer/PlayMy.ts index 33457d6..5c54083 100644 --- a/src/discord/MusicPlayer/PlayMy.ts +++ b/src/discord/MusicPlayer/PlayMy.ts @@ -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 diff --git a/src/discord/MusicPlayer/Queue.ts b/src/discord/MusicPlayer/Queue.ts index fd5a15c..abcfc84 100644 --- a/src/discord/MusicPlayer/Queue.ts +++ b/src/discord/MusicPlayer/Queue.ts @@ -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'; diff --git a/src/discord/MusicPlayer/Resume.ts b/src/discord/MusicPlayer/Resume.ts index 1abd89b..7e52bea 100644 --- a/src/discord/MusicPlayer/Resume.ts +++ b/src/discord/MusicPlayer/Resume.ts @@ -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 { diff --git a/src/discord/MusicPlayer/Search.ts b/src/discord/MusicPlayer/Search.ts index c1a8848..f8588f0 100644 --- a/src/discord/MusicPlayer/Search.ts +++ b/src/discord/MusicPlayer/Search.ts @@ -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'; diff --git a/src/discord/MusicPlayer/Skip.ts b/src/discord/MusicPlayer/Skip.ts index e0ba64f..538b32c 100644 --- a/src/discord/MusicPlayer/Skip.ts +++ b/src/discord/MusicPlayer/Skip.ts @@ -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'; diff --git a/src/providers/Configuration.ts b/src/providers/Configuration.ts index a53e29b..8d7522c 100644 --- a/src/providers/Configuration.ts +++ b/src/providers/Configuration.ts @@ -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'); diff --git a/src/providers/DiscordMusicPlayerTempFix.ts b/src/providers/DiscordMusicPlayerTempFix.ts new file mode 100644 index 0000000..471ea34 --- /dev/null +++ b/src/providers/DiscordMusicPlayerTempFix.ts @@ -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; diff --git a/yarn.lock b/yarn.lock index ad0f15f..ffb1dc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"