diff --git a/Assets/playermenu.jfif b/Assets/playermenu.jfif deleted file mode 100644 index bd10207..0000000 Binary files a/Assets/playermenu.jfif and /dev/null differ diff --git a/Assets/playermenu.jpg b/Assets/playermenu.jpg new file mode 100644 index 0000000..68edc0c Binary files /dev/null and b/Assets/playermenu.jpg differ diff --git a/Assets/queuemessage.jpg b/Assets/queuemessage.jpg new file mode 100644 index 0000000..ffcf2f1 Binary files /dev/null and b/Assets/queuemessage.jpg differ diff --git a/Assets/vulkancommands.jfif b/Assets/vulkancommands.jfif deleted file mode 100644 index 588aaf6..0000000 Binary files a/Assets/vulkancommands.jfif and /dev/null differ diff --git a/Assets/vulkancommands.jpg b/Assets/vulkancommands.jpg new file mode 100644 index 0000000..13537db Binary files /dev/null and b/Assets/vulkancommands.jpg differ diff --git a/Config/Configs.py b/Config/Configs.py index 7cd2ae3..d8d6d4b 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -19,12 +19,14 @@ class VConfigs(Singleton): self.CLEANER_MESSAGES_QUANT = 5 self.ACQUIRE_LOCK_TIMEOUT = 10 + self.QUEUE_VIEW_TIMEOUT = 120 self.COMMANDS_FOLDER_NAME = 'DiscordCogs' self.COMMANDS_PATH = f'{Folder().rootFolder}{self.COMMANDS_FOLDER_NAME}' self.VC_TIMEOUT = 300 self.MAX_PLAYLIST_LENGTH = 50 self.MAX_PLAYLIST_FORCED_LENGTH = 5 + self.MAX_SONGS_IN_PAGE = 10 self.MAX_PRELOAD_SONGS = 15 self.MAX_SONGS_HISTORY = 15 diff --git a/Config/Embeds.py b/Config/Embeds.py index 801a818..21b716b 100644 --- a/Config/Embeds.py +++ b/Config/Embeds.py @@ -34,6 +34,14 @@ class VEmbeds: ) return embed + def INVALID_INDEX(self) -> Embed: + embed = Embed( + title=self.__messages.BAD_COMMAND_TITLE, + description=self.__messages.INVALID_INDEX, + colour=self.__colors.BLACK + ) + return embed + def SONG_ADDED_TWO(self, info: dict, pos: int) -> Embed: embed = self.SONG_INFO(info, self.__messages.SONG_ADDED_TWO, pos) return embed @@ -162,6 +170,14 @@ class VEmbeds: ) return embed + def INVALID_ARGUMENTS(self): + embed = Embed( + title=self.__messages.BAD_COMMAND_TITLE, + description=self.__messages.INVALID_ARGUMENTS, + colour=self.__colors.BLACK + ) + return embed + def COMMAND_NOT_FOUND(self) -> Embed: embed = Embed( title=self.__messages.COMMAND_NOT_FOUND_TITLE, @@ -261,6 +277,41 @@ class VEmbeds: ) return embed + def PLAYER_RESUMED(self) -> Embed: + embed = Embed( + title=self.__messages.SONG_RESUMED, + colour=self.__colors.BLUE + ) + return embed + + def SKIPPING_SONG(self) -> Embed: + embed = Embed( + title=self.__messages.SONG_SKIPPED, + colour=self.__colors.BLUE + ) + return embed + + def STOPPING_PLAYER(self) -> Embed: + embed = Embed( + title=self.__messages.STOPPING, + colour=self.__colors.BLUE + ) + return embed + + def RETURNING_SONG(self) -> Embed: + embed = Embed( + title=self.__messages.RETURNING_SONG, + colour=self.__colors.BLUE + ) + return embed + + def PLAYER_PAUSED(self) -> Embed: + embed = Embed( + title=self.__messages.SONG_PAUSED, + colour=self.__colors.BLUE + ) + return embed + def NOT_PREVIOUS_SONG(self) -> Embed: embed = Embed( title=self.__messages.SONG_PLAYER, diff --git a/Config/Exceptions.py b/Config/Exceptions.py index dee5718..de0d1c9 100644 --- a/Config/Exceptions.py +++ b/Config/Exceptions.py @@ -79,6 +79,11 @@ class ErrorRemoving(VulkanError): super().__init__(message, title, *args) +class InvalidIndex(VulkanError): + def __init__(self, message='', title='', *args: object) -> None: + super().__init__(message, title, *args) + + class NumberRequired(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) diff --git a/Config/Messages.py b/Config/Messages.py index aaa24a7..b243ce5 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -26,6 +26,9 @@ class Messages(Singleton): self.ALL_SONGS_LOOPING = f'{self.__emojis.MUSIC} Looping All Songs' self.SONG_PAUSED = f'{self.__emojis.PAUSE} Song paused' self.SONG_RESUMED = f'{self.__emojis.PLAY} Song playing' + self.SONG_SKIPPED = f'{self.__emojis.SKIP} Song skipped' + self.RETURNING_SONG = f'{self.__emojis.BACK} Playing previous song' + self.STOPPING = f'{self.__emojis.STOP} Player Stopped' self.EMPTY_QUEUE = f'{self.__emojis.QUEUE} Song queue is empty, use {configs.BOT_PREFIX}play to add new songs' self.SONG_DOWNLOADING = f'{self.__emojis.DOWNLOADING} Downloading...' @@ -64,6 +67,8 @@ class Messages(Singleton): self.NO_CHANNEL = 'To play some music, connect to any voice channel first.' self.NO_GUILD = f'This server does not has a Player, try {configs.BOT_PREFIX}reset' self.INVALID_INPUT = f'This URL was too strange, try something better or type {configs.BOT_PREFIX}help play' + self.INVALID_INDEX = f'Invalid index passed as argument.' + self.INVALID_ARGUMENTS = f'Invalid arguments passed to command.' self.DOWNLOADING_ERROR = f"{self.__emojis.ERROR} It's impossible to download and play this video" self.EXTRACTING_ERROR = f'{self.__emojis.ERROR} An error ocurred while searching for the songs' diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index add48a2..b5e6252 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -1,6 +1,8 @@ from discord.ext.commands import Context, command, Cog +from Config.Exceptions import InvalidInput from Config.Helper import Helper from Handlers.ClearHandler import ClearHandler +from Handlers.HandlerResponse import HandlerResponse from Handlers.MoveHandler import MoveHandler from Handlers.NowPlayingHandler import NowPlayingHandler from Handlers.PlayHandler import PlayHandler @@ -15,10 +17,12 @@ from Handlers.ResumeHandler import ResumeHandler from Handlers.HistoryHandler import HistoryHandler from Handlers.QueueHandler import QueueHandler from Handlers.LoopHandler import LoopHandler -from UI.Responses.EmoteCogResponse import EmoteCommandResponse -from UI.Responses.EmbedCogResponse import EmbedCommandResponse +from Messages.MessagesCategory import MessagesCategory +from Messages.Responses.EmoteCogResponse import EmoteCommandResponse +from Messages.Responses.EmbedCogResponse import EmbedCommandResponse from Music.VulkanBot import VulkanBot from Config.Configs import VConfigs +from Config.Embeds import VEmbeds from Parallelism.ProcessManager import ProcessManager helper = Helper() @@ -33,6 +37,7 @@ class MusicCog(Cog): def __init__(self, bot: VulkanBot) -> None: self.__bot: VulkanBot = bot + self.__embeds = VEmbeds() VConfigs().setProcessManager(ProcessManager(bot)) @command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) @@ -42,21 +47,37 @@ class MusicCog(Cog): response = await controller.run(args) if response is not None: - view1 = EmbedCommandResponse(response) - view2 = EmoteCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas']) - async def queue(self, ctx: Context) -> None: + async def queue(self, ctx: Context, *args) -> None: try: + pageNumber = " ".join(args) + controller = QueueHandler(ctx, self.__bot) - response = await controller.run() - view2 = EmbedCommandResponse(response) - await view2.run() + if pageNumber == "": + response = await controller.run() + else: + pageNumber = int(pageNumber) + pageNumber -= 1 # Change index 1 to 0 + response = await controller.run(pageNumber) + + cogResponser = EmbedCommandResponse(response, MessagesCategory.QUEUE) + await cogResponser.run() + except ValueError as e: + # Draft a Handler Response to pass to cogResponser + error = InvalidInput() + embed = self.__embeds.INVALID_ARGUMENTS() + response = HandlerResponse(ctx, embed, error) + + cogResponser = EmbedCommandResponse(response, MessagesCategory.QUEUE) + await cogResponser.run(deleteLast=False) except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -66,12 +87,10 @@ class MusicCog(Cog): controller = SkipHandler(ctx, self.__bot) response = await controller.run() - if response.success: - view = EmoteCommandResponse(response) - else: - view = EmbedCommandResponse(response) - - await view.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -81,12 +100,10 @@ class MusicCog(Cog): controller = StopHandler(ctx, self.__bot) response = await controller.run() - if response.success: - view = EmoteCommandResponse(response) - else: - view = EmbedCommandResponse(response) - - await view.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -96,10 +113,10 @@ class MusicCog(Cog): controller = PauseHandler(ctx, self.__bot) response = await controller.run() - view1 = EmoteCommandResponse(response) - view2 = EmbedCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -109,10 +126,10 @@ class MusicCog(Cog): controller = ResumeHandler(ctx, self.__bot) response = await controller.run() - view1 = EmoteCommandResponse(response) - view2 = EmbedCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -123,10 +140,10 @@ class MusicCog(Cog): response = await controller.run() if response is not None: - view1 = EmbedCommandResponse(response) - view2 = EmoteCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -136,10 +153,10 @@ class MusicCog(Cog): controller = HistoryHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedCommandResponse(response) - view2 = EmoteCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.HISTORY) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.HISTORY) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -149,10 +166,10 @@ class MusicCog(Cog): controller = LoopHandler(ctx, self.__bot) response = await controller.run(args) - view1 = EmoteCommandResponse(response) - view2 = EmbedCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmoteCommandResponse(response, MessagesCategory.LOOP) + cogResponser2 = EmbedCommandResponse(response, MessagesCategory.LOOP) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -162,8 +179,10 @@ class MusicCog(Cog): controller = ClearHandler(ctx, self.__bot) response = await controller.run() - view = EmoteCommandResponse(response) - await view.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -173,10 +192,10 @@ class MusicCog(Cog): controller = NowPlayingHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedCommandResponse(response) - view2 = EmoteCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.NOW_PLAYING) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.NOW_PLAYING) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -186,10 +205,10 @@ class MusicCog(Cog): controller = ShuffleHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedCommandResponse(response) - view2 = EmoteCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -199,10 +218,10 @@ class MusicCog(Cog): controller = MoveHandler(ctx, self.__bot) response = await controller.run(pos1, pos2) - view1 = EmbedCommandResponse(response) - view2 = EmoteCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.MANAGING_QUEUE) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.MANAGING_QUEUE) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -212,10 +231,10 @@ class MusicCog(Cog): controller = RemoveHandler(ctx, self.__bot) response = await controller.run(position) - view1 = EmbedCommandResponse(response) - view2 = EmoteCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.MANAGING_QUEUE) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.MANAGING_QUEUE) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -225,10 +244,10 @@ class MusicCog(Cog): controller = ResetHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedCommandResponse(response) - view2 = EmoteCommandResponse(response) - await view1.run() - await view2.run() + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') diff --git a/Handlers/HandlerResponse.py b/Handlers/HandlerResponse.py index 6840762..99a9bcb 100644 --- a/Handlers/HandlerResponse.py +++ b/Handlers/HandlerResponse.py @@ -2,14 +2,16 @@ from typing import Union from discord.ext.commands import Context from Config.Exceptions import VulkanError from discord import Embed, Interaction +from UI.Views.AbstractView import AbstractView class HandlerResponse: - def __init__(self, ctx: Union[Context, Interaction], embed: Embed = None, error: VulkanError = None) -> None: + def __init__(self, ctx: Union[Context, Interaction], embed: Embed = None, error: VulkanError = None, view=None) -> None: self.__ctx: Context = ctx self.__error: VulkanError = error self.__embed: Embed = embed self.__success = False if error else True + self.__view = view @property def ctx(self) -> Union[Context, Interaction]: @@ -19,6 +21,10 @@ class HandlerResponse: def embed(self) -> Union[Embed, None]: return self.__embed + @property + def view(self) -> AbstractView: + return self.__view + def error(self) -> Union[VulkanError, None]: return self.__error diff --git a/Handlers/JumpMusicHandler.py b/Handlers/JumpMusicHandler.py new file mode 100644 index 0000000..47a16f8 --- /dev/null +++ b/Handlers/JumpMusicHandler.py @@ -0,0 +1,80 @@ +from typing import Union +from Config.Exceptions import BadCommandUsage, InvalidInput, NumberRequired, UnknownError, VulkanError +from Handlers.AbstractHandler import AbstractHandler +from discord.ext.commands import Context +from discord import Interaction +from Handlers.HandlerResponse import HandlerResponse +from Music.Playlist import Playlist +from Music.VulkanBot import VulkanBot +from Parallelism.Commands import VCommands, VCommandsType + + +class JumpMusicHandler(AbstractHandler): + """Move a music from a specific position and play it directly""" + + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: + super().__init__(ctx, bot) + + async def run(self, musicPos: str) -> HandlerResponse: + processManager = self.config.getProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if not processInfo: + embed = self.embeds.NOT_PLAYING() + error = BadCommandUsage() + return HandlerResponse(self.ctx, embed, error) + + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: + # Try to convert input to int + error = self.__validateInput(musicPos) + if error: + embed = self.embeds.ERROR_EMBED(error.message) + processLock.release() + return HandlerResponse(self.ctx, embed, error) + + # Sanitize the input + playlist: Playlist = processInfo.getPlaylist() + musicPos = self.__sanitizeInput(playlist, musicPos) + + # Validate the position + if not playlist.validate_position(musicPos): + error = InvalidInput() + embed = self.embeds.PLAYLIST_RANGE_ERROR() + processLock.release() + return HandlerResponse(self.ctx, embed, error) + try: + # Move the selected song + playlist.move_songs(musicPos, 1) + + # Send a command to the player to skip the music + command = VCommands(VCommandsType.SKIP, None) + queue = processInfo.getQueueToPlayer() + queue.put(command) + + processLock.release() + return HandlerResponse(self.ctx) + except: + # Release the acquired Lock + processLock.release() + embed = self.embeds.ERROR_MOVING() + error = UnknownError() + return HandlerResponse(self.ctx, embed, error) + else: + processManager.resetProcess(self.guild, self.ctx) + embed = self.embeds.PLAYER_RESTARTED() + return HandlerResponse(self.ctx, embed) + + def __validateInput(self, position: str) -> Union[VulkanError, None]: + try: + position = int(position) + except: + return NumberRequired(self.messages.ERROR_NUMBER) + + def __sanitizeInput(self, playlist: Playlist, position: int) -> int: + position = int(position) + + if position == -1: + position = len(playlist.getSongs()) + + return position diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py index cba457e..a76b947 100644 --- a/Handlers/PauseHandler.py +++ b/Handlers/PauseHandler.py @@ -25,7 +25,8 @@ class PauseHandler(AbstractHandler): queue = processInfo.getQueueToPlayer() queue.put(command) - return HandlerResponse(self.ctx) + embed = self.embeds.PLAYER_PAUSED() + return HandlerResponse(self.ctx, embed) else: embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index b1d63ef..dee9ebd 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -45,7 +45,9 @@ class PrevHandler(AbstractHandler): prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id) queue = processInfo.getQueueToPlayer() queue.put(prevCommand) - return HandlerResponse(self.ctx) + + embed = self.embeds.RETURNING_SONG() + return HandlerResponse(self.ctx, embed) def __user_connected(self) -> bool: if self.author.voice: diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index db5ebb2..fea2f2a 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -1,19 +1,26 @@ from discord.ext.commands import Context +from Config.Exceptions import InvalidIndex from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Music.Downloader import Downloader +from Handlers.JumpMusicHandler import JumpMusicHandler +from Messages.MessagesCategory import MessagesCategory +from UI.Views.BasicView import BasicView from Utils.Utils import Utils from Music.VulkanBot import VulkanBot -from typing import Union -from discord import Interaction +from Music.Song import Song +from Music.Playlist import Playlist +from typing import List, Union +from discord import Button, Interaction +from UI.Buttons.CallbackButton import CallbackButton +from UI.Buttons.PlaylistDropdown import PlaylistDropdown +from Config.Emojis import VEmojis class QueueHandler(AbstractHandler): def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) - self.__down = Downloader() - async def run(self) -> HandlerResponse: + async def run(self, pageNumber=0) -> HandlerResponse: # Retrieve the process of the guild processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) @@ -25,7 +32,7 @@ class QueueHandler(AbstractHandler): processLock = processInfo.getLock() acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) if acquired: - playlist = processInfo.getPlaylist() + playlist: Playlist = processInfo.getPlaylist() if playlist.isLoopingOne(): song = playlist.getCurrentSong() @@ -33,13 +40,26 @@ class QueueHandler(AbstractHandler): processLock.release() # Release the Lock return HandlerResponse(self.ctx, embed) - songs_preload = playlist.getSongsToPreload() allSongs = playlist.getSongs() - if len(songs_preload) == 0: + if len(allSongs) == 0: embed = self.embeds.EMPTY_QUEUE() processLock.release() # Release the Lock return HandlerResponse(self.ctx, embed) + songsPages = playlist.getSongsPages() + if pageNumber < 0 or pageNumber >= len(songsPages): + embed = self.embeds.INVALID_INDEX() + error = InvalidIndex() + processLock.release() # Release the Lock + return HandlerResponse(self.ctx, embed, error) + + # Select the page in queue to be printed + songs = songsPages[pageNumber] + # Create view for this embed + buttons = self.__createViewButtons(songsPages, pageNumber) + buttons.extend(self.__createViewJumpButtons(playlist)) + queueView = BasicView(self.bot, buttons, self.config.QUEUE_VIEW_TIMEOUT) + if playlist.isLoopingAll(): title = self.messages.ALL_SONGS_LOOPING else: @@ -49,17 +69,36 @@ class QueueHandler(AbstractHandler): for song in allSongs])) total_songs = len(playlist.getSongs()) - text = f'📜 Queue length: {total_songs} | ⌛ Duration: `{total_time}` downloaded \n\n' + text = f'📜 Queue length: {total_songs} | Page Number: {pageNumber+1}/{len(songsPages)} | ⌛ Duration: `{total_time}` downloaded \n\n' - for pos, song in enumerate(songs_preload, start=1): + # To work get the correct index of all songs + startIndex = (pageNumber * self.config.MAX_SONGS_IN_PAGE) + 1 + for pos, song in enumerate(songs, start=startIndex): song_name = song.title if song.title else self.messages.SONG_DOWNLOADING text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n" embed = self.embeds.QUEUE(title, text) # Release the acquired Lock processLock.release() - return HandlerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed, view=queueView) else: processManager.resetProcess(self.guild, self.ctx) embed = self.embeds.PLAYER_RESTARTED() return HandlerResponse(self.ctx, embed) + + def __createViewButtons(self, songsPages: List[List[Song]], pageNumber: int) -> List[Button]: + buttons = [] + if pageNumber > 0: + prevPageNumber = pageNumber - 1 + buttons.append(CallbackButton(self.bot, self.run, VEmojis().BACK, self.ctx.channel, + self.guild.id, MessagesCategory.QUEUE, "Prev Page", pageNumber=prevPageNumber)) + + if pageNumber < len(songsPages) - 1: + nextPageNumber = pageNumber + 1 + buttons.append(CallbackButton(self.bot, self.run, VEmojis().SKIP, self.ctx.channel, + self.guild.id, MessagesCategory.QUEUE, "Next Page", pageNumber=nextPageNumber)) + + return buttons + + def __createViewJumpButtons(self, playlist: Playlist) -> List[Button]: + return [PlaylistDropdown(self.bot, JumpMusicHandler, playlist, self.ctx.channel, self.guild.id, MessagesCategory.PLAYER)] diff --git a/Handlers/ResumeHandler.py b/Handlers/ResumeHandler.py index 209a55a..a9988e7 100644 --- a/Handlers/ResumeHandler.py +++ b/Handlers/ResumeHandler.py @@ -25,7 +25,8 @@ class ResumeHandler(AbstractHandler): queue = processInfo.getQueueToPlayer() queue.put(command) - return HandlerResponse(self.ctx) + embed = self.embeds.PLAYER_RESUMED() + return HandlerResponse(self.ctx, embed) else: embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index ff43ef3..a4cf3ed 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -26,18 +26,13 @@ class SkipHandler(AbstractHandler): embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) - playlist = processInfo.getPlaylist() - if playlist.isLoopingOne(): - embed = self.embeds.ERROR_DUE_LOOP_ONE_ON() - error = BadCommandUsage() - return HandlerResponse(self.ctx, embed, error) - # Send a command to the player process to skip the music command = VCommands(VCommandsType.SKIP, None) queue = processInfo.getQueueToPlayer() queue.put(command) - return HandlerResponse(self.ctx) + embed = self.embeds.SKIPPING_SONG() + return HandlerResponse(self.ctx, embed) else: embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) diff --git a/Handlers/StopHandler.py b/Handlers/StopHandler.py index 91a486e..e921e5a 100644 --- a/Handlers/StopHandler.py +++ b/Handlers/StopHandler.py @@ -25,7 +25,8 @@ class StopHandler(AbstractHandler): queue = processInfo.getQueueToPlayer() queue.put(command) - return HandlerResponse(self.ctx) + embed = self.embeds.STOPPING_PLAYER() + return HandlerResponse(self.ctx, embed) else: embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) diff --git a/Messages/MessagesCategory.py b/Messages/MessagesCategory.py new file mode 100644 index 0000000..0cb1f31 --- /dev/null +++ b/Messages/MessagesCategory.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class MessagesCategory(Enum): + QUEUE = 1 + HISTORY = 2 + LOOP = 3 + NOW_PLAYING = 4 + PLAYER = 5 + MANAGING_QUEUE = 6 + OTHERS = 7 diff --git a/Messages/MessagesManager.py b/Messages/MessagesManager.py new file mode 100644 index 0000000..58140b0 --- /dev/null +++ b/Messages/MessagesManager.py @@ -0,0 +1,80 @@ +from typing import Dict, List +from discord import Message +from Config.Singleton import Singleton +from UI.Views.AbstractView import AbstractView +from Messages.MessagesCategory import MessagesCategory + + +class MessagesManager(Singleton): + def __init__(self) -> None: + if not super().created: + # For each guild, and for each category, there will be a list of messages + self.__guildsMessages: Dict[int, Dict[MessagesCategory, List[Message]]] = {} + # Will, for each message, store the AbstractView that controls it + self.__messagesViews: Dict[Message, AbstractView] = {} + + def addMessage(self, guildID: int, category: MessagesCategory, message: Message, view: AbstractView = None) -> None: + if message is None: + return + + # If guild not exists create Dict + if guildID not in self.__guildsMessages.keys(): + self.__guildsMessages[guildID] = {} + # If category not in guild yet, add + if category not in self.__guildsMessages[guildID].keys(): + self.__guildsMessages[guildID][category] = [] + + sendedMessages = self.__guildsMessages[guildID][category] + if view is not None and isinstance(view, AbstractView): + self.__messagesViews[message] = view + sendedMessages.append(message) + + async def addMessageAndClearPrevious(self, guildID: int, category: MessagesCategory, message: Message, view: AbstractView = None) -> None: + if message is None: + return + + # If guild not exists create Dict + if guildID not in self.__guildsMessages.keys(): + self.__guildsMessages[guildID] = {} + # If category not in guild yet, add + if category not in self.__guildsMessages[guildID].keys(): + self.__guildsMessages[guildID][category] = [] + + sendedMessages = self.__guildsMessages[guildID][category] + + # Delete sended all messages of this category + for previousMessage in sendedMessages: + await self.__deleteMessage(previousMessage) + + # Create a new list with only the new message + self.__guildsMessages[guildID][category] = [message] + + # Store the view of this message + if view is not None and isinstance(view, AbstractView): + self.__messagesViews[message] = view + + async def clearMessagesOfCategory(self, guildID: int, category: MessagesCategory) -> None: + sendedMessages = self.__guildsMessages[guildID][category] + + for message in sendedMessages: + self.__deleteMessage(message) + + async def clearMessagesOfGuild(self, guildID: int) -> None: + categoriesMessages = self.__guildsMessages[guildID] + + for category in categoriesMessages.keys(): + for message in categoriesMessages[category]: + self.__deleteMessage(message) + + async def __deleteMessage(self, message: Message) -> None: + try: + # If there is a view for this message delete the key + if message in self.__messagesViews.keys(): + messageView = self.__messagesViews.pop(message) + messageView.stopView() + del messageView + + await message.delete() + except Exception as e: + print(f'[ERROR DELETING MESSAGE] -> {e}') + pass diff --git a/UI/Responses/AbstractCogResponse.py b/Messages/Responses/AbstractCogResponse.py similarity index 59% rename from UI/Responses/AbstractCogResponse.py rename to Messages/Responses/AbstractCogResponse.py index 36d7b68..3bfa044 100644 --- a/UI/Responses/AbstractCogResponse.py +++ b/Messages/Responses/AbstractCogResponse.py @@ -2,12 +2,16 @@ from abc import ABC, abstractmethod from Handlers.HandlerResponse import HandlerResponse from discord.ext.commands import Context from discord import Message +from Messages.MessagesCategory import MessagesCategory +from Messages.MessagesManager import MessagesManager from Music.VulkanBot import VulkanBot class AbstractCommandResponse(ABC): - def __init__(self, response: HandlerResponse) -> None: + def __init__(self, response: HandlerResponse, category: MessagesCategory) -> None: + self.__messagesManager = MessagesManager() self.__response: HandlerResponse = response + self.__category: MessagesCategory = category self.__context: Context = response.ctx self.__message: Message = response.ctx.message self.__bot: VulkanBot = response.ctx.bot @@ -16,6 +20,10 @@ class AbstractCommandResponse(ABC): def response(self) -> HandlerResponse: return self.__response + @property + def category(self) -> MessagesCategory: + return self.__category + @property def bot(self) -> VulkanBot: return self.__bot @@ -28,6 +36,10 @@ class AbstractCommandResponse(ABC): def context(self) -> Context: return self.__context + @property + def manager(self) -> MessagesManager: + return self.__messagesManager + @abstractmethod - async def run(self) -> None: + async def run(self, deleteLast: bool = True) -> None: pass diff --git a/Messages/Responses/EmbedCogResponse.py b/Messages/Responses/EmbedCogResponse.py new file mode 100644 index 0000000..058d1d0 --- /dev/null +++ b/Messages/Responses/EmbedCogResponse.py @@ -0,0 +1,24 @@ +from Messages.Responses.AbstractCogResponse import AbstractCommandResponse +from Handlers.HandlerResponse import HandlerResponse +from Messages.MessagesCategory import MessagesCategory + + +class EmbedCommandResponse(AbstractCommandResponse): + def __init__(self, response: HandlerResponse, category: MessagesCategory) -> None: + super().__init__(response, category) + + async def run(self, deleteLast: bool = True) -> None: + message = None + if self.response.embed and self.response.view: + message = await self.context.send(embed=self.response.embed, view=self.response.view) + # Set the view to contain the sended message + self.response.view.set_message(message) + elif self.response.embed: + message = await self.context.send(embed=self.response.embed) + + if message: + # Only delete the previous message if this is not error and not forbidden by method caller + if deleteLast and self.response.success: + await self.manager.addMessageAndClearPrevious(self.context.guild.id, self.category, message, self.response.view) + else: + self.manager.addMessage(self.context.guild.id, self.category, message) diff --git a/UI/Responses/EmoteCogResponse.py b/Messages/Responses/EmoteCogResponse.py similarity index 52% rename from UI/Responses/EmoteCogResponse.py rename to Messages/Responses/EmoteCogResponse.py index 09294cf..c1d62bc 100644 --- a/UI/Responses/EmoteCogResponse.py +++ b/Messages/Responses/EmoteCogResponse.py @@ -1,15 +1,16 @@ from Config.Emojis import VEmojis -from UI.Responses.AbstractCogResponse import AbstractCommandResponse +from Messages.Responses.AbstractCogResponse import AbstractCommandResponse from Handlers.HandlerResponse import HandlerResponse +from Messages.MessagesCategory import MessagesCategory class EmoteCommandResponse(AbstractCommandResponse): - def __init__(self, response: HandlerResponse) -> None: - super().__init__(response) + def __init__(self, response: HandlerResponse, category: MessagesCategory) -> None: + super().__init__(response, category) self.__emojis = VEmojis() - async def run(self) -> None: + async def run(self, deleteLast: bool = True) -> None: if self.response.success: await self.message.add_reaction(self.__emojis.SUCCESS) else: diff --git a/Music/MessagesController.py b/Music/MessagesController.py deleted file mode 100644 index 0e8567a..0000000 --- a/Music/MessagesController.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import List -from discord import Embed, Message, TextChannel -from Music.VulkanBot import VulkanBot -from Parallelism.ProcessInfo import ProcessInfo -from Config.Configs import VConfigs -from Config.Messages import Messages -from Music.Song import Song -from Config.Embeds import VEmbeds -from UI.Views.PlayerView import PlayerView - - -class MessagesController: - def __init__(self, bot: VulkanBot) -> None: - self.__bot = bot - self.__previousMessages = [] - self.__configs = VConfigs() - self.__messages = Messages() - self.__embeds = VEmbeds() - - async def sendNowPlaying(self, processInfo: ProcessInfo, song: Song) -> None: - # Get the lock of the playlist - playlist = processInfo.getPlaylist() - if playlist.isLoopingOne(): - title = self.__messages.ONE_SONG_LOOPING - else: - title = self.__messages.SONG_PLAYING - - # Create View and Embed - embed = self.__embeds.SONG_INFO(song.info, title) - view = PlayerView(self.__bot) - channel = processInfo.getTextChannel() - # Delete the previous and send the message - await self.__deletePreviousNPMessages() - await channel.send(embed=embed, view=view) - - # Get the sended message - sendedMessage = await self.__getSendedMessage(channel) - # Set the message witch contains the view - view.set_message(message=sendedMessage) - self.__previousMessages.append(sendedMessage) - - async def __deletePreviousNPMessages(self) -> None: - for message in self.__previousMessages: - try: - await message.delete() - except: - pass - self.__previousMessages.clear() - - async def __getSendedMessage(self, channel: TextChannel) -> Message: - stringToIdentify = 'Uploader:' - last_messages: List[Message] = await channel.history(limit=5).flatten() - - for message in last_messages: - try: - if message.author == self.__bot.user: - if len(message.embeds) > 0: - embed: Embed = message.embeds[0] - if len(embed.fields) > 0: - if embed.fields[0].name == stringToIdentify: - return message - - except Exception as e: - print(f'DEVELOPER NOTE -> Error cleaning messages {e}') - continue diff --git a/Music/Playlist.py b/Music/Playlist.py index c86b3cd..b044486 100644 --- a/Music/Playlist.py +++ b/Music/Playlist.py @@ -50,6 +50,15 @@ class Playlist: def getSongsToPreload(self) -> List[Song]: return list(self.__queue)[:self.__configs.MAX_PRELOAD_SONGS] + def getSongsPages(self) -> List[List[Song]]: + songsPages = [] + for x in range(0, len(self.__queue), self.__configs.MAX_SONGS_IN_PAGE): + endIndex = x + self.__configs.MAX_SONGS_IN_PAGE + startIndex = x + songsPages.append(list(self.__queue)[startIndex:endIndex]) + + return songsPages + def __len__(self) -> int: return len(self.__queue) @@ -79,7 +88,7 @@ class Playlist: self.__current = None return None - self.__current = self.__queue.popleft() + self.__current: Song = self.__queue.popleft() return self.__current def prev_song(self) -> Song: diff --git a/Music/VulkanInitializer.py b/Music/VulkanInitializer.py index 3249d57..371328e 100644 --- a/Music/VulkanInitializer.py +++ b/Music/VulkanInitializer.py @@ -42,6 +42,7 @@ class VulkanInitializer: cogsStatus.append(bot.load_extension(cogPath, store=True)) if len(bot.cogs.keys()) != self.__getTotalCogs(): + print(cogsStatus) raise VulkanError(message='Failed to load some Cog') except VulkanError as e: diff --git a/Parallelism/ProcessExecutor.py b/Parallelism/ProcessExecutor.py new file mode 100644 index 0000000..ba6ab1e --- /dev/null +++ b/Parallelism/ProcessExecutor.py @@ -0,0 +1,79 @@ +from typing import List +from discord import Button, TextChannel +from discord.ui import View +from Config.Emojis import VEmojis +from Messages.MessagesCategory import MessagesCategory +from Music.VulkanBot import VulkanBot +from Parallelism.ProcessInfo import ProcessInfo +from Config.Messages import Messages +from Music.Song import Song +from Config.Embeds import VEmbeds +from UI.Buttons.HandlerButton import HandlerButton +from UI.Views.BasicView import BasicView +from Messages.MessagesManager import MessagesManager +from Handlers.PrevHandler import PrevHandler +from Handlers.PauseHandler import PauseHandler +from Handlers.SkipHandler import SkipHandler +from Handlers.StopHandler import StopHandler +from Handlers.ResumeHandler import ResumeHandler +from Handlers.LoopHandler import LoopHandler +from Handlers.QueueHandler import QueueHandler + + +class ProcessCommandsExecutor: + def __init__(self, bot: VulkanBot, guildID: int) -> None: + self.__bot = bot + self.__guildID = guildID + self.__messagesManager = MessagesManager() + self.__messages = Messages() + self.__embeds = VEmbeds() + self.__emojis = VEmojis() + + async def sendNowPlaying(self, processInfo: ProcessInfo, song: Song) -> None: + # Get the lock of the playlist + playlist = processInfo.getPlaylist() + if playlist.isLoopingOne(): + title = self.__messages.ONE_SONG_LOOPING + else: + title = self.__messages.SONG_PLAYING + + # Create View and Embed + embed = self.__embeds.SONG_INFO(song.info, title) + channel = processInfo.getTextChannel() + view = self.__getPlayerView(channel) + # Send Message and add to the MessagesManager + message = await channel.send(embed=embed, view=view) + await self.__messagesManager.addMessageAndClearPrevious(self.__guildID, MessagesCategory.NOW_PLAYING, message, view) + + # Set in the view the message witch contains the view + view.set_message(message=message) + + def __getPlayerView(self, channel: TextChannel) -> View: + buttons = self.__getPlayerButtons(channel) + view = BasicView(self.__bot, buttons) + return view + + def __getPlayerButtons(self, textChannel: TextChannel) -> List[Button]: + """Create the Buttons to be inserted in the Player View""" + buttons: List[Button] = [] + + buttons.append(HandlerButton(self.__bot, PrevHandler, self.__emojis.BACK, + textChannel, self.__guildID, MessagesCategory.PLAYER, "Back")) + buttons.append(HandlerButton(self.__bot, PauseHandler, self.__emojis.PAUSE, + textChannel, self.__guildID, MessagesCategory.PLAYER, "Pause")) + buttons.append(HandlerButton(self.__bot, ResumeHandler, self.__emojis.PLAY, + textChannel, self.__guildID, MessagesCategory.PLAYER, "Play")) + buttons.append(HandlerButton(self.__bot, StopHandler, self.__emojis.STOP, + textChannel, self.__guildID, MessagesCategory.PLAYER, "Stop")) + buttons.append(HandlerButton(self.__bot, SkipHandler, self.__emojis.SKIP, + textChannel, self.__guildID, MessagesCategory.PLAYER, "Skip")) + buttons.append(HandlerButton(self.__bot, QueueHandler, self.__emojis.QUEUE, + textChannel, self.__guildID, MessagesCategory.QUEUE, "Songs")) + buttons.append(HandlerButton(self.__bot, LoopHandler, self.__emojis.LOOP_ONE, + textChannel, self.__guildID, MessagesCategory.LOOP, "Loop One", 'One')) + buttons.append(HandlerButton(self.__bot, LoopHandler, self.__emojis.LOOP_OFF, + textChannel, self.__guildID, MessagesCategory.LOOP, "Loop Off", 'Off')) + buttons.append(HandlerButton(self.__bot, LoopHandler, self.__emojis.LOOP_ALL, + textChannel, self.__guildID, MessagesCategory.LOOP, "Loop All", 'All')) + + return buttons diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index 7c0bf20..ccfc667 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -7,7 +7,7 @@ from typing import Dict, Tuple, Union from Config.Singleton import Singleton from discord import Guild, Interaction from discord.ext.commands import Context -from Music.MessagesController import MessagesController +from Parallelism.ProcessExecutor import ProcessCommandsExecutor from Music.Song import Song from Parallelism.PlayerProcess import PlayerProcess from Music.Playlist import Playlist @@ -30,7 +30,7 @@ class ProcessManager(Singleton): self.__manager.start() self.__playersProcess: Dict[Guild, ProcessInfo] = {} self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {} - self.__playersMessages: Dict[Guild, MessagesController] = {} + self.__playersCommandsExecutor: Dict[Guild, ProcessCommandsExecutor] = {} def setPlayerInfo(self, guild: Guild, info: ProcessInfo): self.__playersProcess[guild.id] = info @@ -91,7 +91,7 @@ class ProcessManager(Singleton): thread.start() # Create a Message Controller for this player - self.__playersMessages[guildID] = MessagesController(self.__bot) + self.__playersCommandsExecutor[guildID] = ProcessCommandsExecutor(self.__bot, guildID) return processInfo @@ -157,7 +157,7 @@ class ProcessManager(Singleton): def __terminateProcess(self, guildID: int) -> None: # Delete all structures associated with the Player del self.__playersProcess[guildID] - del self.__playersMessages[guildID] + del self.__playersCommandsExecutor[guildID] threadListening = self.__playersListeners[guildID] threadListening._stop() del self.__playersListeners[guildID] @@ -174,9 +174,9 @@ class ProcessManager(Singleton): self.__playersProcess[guildID].setStatus(ProcessStatus.SLEEPING) async def showNowPlaying(self, guildID: int, song: Song) -> None: - messagesController = self.__playersMessages[guildID] + commandExecutor = self.__playersCommandsExecutor[guildID] processInfo = self.__playersProcess[guildID] - await messagesController.sendNowPlaying(processInfo, song) + await commandExecutor.sendNowPlaying(processInfo, song) class VManager(BaseManager): diff --git a/README.md b/README.md index 9939962..2739248 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Vulkan uses multiprocessing and asynchronous Python modules to maximize Music Pl

- +

@@ -15,14 +15,20 @@ Vulkan uses multiprocessing and asynchronous Python modules to maximize Music Pl - Play musics from Youtube, Spotify and Deezer links (Albums, Artists, Playlists and Tracks). - Play musics in multiple discord server at the same time. - The player contains buttons to shortcut some commands. +- Search for all musics in Queue using buttons +- Shortcut the playing of one song using dropdown menu. - Manage the loop of one or all playing musics. - Manage the order and remove musics from the queue. - Shuffle the musics queue order. +

+ +

+

- +

diff --git a/UI/Buttons/AbstractItem.py b/UI/Buttons/AbstractItem.py new file mode 100644 index 0000000..e730a24 --- /dev/null +++ b/UI/Buttons/AbstractItem.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod +from discord.ui import Item, View + + +class AbstractItem(ABC, Item): + @abstractmethod + def set_view(self, view: View): + pass + + @abstractmethod + def get_view(self) -> View: + pass diff --git a/UI/Buttons/BackButton.py b/UI/Buttons/BackButton.py deleted file mode 100644 index e800f6f..0000000 --- a/UI/Buttons/BackButton.py +++ /dev/null @@ -1,22 +0,0 @@ -from discord import ButtonStyle, Interaction -from discord.ui import Button -from Config.Emojis import VEmojis -from Handlers.PrevHandler import PrevHandler -from Music.VulkanBot import VulkanBot - - -class BackButton(Button): - def __init__(self, bot: VulkanBot): - super().__init__(label="Back", style=ButtonStyle.secondary, emoji=VEmojis().BACK) - self.__bot = bot - - async def callback(self, interaction: Interaction) -> None: - """Callback to when Button is clicked""" - # Return to Discord that this command is being processed - await interaction.response.defer() - - handler = PrevHandler(interaction, self.__bot) - response = await handler.run() - - if response.embed: - await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/CallbackButton.py b/UI/Buttons/CallbackButton.py new file mode 100644 index 0000000..e112938 --- /dev/null +++ b/UI/Buttons/CallbackButton.py @@ -0,0 +1,48 @@ +from typing import Awaitable +from Config.Emojis import VEmojis +from discord import ButtonStyle, Interaction, Message, TextChannel +from discord.ui import Button, View +from Handlers.HandlerResponse import HandlerResponse +from Messages.MessagesCategory import MessagesCategory +from Messages.MessagesManager import MessagesManager +from Music.VulkanBot import VulkanBot + + +class CallbackButton(Button): + """When clicked execute an callback passing the args and kwargs""" + + def __init__(self, bot: VulkanBot, cb: Awaitable, emoji: VEmojis, textChannel: TextChannel, guildID: int, category: MessagesCategory, label=None, *args, **kwargs): + super().__init__(label=label, style=ButtonStyle.secondary, emoji=emoji) + self.__channel = textChannel + self.__guildID = guildID + self.__category = category + self.__messagesManager = MessagesManager() + self.__bot = bot + self.__args = args + self.__kwargs = kwargs + self.__callback = cb + self.__view: View = None + + async def callback(self, interaction: Interaction) -> None: + """Callback to when Button is clicked""" + # Return to Discord that this command is being processed + await interaction.response.defer() + + response: HandlerResponse = await self.__callback(*self.__args, **self.__kwargs) + + message = None + if response and response.view is not None: + message: Message = await self.__channel.send(embed=response.embed, view=response.view) + response.view.set_message(message) + elif response.embed: + message: Message = await self.__channel.send(embed=response.embed) + + # Clear the last sended message in this category and add the new one + if message: + await self.__messagesManager.addMessageAndClearPrevious(self.__guildID, self.__category, message, response.view) + + def set_view(self, view: View): + self.__view = view + + def get_view(self) -> View: + return self.__view diff --git a/UI/Buttons/HandlerButton.py b/UI/Buttons/HandlerButton.py new file mode 100644 index 0000000..316e954 --- /dev/null +++ b/UI/Buttons/HandlerButton.py @@ -0,0 +1,50 @@ +from Config.Emojis import VEmojis +from discord import ButtonStyle, Interaction, Message, TextChannel +from discord.ui import Button, View +from Handlers.HandlerResponse import HandlerResponse +from Messages.MessagesCategory import MessagesCategory +from Music.VulkanBot import VulkanBot +from Handlers.AbstractHandler import AbstractHandler +from Messages.MessagesManager import MessagesManager + + +class HandlerButton(Button): + """Button that will create and execute a Handler Object when clicked""" + + def __init__(self, bot: VulkanBot, handler: type[AbstractHandler], emoji: VEmojis, textChannel: TextChannel, guildID: int, category: MessagesCategory, label=None, *args, **kwargs): + super().__init__(label=label, style=ButtonStyle.secondary, emoji=emoji) + self.__messagesManager = MessagesManager() + self.__category = category + self.__guildID = guildID + self.__channel = textChannel + self.__bot = bot + self.__args = args + self.__kwargs = kwargs + self.__handlerClass = handler + self.__view: View = None + + async def callback(self, interaction: Interaction) -> None: + """Callback to when Button is clicked""" + # Return to Discord that this command is being processed + await interaction.response.defer() + + # Create the handler object + handler = self.__handlerClass(interaction, self.__bot) + response: HandlerResponse = await handler.run(*self.__args, **self.__kwargs) + + message = None + if response and response.view is not None: + message: Message = await self.__channel.send(embed=response.embed, view=response.view) + response.view.set_message(message) + elif response.embed: + message: Message = await self.__channel.send(embed=response.embed) + + # Clear the last category sended message and add the new one + if message: + await self.__messagesManager.addMessageAndClearPrevious(self.__guildID, self.__category, message, response.view) + + def set_view(self, view: View): + self.__view = view + + def get_view(self) -> View: + return self.__view diff --git a/UI/Buttons/LoopAllButton.py b/UI/Buttons/LoopAllButton.py deleted file mode 100644 index 00d7571..0000000 --- a/UI/Buttons/LoopAllButton.py +++ /dev/null @@ -1,20 +0,0 @@ -from discord import ButtonStyle, Interaction -from discord.ui import Button -from Config.Emojis import VEmojis -from Handlers.LoopHandler import LoopHandler -from Music.VulkanBot import VulkanBot - - -class LoopAllButton(Button): - def __init__(self, bot: VulkanBot): - super().__init__(label="Loop All", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ALL) - self.__bot = bot - - async def callback(self, interaction: Interaction) -> None: - await interaction.response.defer() - - handler = LoopHandler(interaction, self.__bot) - response = await handler.run('all') - - if response.embed: - await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/LoopOffButton.py b/UI/Buttons/LoopOffButton.py deleted file mode 100644 index ce943b3..0000000 --- a/UI/Buttons/LoopOffButton.py +++ /dev/null @@ -1,20 +0,0 @@ -from discord import ButtonStyle, Interaction -from discord.ui import Button -from Config.Emojis import VEmojis -from Handlers.LoopHandler import LoopHandler -from Music.VulkanBot import VulkanBot - - -class LoopOffButton(Button): - def __init__(self, bot: VulkanBot): - super().__init__(label="Loop Off", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_OFF) - self.__bot = bot - - async def callback(self, interaction: Interaction) -> None: - await interaction.response.defer() - - handler = LoopHandler(interaction, self.__bot) - response = await handler.run('off') - - if response.embed: - await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/LoopOneButton.py b/UI/Buttons/LoopOneButton.py deleted file mode 100644 index 45c3dc7..0000000 --- a/UI/Buttons/LoopOneButton.py +++ /dev/null @@ -1,20 +0,0 @@ -from discord import ButtonStyle, Interaction -from discord.ui import Button -from Config.Emojis import VEmojis -from Handlers.LoopHandler import LoopHandler -from Music.VulkanBot import VulkanBot - - -class LoopOneButton(Button): - def __init__(self, bot: VulkanBot): - super().__init__(label="Loop One", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ONE) - self.__bot = bot - - async def callback(self, interaction: Interaction) -> None: - await interaction.response.defer() - - handler = LoopHandler(interaction, self.__bot) - response = await handler.run('one') - - if response.embed: - await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/PauseButton.py b/UI/Buttons/PauseButton.py deleted file mode 100644 index 996e829..0000000 --- a/UI/Buttons/PauseButton.py +++ /dev/null @@ -1,20 +0,0 @@ -from discord import ButtonStyle, Interaction -from discord.ui import Button -from Config.Emojis import VEmojis -from Handlers.PauseHandler import PauseHandler -from Music.VulkanBot import VulkanBot - - -class PauseButton(Button): - def __init__(self, bot: VulkanBot): - super().__init__(label="Pause", style=ButtonStyle.secondary, emoji=VEmojis().PAUSE) - self.__bot = bot - - async def callback(self, interaction: Interaction) -> None: - await interaction.response.defer() - - handler = PauseHandler(interaction, self.__bot) - response = await handler.run() - - if response.embed: - await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/PlayButton.py b/UI/Buttons/PlayButton.py deleted file mode 100644 index e0e939b..0000000 --- a/UI/Buttons/PlayButton.py +++ /dev/null @@ -1,20 +0,0 @@ -from discord import ButtonStyle, Interaction -from discord.ui import Button -from Config.Emojis import VEmojis -from Music.VulkanBot import VulkanBot -from Handlers.ResumeHandler import ResumeHandler - - -class PlayButton(Button): - def __init__(self, bot: VulkanBot): - super().__init__(label="Play", style=ButtonStyle.secondary, emoji=VEmojis().PLAY) - self.__bot = bot - - async def callback(self, interaction: Interaction) -> None: - await interaction.response.defer() - - handler = ResumeHandler(interaction, self.__bot) - response = await handler.run() - - if response.embed: - await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/PlaylistDropdown.py b/UI/Buttons/PlaylistDropdown.py new file mode 100644 index 0000000..b8ad7a4 --- /dev/null +++ b/UI/Buttons/PlaylistDropdown.py @@ -0,0 +1,88 @@ +import asyncio +from typing import List +from discord import Interaction, Message, TextChannel, SelectOption +from discord.ui import Select, View +from Handlers.HandlerResponse import HandlerResponse +from Messages.MessagesCategory import MessagesCategory +from Messages.MessagesManager import MessagesManager +from Music.VulkanBot import VulkanBot +from Handlers.AbstractHandler import AbstractHandler +from UI.Buttons.AbstractItem import AbstractItem +from UI.Views.AbstractView import AbstractView +from Music.Playlist import Playlist + + +class PlaylistDropdown(Select, AbstractItem): + """Receives n elements to put in drop down and return the selected, pass the index value to a handler""" + + def __init__(self, bot: VulkanBot, handler: type[AbstractHandler], playlist: Playlist, textChannel: TextChannel, guildID: int, category: MessagesCategory): + songs = list(playlist.getSongs()) + + values = [str(x) for x in range(1, len(songs) + 1)] + # Get the title of each of the 20 first songs, the pycord library doesn't accept more + songsNames: List[str] = [] + for x in range(20): + songsNames.append(f'{x + 1} - {songs[x].title[:80]}') + + selectOptions: List[SelectOption] = [] + + for x in range(len(songsNames)): + selectOptions.append(SelectOption(label=songsNames[x], value=values[x])) + + super().__init__(placeholder="Select one music to play now, may be outdated", + min_values=1, max_values=1, options=selectOptions) + + self.__playlist = playlist + self.__channel = textChannel + self.__guildID = guildID + self.__category = category + self.__handlerClass = handler + self.__messagesManager = MessagesManager() + self.__bot = bot + self.__view: AbstractView = None + + async def callback(self, interaction: Interaction) -> None: + """Callback to when the selection is selected""" + await interaction.response.defer() + + # Execute the handler passing the value selected + handler = self.__handlerClass(interaction, self.__bot) + response: HandlerResponse = await handler.run(self.values[0]) + + message = None + if response and response.view is not None: + message: Message = await self.__channel.send(embed=response.embed, view=response.view) + elif response.embed: + message: Message = await self.__channel.send(embed=response.embed) + + # Clear the last sended message in this category and add the new one + if message: + await self.__messagesManager.addMessageAndClearPrevious(self.__guildID, self.__category, message, response.view) + + # Extreme ugly way to wait for the player process to actually retrieve the next song + await asyncio.sleep(2) + + await self.__update() + + async def __update(self): + songs = list(self.__playlist.getSongs()) + + values = [str(x) for x in range(1, len(songs) + 1)] + # Get the title of each of the 20 first songs, library doesn't accept more + songsNames = [song.title[:80] for song in songs[:20]] + + selectOptions: List[SelectOption] = [] + + for x in range(len(songsNames)): + selectOptions.append(SelectOption(label=songsNames[x], value=values[x])) + + self.options = selectOptions + + if self.__view is not None: + await self.__view.update() + + def set_view(self, view: View): + self.__view = view + + def get_view(self) -> View: + return self.__view diff --git a/UI/Buttons/SkipButton.py b/UI/Buttons/SkipButton.py deleted file mode 100644 index 479a210..0000000 --- a/UI/Buttons/SkipButton.py +++ /dev/null @@ -1,20 +0,0 @@ -from discord import ButtonStyle, Interaction -from discord.ui import Button -from Config.Emojis import VEmojis -from Music.VulkanBot import VulkanBot -from Handlers.SkipHandler import SkipHandler - - -class SkipButton(Button): - def __init__(self, bot: VulkanBot): - super().__init__(label="Skip", style=ButtonStyle.secondary, emoji=VEmojis().SKIP) - self.__bot = bot - - async def callback(self, interaction: Interaction) -> None: - await interaction.response.defer() - - handler = SkipHandler(interaction, self.__bot) - response = await handler.run() - - if response.embed: - await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/SongsButton.py b/UI/Buttons/SongsButton.py deleted file mode 100644 index fa57da1..0000000 --- a/UI/Buttons/SongsButton.py +++ /dev/null @@ -1,20 +0,0 @@ -from Handlers.QueueHandler import QueueHandler -from discord import ButtonStyle, Interaction -from discord.ui import Button -from Config.Emojis import VEmojis -from Music.VulkanBot import VulkanBot - - -class SongsButton(Button): - def __init__(self, bot: VulkanBot): - super().__init__(label="Songs", style=ButtonStyle.secondary, emoji=VEmojis().QUEUE) - self.__bot = bot - - async def callback(self, interaction: Interaction) -> None: - await interaction.response.defer() - - handler = QueueHandler(interaction, self.__bot) - response = await handler.run() - - if response.embed: - await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/StopButton.py b/UI/Buttons/StopButton.py deleted file mode 100644 index 18b0535..0000000 --- a/UI/Buttons/StopButton.py +++ /dev/null @@ -1,20 +0,0 @@ -from discord import ButtonStyle, Interaction -from discord.ui import Button -from Config.Emojis import VEmojis -from Music.VulkanBot import VulkanBot -from Handlers.StopHandler import StopHandler - - -class StopButton(Button): - def __init__(self, bot: VulkanBot): - super().__init__(label="Stop", style=ButtonStyle.secondary, emoji=VEmojis().STOP) - self.__bot = bot - - async def callback(self, interaction: Interaction) -> None: - await interaction.response.defer() - - handler = StopHandler(interaction, self.__bot) - response = await handler.run() - - if response.embed: - await interaction.followup.send(embed=response.embed) diff --git a/UI/Responses/EmbedCogResponse.py b/UI/Responses/EmbedCogResponse.py deleted file mode 100644 index 9dac78c..0000000 --- a/UI/Responses/EmbedCogResponse.py +++ /dev/null @@ -1,11 +0,0 @@ -from UI.Responses.AbstractCogResponse import AbstractCommandResponse -from Handlers.HandlerResponse import HandlerResponse - - -class EmbedCommandResponse(AbstractCommandResponse): - def __init__(self, response: HandlerResponse) -> None: - super().__init__(response) - - async def run(self) -> None: - if self.response.embed: - await self.context.send(embed=self.response.embed) diff --git a/UI/Views/AbstractView.py b/UI/Views/AbstractView.py new file mode 100644 index 0000000..71948f5 --- /dev/null +++ b/UI/Views/AbstractView.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod + + +class AbstractView(ABC): + @abstractmethod + async def update(self) -> None: + pass + + def set_message(self, message) -> None: + pass + + @abstractmethod + def stopView(self) -> None: + pass diff --git a/UI/Views/BasicView.py b/UI/Views/BasicView.py new file mode 100644 index 0000000..46f04ed --- /dev/null +++ b/UI/Views/BasicView.py @@ -0,0 +1,52 @@ +from typing import List +from discord import Message +from discord.ui import View +from Config.Emojis import VEmojis +from Music.VulkanBot import VulkanBot +from UI.Views.AbstractView import AbstractView +from UI.Buttons.AbstractItem import AbstractItem + +emojis = VEmojis() + + +class BasicView(View, AbstractView): + """View that receives buttons to hold, in timeout disable buttons""" + + def __init__(self, bot: VulkanBot, buttons: List[AbstractItem], timeout: float = 6000): + super().__init__(timeout=timeout) + self.__bot = bot + self.__message: Message = None + self.__working = True + + for button in buttons: + # Set the buttons to have a instance of the view that contains them + button.set_view(self) + self.add_item(button) + + def stopView(self): + self.__working = False + + async def on_timeout(self) -> None: + # Disable all itens and, if has the message, edit it + try: + if not self.__working: + return + + self.disable_all_items() + if self.__message is not None and isinstance(self.__message, Message): + await self.__message.edit(f"{emojis.MUSIC} - The buttons in this message have been disabled due timeout", view=self) + except Exception as e: + print(f'[ERROR EDITING MESSAGE] -> {e}') + + def set_message(self, message: Message) -> None: + self.__message = message + + async def update(self): + try: + if not self.__working: + return + + if self.__message is not None: + await self.__message.edit(view=self) + except Exception as e: + print(f'[ERROR UPDATING MESSAGE] -> {e}') diff --git a/UI/Views/PlayerView.py b/UI/Views/PlayerView.py deleted file mode 100644 index d7573c7..0000000 --- a/UI/Views/PlayerView.py +++ /dev/null @@ -1,43 +0,0 @@ -from discord import Message -from discord.ui import View -from Config.Emojis import VEmojis -from UI.Buttons.PauseButton import PauseButton -from UI.Buttons.BackButton import BackButton -from UI.Buttons.SkipButton import SkipButton -from UI.Buttons.StopButton import StopButton -from UI.Buttons.SongsButton import SongsButton -from UI.Buttons.PlayButton import PlayButton -from UI.Buttons.LoopAllButton import LoopAllButton -from UI.Buttons.LoopOneButton import LoopOneButton -from UI.Buttons.LoopOffButton import LoopOffButton -from Music.VulkanBot import VulkanBot - -emojis = VEmojis() - - -class PlayerView(View): - def __init__(self, bot: VulkanBot, timeout: float = 6000): - super().__init__(timeout=timeout) - self.__bot = bot - self.__message: Message = None - self.add_item(BackButton(self.__bot)) - self.add_item(PauseButton(self.__bot)) - self.add_item(PlayButton(self.__bot)) - self.add_item(StopButton(self.__bot)) - self.add_item(SkipButton(self.__bot)) - self.add_item(SongsButton(self.__bot)) - self.add_item(LoopOneButton(self.__bot)) - self.add_item(LoopOffButton(self.__bot)) - self.add_item(LoopAllButton(self.__bot)) - - async def on_timeout(self) -> None: - # Disable all itens and, if has the message, edit it - try: - self.disable_all_items() - if self.__message is not None and isinstance(self.__message, Message): - await self.__message.edit(view=self) - except Exception as e: - print(f'[ERROR EDITING MESSAGE] -> {e}') - - def set_message(self, message: Message) -> None: - self.__message = message