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..c367853 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, 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..cf6ca05 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -64,6 +64,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..b5409d9 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 @@ -19,6 +21,7 @@ from UI.Responses.EmoteCogResponse import EmoteCommandResponse from UI.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 +36,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']) @@ -50,13 +54,27 @@ class MusicCog(Cog): 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: - controller = QueueHandler(ctx, self.__bot) + pageNumber = " ".join(args) - response = await controller.run() - view2 = EmbedCommandResponse(response) - await view2.run() + controller = QueueHandler(ctx, self.__bot,) + + if pageNumber == "": + response = await controller.run() + else: + pageNumber = int(pageNumber) + pageNumber -= 1 # Change index 1 to 0 + response = await controller.run(pageNumber) + + view = EmbedCommandResponse(response) + await view.run() + except ValueError as e: + error = InvalidInput() + embed = self.__embeds.INVALID_ARGUMENTS() + response = HandlerResponse(ctx, embed, error) + view = EmbedCommandResponse(response) + await view.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') diff --git a/Handlers/HandlerResponse.py b/Handlers/HandlerResponse.py index 6840762..e62b8e5 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 discord.ui import View 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) -> View: + return self.__view + def error(self) -> Union[VulkanError, None]: return self.__error diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index db5ebb2..e2ca767 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -1,19 +1,23 @@ 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 UI.Views.EmptyView import EmptyView 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.EmptyButton import EmptyButton +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 +29,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 +37,25 @@ class QueueHandler(AbstractHandler): processLock.release() # Release the Lock return HandlerResponse(self.ctx, embed) - songs_preload = playlist.getSongsToPreload() + 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) + 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) + # Select the page in queue to be printed + songs = songsPages[pageNumber] + # Create view for this embed + buttons = self.__createViewButtons(songsPages, pageNumber) + queueView = EmptyView(self.bot, buttons, self.config.QUEUE_VIEW_TIMEOUT) + if playlist.isLoopingAll(): title = self.messages.ALL_SONGS_LOOPING else: @@ -49,17 +65,33 @@ 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(EmptyButton(self.bot, self.run, VEmojis().BACK, + "Prev Page", pageNumber=prevPageNumber)) + + if pageNumber < len(songsPages) - 1: + nextPageNumber = pageNumber + 1 + buttons.append(EmptyButton(self.bot, self.run, VEmojis().SKIP, + "Next Page", pageNumber=nextPageNumber)) + + return buttons diff --git a/Music/Playlist.py b/Music/Playlist.py index c86b3cd..470f9b9 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) 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/UI/Buttons/BackButton.py b/UI/Buttons/BackButton.py index e800f6f..87a82b4 100644 --- a/UI/Buttons/BackButton.py +++ b/UI/Buttons/BackButton.py @@ -18,5 +18,7 @@ class BackButton(Button): handler = PrevHandler(interaction, self.__bot) response = await handler.run() - if response.embed: + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/EmptyButton.py b/UI/Buttons/EmptyButton.py new file mode 100644 index 0000000..1f12fd4 --- /dev/null +++ b/UI/Buttons/EmptyButton.py @@ -0,0 +1,26 @@ +from typing import Awaitable +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Handlers.HandlerResponse import HandlerResponse +from Music.VulkanBot import VulkanBot + + +class EmptyButton(Button): + def __init__(self, bot: VulkanBot, cb: Awaitable, emoji, label=None, *args, **kwargs): + super().__init__(label=label, style=ButtonStyle.secondary, emoji=emoji) + self.__bot = bot + self.__args = args + self.__kwargs = kwargs + self.__callback = cb + + 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) + + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/LoopAllButton.py b/UI/Buttons/LoopAllButton.py index 00d7571..771640e 100644 --- a/UI/Buttons/LoopAllButton.py +++ b/UI/Buttons/LoopAllButton.py @@ -16,5 +16,7 @@ class LoopAllButton(Button): handler = LoopHandler(interaction, self.__bot) response = await handler.run('all') - if response.embed: + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/LoopOffButton.py b/UI/Buttons/LoopOffButton.py index ce943b3..57d2753 100644 --- a/UI/Buttons/LoopOffButton.py +++ b/UI/Buttons/LoopOffButton.py @@ -16,5 +16,7 @@ class LoopOffButton(Button): handler = LoopHandler(interaction, self.__bot) response = await handler.run('off') - if response.embed: + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/LoopOneButton.py b/UI/Buttons/LoopOneButton.py index 45c3dc7..d4d1aac 100644 --- a/UI/Buttons/LoopOneButton.py +++ b/UI/Buttons/LoopOneButton.py @@ -16,5 +16,7 @@ class LoopOneButton(Button): handler = LoopHandler(interaction, self.__bot) response = await handler.run('one') - if response.embed: + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/PauseButton.py b/UI/Buttons/PauseButton.py index 996e829..9bb9830 100644 --- a/UI/Buttons/PauseButton.py +++ b/UI/Buttons/PauseButton.py @@ -16,5 +16,7 @@ class PauseButton(Button): handler = PauseHandler(interaction, self.__bot) response = await handler.run() - if response.embed: + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/PlayButton.py b/UI/Buttons/PlayButton.py index e0e939b..e417640 100644 --- a/UI/Buttons/PlayButton.py +++ b/UI/Buttons/PlayButton.py @@ -16,5 +16,7 @@ class PlayButton(Button): handler = ResumeHandler(interaction, self.__bot) response = await handler.run() - if response.embed: + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/SkipButton.py b/UI/Buttons/SkipButton.py index 479a210..febd71f 100644 --- a/UI/Buttons/SkipButton.py +++ b/UI/Buttons/SkipButton.py @@ -16,5 +16,7 @@ class SkipButton(Button): handler = SkipHandler(interaction, self.__bot) response = await handler.run() - if response.embed: + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/SongsButton.py b/UI/Buttons/SongsButton.py index fa57da1..e21b7fb 100644 --- a/UI/Buttons/SongsButton.py +++ b/UI/Buttons/SongsButton.py @@ -16,5 +16,7 @@ class SongsButton(Button): handler = QueueHandler(interaction, self.__bot) response = await handler.run() - if response.embed: + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/StopButton.py b/UI/Buttons/StopButton.py index 18b0535..cb74449 100644 --- a/UI/Buttons/StopButton.py +++ b/UI/Buttons/StopButton.py @@ -16,5 +16,7 @@ class StopButton(Button): handler = StopHandler(interaction, self.__bot) response = await handler.run() - if response.embed: + if response and response.view is not None: + await interaction.followup.send(embed=response.embed, view=response.view) + elif response: await interaction.followup.send(embed=response.embed) diff --git a/UI/Responses/AbstractCogResponse.py b/UI/Responses/AbstractCogResponse.py index 36d7b68..1cee764 100644 --- a/UI/Responses/AbstractCogResponse.py +++ b/UI/Responses/AbstractCogResponse.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from Handlers.HandlerResponse import HandlerResponse from discord.ext.commands import Context from discord import Message +from discord.ui import View from Music.VulkanBot import VulkanBot diff --git a/UI/Responses/EmbedCogResponse.py b/UI/Responses/EmbedCogResponse.py index 9dac78c..f86490b 100644 --- a/UI/Responses/EmbedCogResponse.py +++ b/UI/Responses/EmbedCogResponse.py @@ -8,4 +8,4 @@ class EmbedCommandResponse(AbstractCommandResponse): async def run(self) -> None: if self.response.embed: - await self.context.send(embed=self.response.embed) + await self.context.send(embed=self.response.embed, view=self.response.view) diff --git a/UI/Views/EmptyView.py b/UI/Views/EmptyView.py new file mode 100644 index 0000000..aee9da1 --- /dev/null +++ b/UI/Views/EmptyView.py @@ -0,0 +1,29 @@ +from typing import List +from discord import Button, Message +from discord.ui import View +from Config.Emojis import VEmojis +from Music.VulkanBot import VulkanBot + +emojis = VEmojis() + + +class EmptyView(View): + def __init__(self, bot: VulkanBot, buttons: List[Button], timeout: float = 6000): + super().__init__(timeout=timeout) + self.__bot = bot + self.__message: Message = None + + for button in buttons: + self.add_item(button) + + 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