Adding pages to songs queue to move between all queue, adding buttons to queue embed to better user experience

This commit is contained in:
Rafael Vargas 2022-08-02 21:52:54 -04:00
parent 6ba7734a36
commit 2627f95a6d
22 changed files with 192 additions and 27 deletions

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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'

View File

@ -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}')

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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)

26
UI/Buttons/EmptyButton.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

29
UI/Views/EmptyView.py Normal file
View File

@ -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