diff --git a/.gitignore b/.gitignore index 40aa9d2..71d50e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .vscode -assets/ __pycache__ .env .cache diff --git a/Assets/playermenu.jfif b/Assets/playermenu.jfif new file mode 100644 index 0000000..bd10207 Binary files /dev/null and b/Assets/playermenu.jfif differ diff --git a/Assets/vulkan-logo.png b/Assets/vulkan-logo.png new file mode 100644 index 0000000..7f52e39 Binary files /dev/null and b/Assets/vulkan-logo.png differ diff --git a/Assets/vulkancommands.jfif b/Assets/vulkancommands.jfif new file mode 100644 index 0000000..588aaf6 Binary files /dev/null and b/Assets/vulkancommands.jfif differ diff --git a/Config/Colors.py b/Config/Colors.py index 1f55a3d..c990c2a 100644 --- a/Config/Colors.py +++ b/Config/Colors.py @@ -1,7 +1,7 @@ from Config.Singleton import Singleton -class Colors(Singleton): +class VColors(Singleton): def __init__(self) -> None: self.__red = 0xDC143C self.__green = 0x1F8B4C diff --git a/Config/Configs.py b/Config/Configs.py index 09395bb..06efea9 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -2,7 +2,7 @@ from decouple import config from Config.Singleton import Singleton -class Configs(Singleton): +class VConfigs(Singleton): def __init__(self) -> None: if not super().created: self.BOT_PREFIX = '!' @@ -18,7 +18,7 @@ class Configs(Singleton): self.CLEANER_MESSAGES_QUANT = 5 self.ACQUIRE_LOCK_TIMEOUT = 10 self.COMMANDS_PATH = 'DiscordCogs' - self.VC_TIMEOUT = 600 + self.VC_TIMEOUT = 300 self.MAX_PLAYLIST_LENGTH = 50 self.MAX_PLAYLIST_FORCED_LENGTH = 5 @@ -30,3 +30,9 @@ class Configs(Singleton): self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose' self.INVITE_URL = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot' + + def getProcessManager(self): + return self.__manager + + def setProcessManager(self, newManager): + self.__manager = newManager diff --git a/Views/Embeds.py b/Config/Embeds.py similarity index 98% rename from Views/Embeds.py rename to Config/Embeds.py index ef60595..801a818 100644 --- a/Views/Embeds.py +++ b/Config/Embeds.py @@ -1,16 +1,16 @@ from Config.Messages import Messages from Config.Exceptions import VulkanError from discord import Embed -from Config.Configs import Configs -from Config.Colors import Colors +from Config.Configs import VConfigs +from Config.Colors import VColors from datetime import timedelta -class Embeds: +class VEmbeds: def __init__(self) -> None: - self.__config = Configs() + self.__config = VConfigs() self.__messages = Messages() - self.__colors = Colors() + self.__colors = VColors() def ONE_SONG_LOOPING(self, info: dict) -> Embed: title = self.__messages.ONE_SONG_LOOPING @@ -334,7 +334,7 @@ class Embeds: def CARA_COROA(self, result: str) -> Embed: embed = Embed( - title='Cara Cora', + title='Cara Coroa', description=f'Result: {result}', colour=self.__colors.GREEN ) diff --git a/Config/Emojis.py b/Config/Emojis.py new file mode 100644 index 0000000..d34b4d2 --- /dev/null +++ b/Config/Emojis.py @@ -0,0 +1,20 @@ +from Config.Singleton import Singleton + + +class VEmojis(Singleton): + def __init__(self) -> None: + if not super().created: + self.SKIP = "⏩" + self.BACK = "⏪" + self.PAUSE = "⏸️" + self.PLAY = "▶️" + self.STOP = "⏹️" + self.LOOP_ONE = "🔂" + self.LOOP_OFF = "➡️" + self.LOOP_ALL = "🔁" + self.SHUFFLE = "🔀" + self.QUEUE = "📜" + self.MUSIC = "🎧" + self.ERROR = "❌" + self.DOWNLOADING = "📥" + self.SUCCESS = "✅" diff --git a/Config/Helper.py b/Config/Helper.py index 656bfa6..49ee40c 100644 --- a/Config/Helper.py +++ b/Config/Helper.py @@ -1,11 +1,11 @@ from Config.Singleton import Singleton -from Config.Configs import Configs +from Config.Configs import VConfigs class Helper(Singleton): def __init__(self) -> None: if not super().created: - config = Configs() + config = VConfigs() self.HELP_SKIP = 'Skip the current playing song.' self.HELP_SKIP_LONG = 'Skip the playing of the current song, does not work if loop one is activated. \n\nArguments: None.' self.HELP_RESUME = 'Resumes the song player.' diff --git a/Config/Messages.py b/Config/Messages.py index 47ab1b5..aaa24a7 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -1,11 +1,13 @@ from Config.Singleton import Singleton -from Config.Configs import Configs +from Config.Configs import VConfigs +from Config.Emojis import VEmojis class Messages(Singleton): def __init__(self) -> None: if not super().created: - configs = Configs() + self.__emojis = VEmojis() + configs = VConfigs() self.STARTUP_MESSAGE = 'Starting Vulkan...' self.STARTUP_COMPLETE_MESSAGE = 'Vulkan is now operating.' @@ -16,67 +18,67 @@ class Messages(Singleton): self.SONGS_ADDED = 'Downloading `{}` songs to add to the queue' self.SONG_ADDED = 'Downloading the song `{}` to add to the queue' - self.SONG_ADDED_TWO = '🎧 Song added to the queue' - self.SONG_PLAYING = '🎧 Song playing now' - self.SONG_PLAYER = '🎧 Song Player' - self.QUEUE_TITLE = '🎧 Songs in Queue' - self.ONE_SONG_LOOPING = '🎧 Looping One Song' - self.ALL_SONGS_LOOPING = '🎧 Looping All Songs' - self.SONG_PAUSED = '⏸️ Song paused' - self.SONG_RESUMED = '▶️ Song playing' - self.EMPTY_QUEUE = f'📜 Song queue is empty, use {configs.BOT_PREFIX}play to add new songs' - self.SONG_DOWNLOADING = '📥 Downloading...' + self.SONG_ADDED_TWO = f'{self.__emojis.MUSIC} Song added to the queue' + self.SONG_PLAYING = f'{self.__emojis.MUSIC} Song playing now' + self.SONG_PLAYER = f'{self.__emojis.MUSIC} Song Player' + self.QUEUE_TITLE = f'{self.__emojis.MUSIC} Songs in Queue' + self.ONE_SONG_LOOPING = f'{self.__emojis.MUSIC} Looping One Song' + 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.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...' - self.HISTORY_TITLE = '🎧 Played Songs' - self.HISTORY_EMPTY = '📜 There is no musics in history' + self.HISTORY_TITLE = f'{self.__emojis.MUSIC} Played Songs' + self.HISTORY_EMPTY = f'{self.__emojis.QUEUE} There is no musics in history' self.SONG_MOVED_SUCCESSFULLY = 'Song `{}` in position `{}` moved to the position `{}` successfully' self.SONG_REMOVED_SUCCESSFULLY = 'Song `{}` removed successfully' - self.LOOP_ALL_ON = f'❌ Vulkan is looping all songs, use {configs.BOT_PREFIX}loop off to disable this loop first' - self.LOOP_ONE_ON = f'❌ Vulkan is looping one song, use {configs.BOT_PREFIX}loop off to disable this loop first' - self.LOOP_ALL_ALREADY_ON = '🔁 Vulkan is already looping all songs' - self.LOOP_ONE_ALREADY_ON = '🔂 Vulkan is already looping the current song' - self.LOOP_ALL_ACTIVATE = '🔁 Looping all songs' - self.LOOP_ONE_ACTIVATE = '🔂 Looping the current song' - self.LOOP_DISABLE = '➡️ Loop disabled' - self.LOOP_ALREADY_DISABLE = '❌ Loop is already disabled' - self.LOOP_ON = f'❌ This command cannot be invoked with any loop activated. Use {configs.BOT_PREFIX}loop off to disable loop' - self.BAD_USE_OF_LOOP = f"""❌ Invalid arguments of Loop command. Use {configs.BOT_PREFIX}help loop to more information. - -> Available Arguments: ["all", "off", "one", ""]""" + self.LOOP_ALL_ON = f'{self.__emojis.ERROR} Vulkan is looping all songs, use {configs.BOT_PREFIX}loop off to disable this loop first' + self.LOOP_ONE_ON = f'{self.__emojis.ERROR} Vulkan is looping one song, use {configs.BOT_PREFIX}loop off to disable this loop first' + self.LOOP_ALL_ALREADY_ON = f'{self.__emojis.LOOP_ALL} Vulkan is already looping all songs' + self.LOOP_ONE_ALREADY_ON = f'{self.__emojis.LOOP_ONE} Vulkan is already looping the current song' + self.LOOP_ALL_ACTIVATE = f'{self.__emojis.LOOP_ALL} Looping all songs' + self.LOOP_ONE_ACTIVATE = f'{self.__emojis.LOOP_ONE} Looping the current song' + self.LOOP_DISABLE = f'{self.__emojis.LOOP_OFF} Loop disabled' + self.LOOP_ALREADY_DISABLE = f'{self.__emojis.ERROR} Loop is already disabled' + self.LOOP_ON = f'{self.__emojis.ERROR} This command cannot be invoked with any loop activated. Use {configs.BOT_PREFIX}loop off to disable loop' + self.BAD_USE_OF_LOOP = f"""{self.__emojis.ERROR} Invalid arguments of Loop command. Use {configs.BOT_PREFIX}help loop to more information. + -> Available Arguments: ["all", "off", "one", ""]""" - self.SONGS_SHUFFLED = '🔀 Songs shuffled successfully' - self.ERROR_SHUFFLING = '❌ Error while shuffling the songs' - self.ERROR_MOVING = '❌ Error while moving the songs' - self.LENGTH_ERROR = '❌ Numbers must be between 1 and queue length, use -1 for the last song' - self.ERROR_NUMBER = '❌ This command require a number' - self.ERROR_PLAYING = '❌ Error while playing songs' - self.COMMAND_NOT_FOUND = f'❌ Command not found, type {configs.BOT_PREFIX}help to see all commands' - self.UNKNOWN_ERROR = f'❌ Unknown Error, if needed, use {configs.BOT_PREFIX}reset to reset the player of your server' - self.ERROR_MISSING_ARGUMENTS = f'❌ Missing arguments in this command. Type {configs.BOT_PREFIX}help "command" to see more info about this command' - self.NOT_PREVIOUS = '❌ There is none previous song to play' - self.PLAYER_NOT_PLAYING = f'❌ No song playing. Use {configs.BOT_PREFIX}play to start the player' + self.SONGS_SHUFFLED = f'{self.__emojis.SHUFFLE} Songs shuffled successfully' + self.ERROR_SHUFFLING = f'{self.__emojis.ERROR} Error while shuffling the songs' + self.ERROR_MOVING = f'{self.__emojis.ERROR} Error while moving the songs' + self.LENGTH_ERROR = f'{self.__emojis.ERROR} Numbers must be between 1 and queue length, use -1 for the last song' + self.ERROR_NUMBER = f'{self.__emojis.ERROR} This command require a number' + self.ERROR_PLAYING = f'{self.__emojis.ERROR} Error while playing songs' + self.COMMAND_NOT_FOUND = f'{self.__emojis.ERROR} Command not found, type {configs.BOT_PREFIX}help to see all commands' + self.UNKNOWN_ERROR = f'{self.__emojis.ERROR} Unknown Error, if needed, use {configs.BOT_PREFIX}reset to reset the player of your server' + self.ERROR_MISSING_ARGUMENTS = f'{self.__emojis.ERROR} Missing arguments in this command. Type {configs.BOT_PREFIX}help "command" to see more info about this command' + self.NOT_PREVIOUS = f'{self.__emojis.ERROR} There is none previous song to play' + self.PLAYER_NOT_PLAYING = f'{self.__emojis.ERROR} No song playing. Use {configs.BOT_PREFIX}play to start the player' self.IMPOSSIBLE_MOVE = 'That is impossible :(' self.ERROR_TITLE = 'Error :-(' self.COMMAND_NOT_FOUND_TITLE = 'This is strange :-(' 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.DOWNLOADING_ERROR = "❌ It's impossible to download and play this video" - self.EXTRACTING_ERROR = '❌ An error ocurred while searching for the songs' + 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' - self.ERROR_IN_PROCESS = "❌ Due to a internal error your player was restarted, skipping the song." + self.ERROR_IN_PROCESS = f"{self.__emojis.ERROR} Due to a internal error your player was restarted, skipping the song." self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose' self.BAD_COMMAND_TITLE = 'Misuse of command' - self.BAD_COMMAND = f'❌ Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better' - self.VIDEO_UNAVAILABLE = '❌ Sorry. This video is unavailable for download.' - self.ERROR_DUE_LOOP_ONE_ON = f'❌ This command cannot be executed with loop one activated. Use {configs.BOT_PREFIX}loop off to disable loop.' + self.BAD_COMMAND = f'{self.__emojis.ERROR} Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better' + self.VIDEO_UNAVAILABLE = f'{self.__emojis.ERROR} Sorry. This video is unavailable for download.' + self.ERROR_DUE_LOOP_ONE_ON = f'{self.__emojis.ERROR} This command cannot be executed with loop one activated. Use {configs.BOT_PREFIX}loop off to disable loop.' class SearchMessages(Singleton): def __init__(self) -> None: if not super().created: - config = Configs() + config = VConfigs() self.UNKNOWN_INPUT = f'This type of input was too strange, try something else or type {config.BOT_PREFIX}help play' self.UNKNOWN_INPUT_TITLE = 'Nothing Found' self.GENERIC_TITLE = 'URL could not be processed' diff --git a/DiscordCogs/ControlCog.py b/DiscordCogs/ControlCog.py index e0e62da..a3cce55 100644 --- a/DiscordCogs/ControlCog.py +++ b/DiscordCogs/ControlCog.py @@ -1,24 +1,22 @@ -from discord import Client, Game, Status, Embed -from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument -from discord.ext import commands -from Config.Configs import Configs +from discord import Embed +from discord.ext.commands import Cog, command +from Config.Configs import VConfigs from Config.Helper import Helper -from Config.Messages import Messages -from Config.Colors import Colors -from Views.Embeds import Embeds +from Config.Colors import VColors +from Music.VulkanBot import VulkanBot +from Config.Embeds import VEmbeds helper = Helper() -class ControlCog(commands.Cog): +class ControlCog(Cog): """Class to handle discord events""" - def __init__(self, bot: Client): + def __init__(self, bot: VulkanBot): self.__bot = bot - self.__config = Configs() - self.__messages = Messages() - self.__colors = Colors() - self.__embeds = Embeds() + self.__config = VConfigs() + self.__colors = VColors() + self.__embeds = VEmbeds() self.__commands = { 'MUSIC': ['resume', 'pause', 'loop', 'stop', 'skip', 'play', 'queue', 'clear', @@ -28,28 +26,7 @@ class ControlCog(commands.Cog): } - @commands.Cog.listener() - async def on_ready(self): - print(self.__messages.STARTUP_MESSAGE) - await self.__bot.change_presence(status=Status.online, activity=Game(name=f"Vulkan | {self.__config.BOT_PREFIX}help")) - print(self.__messages.STARTUP_COMPLETE_MESSAGE) - - @commands.Cog.listener() - async def on_command_error(self, ctx, error): - if isinstance(error, MissingRequiredArgument): - embed = self.__embeds.MISSING_ARGUMENTS() - await ctx.send(embed=embed) - - elif isinstance(error, CommandNotFound): - embed = self.__embeds.COMMAND_NOT_FOUND() - await ctx.send(embed=embed) - - else: - print(f'DEVELOPER NOTE -> Command Error: {error}') - embed = self.__embeds.UNKNOWN_ERROR() - await ctx.send(embed=embed) - - @commands.command(name="help", help=helper.HELP_HELP, description=helper.HELP_HELP_LONG, aliases=['h', 'ajuda']) + @command(name="help", help=helper.HELP_HELP, description=helper.HELP_HELP_LONG, aliases=['h', 'ajuda']) async def help_msg(self, ctx, command_help=''): if command_help != '': for command in self.__bot.commands: @@ -97,10 +74,10 @@ class ControlCog(commands.Cog): colour=self.__colors.BLUE ) - embedhelp.set_thumbnail(url=self.__bot.user.avatar_url) + embedhelp.set_thumbnail(url=self.__bot.user.avatar) await ctx.send(embed=embedhelp) - @commands.command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG, aliases=['convite', 'inv', 'convidar']) + @command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG, aliases=['convite', 'inv', 'convidar']) async def invite_bot(self, ctx): invite_url = self.__config.INVITE_URL.format(self.__bot.user.id) txt = self.__config.INVITE_MESSAGE.format(invite_url, invite_url) diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 4f3b137..add48a2 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -1,6 +1,4 @@ -from discord import Guild, Client -from discord.ext import commands -from discord.ext.commands import Context +from discord.ext.commands import Context, command, Cog from Config.Helper import Helper from Handlers.ClearHandler import ClearHandler from Handlers.MoveHandler import MoveHandler @@ -17,214 +15,218 @@ from Handlers.ResumeHandler import ResumeHandler from Handlers.HistoryHandler import HistoryHandler from Handlers.QueueHandler import QueueHandler from Handlers.LoopHandler import LoopHandler -from Views.EmoteView import EmoteView -from Views.EmbedView import EmbedView +from UI.Responses.EmoteCogResponse import EmoteCommandResponse +from UI.Responses.EmbedCogResponse import EmbedCommandResponse +from Music.VulkanBot import VulkanBot +from Config.Configs import VConfigs +from Parallelism.ProcessManager import ProcessManager helper = Helper() -class MusicCog(commands.Cog): +class MusicCog(Cog): """ Class to listen to Music commands It'll listen for commands from discord, when triggered will create a specific Handler for the command Execute the handler and then create a specific View to be showed in Discord """ - def __init__(self, bot) -> None: - self.__bot: Client = bot + def __init__(self, bot: VulkanBot) -> None: + self.__bot: VulkanBot = bot + VConfigs().setProcessManager(ProcessManager(bot)) - @commands.command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) + @command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) async def play(self, ctx: Context, *args) -> None: try: controller = PlayHandler(ctx, self.__bot) response = await controller.run(args) if response is not None: - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas']) + @command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas']) async def queue(self, ctx: Context) -> None: try: controller = QueueHandler(ctx, self.__bot) response = await controller.run() - view2 = EmbedView(response) + view2 = EmbedCommandResponse(response) await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name="skip", help=helper.HELP_SKIP, description=helper.HELP_SKIP_LONG, aliases=['s', 'pular', 'next']) + @command(name="skip", help=helper.HELP_SKIP, description=helper.HELP_SKIP_LONG, aliases=['s', 'pular', 'next']) async def skip(self, ctx: Context) -> None: try: controller = SkipHandler(ctx, self.__bot) response = await controller.run() if response.success: - view = EmoteView(response) + view = EmoteCommandResponse(response) else: - view = EmbedView(response) + view = EmbedCommandResponse(response) await view.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='stop', help=helper.HELP_STOP, description=helper.HELP_STOP_LONG, aliases=['parar']) + @command(name='stop', help=helper.HELP_STOP, description=helper.HELP_STOP_LONG, aliases=['parar']) async def stop(self, ctx: Context) -> None: try: controller = StopHandler(ctx, self.__bot) response = await controller.run() if response.success: - view = EmoteView(response) + view = EmoteCommandResponse(response) else: - view = EmbedView(response) + view = EmbedCommandResponse(response) await view.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar', 'pare']) + @command(name='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar', 'pare']) async def pause(self, ctx: Context) -> None: try: controller = PauseHandler(ctx, self.__bot) response = await controller.run() - view1 = EmoteView(response) - view2 = EmbedView(response) + view1 = EmoteCommandResponse(response) + view2 = EmbedCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='resume', help=helper.HELP_RESUME, description=helper.HELP_RESUME_LONG, aliases=['soltar', 'despausar']) + @command(name='resume', help=helper.HELP_RESUME, description=helper.HELP_RESUME_LONG, aliases=['soltar', 'despausar']) async def resume(self, ctx: Context) -> None: try: controller = ResumeHandler(ctx, self.__bot) response = await controller.run() - view1 = EmoteView(response) - view2 = EmbedView(response) + view1 = EmoteCommandResponse(response) + view2 = EmbedCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='prev', help=helper.HELP_PREV, description=helper.HELP_PREV_LONG, aliases=['anterior', 'return', 'previous', 'back']) + @command(name='prev', help=helper.HELP_PREV, description=helper.HELP_PREV_LONG, aliases=['anterior', 'return', 'previous', 'back']) async def prev(self, ctx: Context) -> None: try: controller = PrevHandler(ctx, self.__bot) response = await controller.run() if response is not None: - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico', 'anteriores', 'hist']) + @command(name='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico', 'anteriores', 'hist']) async def history(self, ctx: Context) -> None: try: controller = HistoryHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='loop', help=helper.HELP_LOOP, description=helper.HELP_LOOP_LONG, aliases=['l', 'repeat']) + @command(name='loop', help=helper.HELP_LOOP, description=helper.HELP_LOOP_LONG, aliases=['l', 'repeat']) async def loop(self, ctx: Context, args='') -> None: try: controller = LoopHandler(ctx, self.__bot) response = await controller.run(args) - view1 = EmoteView(response) - view2 = EmbedView(response) + view1 = EmoteCommandResponse(response) + view2 = EmbedCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='clear', help=helper.HELP_CLEAR, description=helper.HELP_CLEAR_LONG, aliases=['c', 'limpar']) + @command(name='clear', help=helper.HELP_CLEAR, description=helper.HELP_CLEAR_LONG, aliases=['c', 'limpar']) async def clear(self, ctx: Context) -> None: try: controller = ClearHandler(ctx, self.__bot) response = await controller.run() - view = EmoteView(response) + view = EmoteCommandResponse(response) await view.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='np', help=helper.HELP_NP, description=helper.HELP_NP_LONG, aliases=['playing', 'now', 'this']) + @command(name='np', help=helper.HELP_NP, description=helper.HELP_NP_LONG, aliases=['playing', 'now', 'this']) async def now_playing(self, ctx: Context) -> None: try: controller = NowPlayingHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio', 'misturar']) + @command(name='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio', 'misturar']) async def shuffle(self, ctx: Context) -> None: try: controller = ShuffleHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='move', help=helper.HELP_MOVE, description=helper.HELP_MOVE_LONG, aliases=['m', 'mover']) + @command(name='move', help=helper.HELP_MOVE, description=helper.HELP_MOVE_LONG, aliases=['m', 'mover']) async def move(self, ctx: Context, pos1, pos2='1') -> None: try: controller = MoveHandler(ctx, self.__bot) response = await controller.run(pos1, pos2) - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='remove', help=helper.HELP_REMOVE, description=helper.HELP_REMOVE_LONG, aliases=['remover']) + @command(name='remove', help=helper.HELP_REMOVE, description=helper.HELP_REMOVE_LONG, aliases=['remover']) async def remove(self, ctx: Context, position) -> None: try: controller = RemoveHandler(ctx, self.__bot) response = await controller.run(position) - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='reset', help=helper.HELP_RESET, description=helper.HELP_RESET_LONG, aliases=['resetar']) + @command(name='reset', help=helper.HELP_RESET, description=helper.HELP_RESET_LONG, aliases=['resetar']) async def reset(self, ctx: Context) -> None: try: controller = ResetHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: diff --git a/DiscordCogs/RandomCog.py b/DiscordCogs/RandomCog.py index b2ec3fc..7de8a2f 100644 --- a/DiscordCogs/RandomCog.py +++ b/DiscordCogs/RandomCog.py @@ -1,8 +1,8 @@ from random import randint, random -from discord import Client +from Music.VulkanBot import VulkanBot from discord.ext.commands import Context, command, Cog from Config.Helper import Helper -from Views.Embeds import Embeds +from Config.Embeds import VEmbeds helper = Helper() @@ -10,8 +10,8 @@ helper = Helper() class RandomCog(Cog): """Class to listen to commands of type Random""" - def __init__(self, bot: Client): - self.__embeds = Embeds() + def __init__(self, bot: VulkanBot): + self.__embeds = VEmbeds() @command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG, aliases=['rand']) async def random(self, ctx: Context, arg: str) -> None: diff --git a/Handlers/AbstractHandler.py b/Handlers/AbstractHandler.py index 93e7e58..db6729e 100644 --- a/Handlers/AbstractHandler.py +++ b/Handlers/AbstractHandler.py @@ -1,26 +1,31 @@ from abc import ABC, abstractmethod -from typing import List +from typing import List, Union from discord.ext.commands import Context -from discord import Client, Guild, ClientUser, Member +from discord import Client, Guild, ClientUser, Interaction, Member, User from Config.Messages import Messages +from Music.VulkanBot import VulkanBot from Handlers.HandlerResponse import HandlerResponse -from Config.Configs import Configs +from Config.Configs import VConfigs from Config.Helper import Helper -from Views.Embeds import Embeds +from Config.Embeds import VEmbeds class AbstractHandler(ABC): - def __init__(self, ctx: Context, bot: Client) -> None: - self.__bot: Client = bot + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: + self.__bot: VulkanBot = bot self.__guild: Guild = ctx.guild self.__ctx: Context = ctx self.__bot_user: ClientUser = self.__bot.user self.__id = self.__bot_user.id self.__messages = Messages() - self.__config = Configs() + self.__config = VConfigs() self.__helper = Helper() - self.__embeds = Embeds() + self.__embeds = VEmbeds() self.__bot_member: Member = self.__get_member() + if isinstance(ctx, Context): + self.__author = ctx.author + else: + self.__author = ctx.user @abstractmethod async def run(self) -> HandlerResponse: @@ -38,6 +43,10 @@ class AbstractHandler(ABC): def bot_user(self) -> ClientUser: return self.__bot_user + @property + def author(self) -> User: + return self.__author + @property def guild(self) -> Guild: return self.__guild @@ -47,7 +56,7 @@ class AbstractHandler(ABC): return self.__bot @property - def config(self) -> Configs: + def config(self) -> VConfigs: return self.__config @property @@ -59,11 +68,11 @@ class AbstractHandler(ABC): return self.__helper @property - def ctx(self) -> Context: + def ctx(self) -> Union[Context, Interaction]: return self.__ctx @property - def embeds(self) -> Embeds: + def embeds(self) -> VEmbeds: return self.__embeds def __get_member(self) -> Member: diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index df2d822..935725f 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -1,17 +1,18 @@ +from typing import Union +from discord import Interaction from discord.ext.commands import Context -from discord import Client +from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager class ClearHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Clear the playlist @@ -21,7 +22,6 @@ class ClearHandler(AbstractHandler): if acquired: playlist.clear() processLock.release() - processLock.release() return HandlerResponse(self.ctx) else: processManager.resetProcess(self.guild, self.ctx) diff --git a/Handlers/HandlerResponse.py b/Handlers/HandlerResponse.py index 42c5fdf..6840762 100644 --- a/Handlers/HandlerResponse.py +++ b/Handlers/HandlerResponse.py @@ -1,18 +1,18 @@ from typing import Union from discord.ext.commands import Context from Config.Exceptions import VulkanError -from discord import Embed +from discord import Embed, Interaction class HandlerResponse: - def __init__(self, ctx: Context, embed: Embed = None, error: VulkanError = None) -> None: + def __init__(self, ctx: Union[Context, Interaction], embed: Embed = None, error: VulkanError = None) -> None: self.__ctx: Context = ctx self.__error: VulkanError = error self.__embed: Embed = embed self.__success = False if error else True @property - def ctx(self) -> Context: + def ctx(self) -> Union[Context, Interaction]: return self.__ctx @property diff --git a/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py index b33c263..864d90e 100644 --- a/Handlers/HistoryHandler.py +++ b/Handlers/HistoryHandler.py @@ -1,18 +1,19 @@ from discord.ext.commands import Context -from discord import Client +from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Utils.Utils import Utils -from Parallelism.ProcessManager import ProcessManager +from typing import Union +from discord import Interaction class HistoryHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: processLock = processInfo.getLock() diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index b9b0bf4..065334a 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -1,18 +1,19 @@ from discord.ext.commands import Context -from discord import Client +from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage -from Parallelism.ProcessManager import ProcessManager +from typing import Union +from discord import Interaction class LoopHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self, args: str) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/MoveHandler.py b/Handlers/MoveHandler.py index 8ce3ce9..0f36260 100644 --- a/Handlers/MoveHandler.py +++ b/Handlers/MoveHandler.py @@ -1,19 +1,20 @@ from typing import Union from discord.ext.commands import Context -from discord import Client +from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError from Music.Playlist import Playlist -from Parallelism.ProcessManager import ProcessManager +from typing import Union +from discord import Interaction class MoveHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self, pos1: str, pos2: str) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/NowPlayingHandler.py b/Handlers/NowPlayingHandler.py index 93095c9..81dc414 100644 --- a/Handlers/NowPlayingHandler.py +++ b/Handlers/NowPlayingHandler.py @@ -1,19 +1,20 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse +from Music.VulkanBot import VulkanBot from Utils.Cleaner import Cleaner -from Parallelism.ProcessManager import ProcessManager +from typing import Union +from discord import Interaction class NowPlayingHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) self.__cleaner = Cleaner() async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py index 20afe7b..dc80bce 100644 --- a/Handlers/PauseHandler.py +++ b/Handlers/PauseHandler.py @@ -1,22 +1,23 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class PauseHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Send Pause command to be execute by player process command = VCommands(VCommandsType.PAUSE, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index b9636c1..505e793 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -2,20 +2,21 @@ import asyncio from typing import List from Config.Exceptions import DownloadingError, InvalidInput, VulkanError from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Config.Exceptions import ImpossibleMove, UnknownError from Handlers.HandlerResponse import HandlerResponse from Music.Downloader import Downloader from Music.Searcher import Searcher from Music.Song import Song -from Parallelism.ProcessManager import ProcessManager from Parallelism.ProcessInfo import ProcessInfo from Parallelism.Commands import VCommands, VCommandsType +from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class PlayHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) self.__searcher = Searcher() self.__down = Downloader() @@ -36,8 +37,8 @@ class PlayHandler(AbstractHandler): raise InvalidInput(self.messages.INVALID_INPUT, self.messages.ERROR_TITLE) # Get the process context for the current guild - processManager = ProcessManager() - processInfo = processManager.getPlayerInfo(self.guild, self.ctx) + processManager = self.config.getProcessManager() + processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) playlist = processInfo.getPlaylist() process = processInfo.getProcess() if not process.is_alive(): # If process has not yet started, start @@ -72,7 +73,7 @@ class PlayHandler(AbstractHandler): playlist.add_song(song) # Release the acquired Lock processLock.release() - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() playCommand = VCommands(VCommandsType.PLAY, None) queue.put(playCommand) else: @@ -104,7 +105,7 @@ class PlayHandler(AbstractHandler): async def __downloadSongsAndStore(self, songs: List[Song], processInfo: ProcessInfo) -> None: playlist = processInfo.getPlaylist() - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() playCommand = VCommands(VCommandsType.PLAY, None) # Trigger a task for each song to be downloaded tasks: List[asyncio.Task] = [] @@ -113,12 +114,12 @@ class PlayHandler(AbstractHandler): tasks.append(task) # In the original order, await for the task and then if successfully downloaded add in the playlist - processManager = ProcessManager() + processManager = self.config.getProcessManager() for index, task in enumerate(tasks): await task song = songs[index] if not song.problematic: # If downloaded add to the playlist and send play command - processInfo = processManager.getPlayerInfo(self.guild, self.ctx) + processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) processLock = processInfo.getLock() acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) if acquired: diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index a1336ae..b1d63ef 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -1,19 +1,25 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Config.Exceptions import BadCommandUsage, ImpossibleMove from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class PrevHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() - processInfo = processManager.getPlayerInfo(self.guild, self.ctx) + if not self.__user_connected(): + error = ImpossibleMove() + embed = self.embeds.NO_CHANNEL() + return HandlerResponse(self.ctx, embed, error) + + processManager = self.config.getProcessManager() + processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) if not processInfo: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() @@ -25,11 +31,6 @@ class PrevHandler(AbstractHandler): embed = self.embeds.NOT_PREVIOUS_SONG() return HandlerResponse(self.ctx, embed, error) - if not self.__user_connected(): - error = ImpossibleMove() - embed = self.embeds.NO_CHANNEL() - return HandlerResponse(self.ctx, embed, error) - if playlist.isLoopingAll() or playlist.isLoopingOne(): error = BadCommandUsage() embed = self.embeds.FAIL_DUE_TO_LOOP_ON() @@ -41,13 +42,13 @@ class PrevHandler(AbstractHandler): process.start() # Send a prev command, together with the user voice channel - prevCommand = VCommands(VCommandsType.PREV, self.ctx.author.voice.channel.id) - queue = processInfo.getQueue() + prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id) + queue = processInfo.getQueueToPlayer() queue.put(prevCommand) return HandlerResponse(self.ctx) def __user_connected(self) -> bool: - if self.ctx.author.voice: + if self.author.voice: return True else: return False diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index 953287a..db5ebb2 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -1,20 +1,21 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Music.Downloader import Downloader from Utils.Utils import Utils -from Parallelism.ProcessManager import ProcessManager +from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class QueueHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) self.__down = Downloader() async def run(self) -> HandlerResponse: # Retrieve the process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: # If no process return empty list embed = self.embeds.EMPTY_QUEUE() diff --git a/Handlers/RemoveHandler.py b/Handlers/RemoveHandler.py index c611f90..b55d61a 100644 --- a/Handlers/RemoveHandler.py +++ b/Handlers/RemoveHandler.py @@ -1,20 +1,21 @@ from typing import Union from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired from Music.Playlist import Playlist -from Parallelism.ProcessManager import ProcessManager +from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class RemoveHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self, position: str) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: # Clear the playlist diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py index c938d21..272f02c 100644 --- a/Handlers/ResetHandler.py +++ b/Handlers/ResetHandler.py @@ -1,22 +1,23 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class ResetHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: command = VCommands(VCommandsType.RESET, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/ResumeHandler.py b/Handlers/ResumeHandler.py index bd5c254..da40bc0 100644 --- a/Handlers/ResumeHandler.py +++ b/Handlers/ResumeHandler.py @@ -1,22 +1,23 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class ResumeHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Send Resume command to be execute by player process command = VCommands(VCommandsType.RESUME, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py index 94999c8..3f38b8b 100644 --- a/Handlers/ShuffleHandler.py +++ b/Handlers/ShuffleHandler.py @@ -1,17 +1,18 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import UnknownError -from Parallelism.ProcessManager import ProcessManager +from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class ShuffleHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: try: diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index ab0355d..0640b85 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -1,18 +1,24 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler -from Config.Exceptions import BadCommandUsage +from Config.Exceptions import BadCommandUsage, ImpossibleMove from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager +from Music.VulkanBot import VulkanBot from Parallelism.Commands import VCommands, VCommandsType +from typing import Union +from discord import Interaction class SkipHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + if not self.__user_connected(): + error = ImpossibleMove() + embed = self.embeds.NO_CHANNEL() + return HandlerResponse(self.ctx, embed, error) + + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Verify if there is a running process playlist = processInfo.getPlaylist() @@ -23,10 +29,16 @@ class SkipHandler(AbstractHandler): # Send a command to the player process to skip the music command = VCommands(VCommandsType.SKIP, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) else: embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) + + def __user_connected(self) -> bool: + if self.author.voice: + return True + else: + return False diff --git a/Handlers/StopHandler.py b/Handlers/StopHandler.py index 669a953..89c9e09 100644 --- a/Handlers/StopHandler.py +++ b/Handlers/StopHandler.py @@ -1,22 +1,23 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager +from Music.VulkanBot import VulkanBot from Parallelism.Commands import VCommands, VCommandsType +from typing import Union +from discord import Interaction class StopHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Send command to player process stop command = VCommands(VCommandsType.STOP, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Music/Downloader.py b/Music/Downloader.py index 3b97c8d..97a22d0 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -1,6 +1,6 @@ import asyncio from typing import List -from Config.Configs import Configs +from Config.Configs import VConfigs from yt_dlp import YoutubeDL, DownloadError from concurrent.futures import ThreadPoolExecutor from Music.Song import Song @@ -9,7 +9,7 @@ from Config.Exceptions import DownloadingError class Downloader: - config = Configs() + config = VConfigs() __YDL_OPTIONS = {'format': 'bestaudio/best', 'default_search': 'auto', 'playliststart': 0, @@ -34,7 +34,7 @@ class Downloader: __BASE_URL = 'https://www.youtube.com/watch?v={}' def __init__(self) -> None: - self.__config = Configs() + self.__config = VConfigs() self.__music_keys_only = ['resolution', 'fps', 'quality'] self.__not_extracted_keys_only = ['ie_key'] self.__not_extracted_not_keys = ['entries'] diff --git a/Music/MessagesController.py b/Music/MessagesController.py new file mode 100644 index 0000000..0e8567a --- /dev/null +++ b/Music/MessagesController.py @@ -0,0 +1,65 @@ +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 33bcbce..c86b3cd 100644 --- a/Music/Playlist.py +++ b/Music/Playlist.py @@ -1,6 +1,6 @@ from collections import deque from typing import List -from Config.Configs import Configs +from Config.Configs import VConfigs from Music.Song import Song import random @@ -8,7 +8,7 @@ import random class Playlist: def __init__(self) -> None: - self.__configs = Configs() + self.__configs = VConfigs() self.__queue = deque() # Store the musics to play self.__songs_history = deque() # Store the musics played @@ -62,7 +62,7 @@ class Playlist: # Att played song info if played_song != None: if not self.__looping_one and not self.__looping_all: - if played_song.problematic == False: + if not played_song.problematic: self.__songs_history.appendleft(played_song) if len(self.__songs_history) > self.__configs.MAX_SONGS_HISTORY: diff --git a/Music/SpotifySearcher.py b/Music/SpotifySearcher.py index f663b3a..c423dd5 100644 --- a/Music/SpotifySearcher.py +++ b/Music/SpotifySearcher.py @@ -2,14 +2,14 @@ from spotipy import Spotify from spotipy.oauth2 import SpotifyClientCredentials from spotipy.exceptions import SpotifyException from Config.Exceptions import SpotifyError -from Config.Configs import Configs +from Config.Configs import VConfigs from Config.Messages import SpotifyMessages class SpotifySearch(): def __init__(self) -> None: self.__messages = SpotifyMessages() - self.__config = Configs() + self.__config = VConfigs() self.__connected = False self.__connect() diff --git a/Music/VulkanBot.py b/Music/VulkanBot.py new file mode 100644 index 0000000..8a8ba2a --- /dev/null +++ b/Music/VulkanBot.py @@ -0,0 +1,73 @@ +from asyncio import AbstractEventLoop +from discord import Guild, Status, Game, Message +from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument +from Config.Configs import VConfigs +from discord.ext.commands import Bot, Context +from Config.Messages import Messages +from Config.Embeds import VEmbeds + + +class VulkanBot(Bot): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__configs = VConfigs() + self.__messages = Messages() + self.__embeds = VEmbeds() + self.remove_command("help") + + def startBot(self) -> None: + """Blocking function that will start the bot""" + if self.__configs.BOT_TOKEN == '': + print('DEVELOPER NOTE -> Token not found') + exit() + + super().run(self.__configs.BOT_TOKEN, reconnect=True) + + async def startBotCoro(self, loop: AbstractEventLoop) -> None: + """Start a bot coroutine, does not wait for connection to be established""" + task = loop.create_task(self.__login()) + await task + loop.create_task(self.__connect()) + + async def __login(self): + """Coroutine to login the Bot in discord""" + await self.login(token=self.__configs.BOT_TOKEN) + + async def __connect(self): + """Coroutine to connect the Bot in discord""" + await self.connect(reconnect=True) + + async def on_ready(self): + print(self.__messages.STARTUP_MESSAGE) + await self.change_presence(status=Status.online, activity=Game(name=f"Vulkan | {self.__configs.BOT_PREFIX}help")) + print(self.__messages.STARTUP_COMPLETE_MESSAGE) + + async def on_command_error(self, ctx, error): + if isinstance(error, MissingRequiredArgument): + embed = self.__embeds.MISSING_ARGUMENTS() + await ctx.send(embed=embed) + + elif isinstance(error, CommandNotFound): + embed = self.__embeds.COMMAND_NOT_FOUND() + await ctx.send(embed=embed) + + else: + print(f'DEVELOPER NOTE -> Command Error: {error}') + embed = self.__embeds.UNKNOWN_ERROR() + await ctx.send(embed=embed) + + async def process_commands(self, message: Message): + if message.author.bot: + return + + ctx = await self.get_context(message, cls=Context) + + if ctx.valid and not message.guild: + return + + await self.invoke(ctx) + + +class Context(Context): + bot: VulkanBot + guild: Guild diff --git a/Music/VulkanInitializer.py b/Music/VulkanInitializer.py new file mode 100644 index 0000000..9f847cc --- /dev/null +++ b/Music/VulkanInitializer.py @@ -0,0 +1,55 @@ +from random import choices +import string +from discord.bot import Bot +from discord import Intents +from Music.VulkanBot import VulkanBot +from os import listdir +from Config.Configs import VConfigs +from Config.Exceptions import VulkanError + + +class VulkanInitializer: + def __init__(self, willListen: bool) -> None: + self.__config = VConfigs() + self.__intents = Intents.default() + self.__intents.message_content = True + self.__intents.members = True + self.__bot = self.__create_bot(willListen) + self.__add_cogs(self.__bot) + + def getBot(self) -> VulkanBot: + return self.__bot + + def __create_bot(self, willListen: bool) -> VulkanBot: + if willListen: + prefix = self.__config.BOT_PREFIX + else: + prefix = ''.join(choices(string.ascii_uppercase + string.digits, k=4)) + + bot = VulkanBot(command_prefix=prefix, + pm_help=True, + case_insensitive=True, + intents=self.__intents) + return bot + + def __add_cogs(self, bot: Bot) -> None: + try: + cogsStatus = [] + for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): + if filename.endswith('.py'): + cogPath = f'{self.__config.COMMANDS_PATH}.{filename[:-3]}' + 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: + print(f'[Error Loading Vulkan] -> {e.message}') + + def __getTotalCogs(self) -> int: + quant = 0 + for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): + if filename.endswith('.py'): + quant += 1 + return quant diff --git a/Parallelism/Commands.py b/Parallelism/Commands.py index d6c8baa..9a18a09 100644 --- a/Parallelism/Commands.py +++ b/Parallelism/Commands.py @@ -11,6 +11,9 @@ class VCommandsType(Enum): PLAY = 'Play' STOP = 'Stop' RESET = 'Reset' + NOW_PLAYING = 'Now Playing' + TERMINATE = 'Terminate' + SLEEPING = 'Sleeping' class VCommands: diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 3fc49ee..d47aff0 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -1,17 +1,17 @@ import asyncio -from os import listdir -from discord import Intents, User, Member, Message, Embed -from asyncio import AbstractEventLoop, Semaphore -from multiprocessing import Process, Queue, RLock -from threading import Lock, Thread +from Music.VulkanInitializer import VulkanInitializer +from discord import User, Member, Message +from asyncio import AbstractEventLoop, Semaphore, Queue +from multiprocessing import Process, RLock, Lock, Queue +from threading import Thread from typing import Callable, List -from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel +from discord import Guild, FFmpegPCMAudio, VoiceChannel, TextChannel from Music.Playlist import Playlist from Music.Song import Song -from Config.Configs import Configs +from Config.Configs import VConfigs from Config.Messages import Messages -from discord.ext.commands import Bot -from Views.Embeds import Embeds +from Music.VulkanBot import VulkanBot +from Config.Embeds import VEmbeds from Parallelism.Commands import VCommands, VCommandsType @@ -21,7 +21,7 @@ class TimeoutClock: self.__task = loop.create_task(self.__executor()) async def __executor(self): - await asyncio.sleep(Configs().VC_TIMEOUT) + await asyncio.sleep(VConfigs().VC_TIMEOUT) await self.__callback() def cancel(self): @@ -31,7 +31,7 @@ class TimeoutClock: class PlayerProcess(Process): """Process that will play songs, receive commands from the main process by a Queue""" - def __init__(self, name: str, playlist: Playlist, lock: Lock, queue: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None: + def __init__(self, name: str, playlist: Playlist, lock: Lock, queueToReceive: Queue, queueToSend: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None: """ Start a new process that will have his own bot instance Due to pickle serialization, no objects are stored, the values initialization are being made in the run method @@ -40,7 +40,8 @@ class PlayerProcess(Process): # Synchronization objects self.__playlist: Playlist = playlist self.__playlistLock: Lock = lock - self.__queue: Queue = queue + self.__queueReceive: Queue = queueToReceive + self.__queueSend: Queue = queueToSend self.__semStopPlaying: Semaphore = None self.__loop: AbstractEventLoop = None # Discord context ID @@ -50,14 +51,14 @@ class PlayerProcess(Process): self.__authorID = authorID # All information of discord context will be retrieved directly with discord API self.__guild: Guild = None - self.__bot: Client = None + self.__bot: VulkanBot = None self.__voiceChannel: VoiceChannel = None self.__textChannel: TextChannel = None self.__author: User = None self.__botMember: Member = None - self.__configs: Configs = None - self.__embeds: Embeds = None + self.__configs: VConfigs = None + self.__embeds: VEmbeds = None self.__messages: Messages = None self.__messagesToDelete: List[Message] = [] self.__playing = False @@ -70,11 +71,12 @@ class PlayerProcess(Process): try: print(f'Starting Process {self.name}') self.__playerLock = RLock() - self.__loop = asyncio.get_event_loop() + self.__loop = asyncio.get_event_loop_policy().new_event_loop() + asyncio.set_event_loop(self.__loop) - self.__configs = Configs() + self.__configs = VConfigs() self.__messages = Messages() - self.__embeds = Embeds() + self.__embeds = VEmbeds() self.__semStopPlaying = Semaphore(0) self.__loop.run_until_complete(self._run()) @@ -107,9 +109,13 @@ class PlayerProcess(Process): self.__timer.cancel() async def __playPlaylistSongs(self) -> None: + """If the player is not running trigger to play a new song""" if not self.__playing: + song = None with self.__playlistLock: - song = self.__playlist.next_song() + with self.__playerLock: + if not (self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused()): + song = self.__playlist.next_song() if song is not None: self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') @@ -141,8 +147,8 @@ class PlayerProcess(Process): self.__timer.cancel() self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) - await self.__deletePrevNowPlaying() - await self.__showNowPlaying() + nowPlayingCommand = VCommands(VCommandsType.NOW_PLAYING, song) + self.__queueSend.put(nowPlayingCommand) except Exception as e: print(f'[ERROR IN PLAY SONG] -> {e}, {type(e)}') self.__playNext(None) @@ -150,44 +156,49 @@ class PlayerProcess(Process): self.__playerLock.release() def __playNext(self, error) -> None: - with self.__playerLock: - if self.__forceStop: # If it's forced to stop player - self.__forceStop = False - return None + with self.__playlistLock: + with self.__playerLock: + if self.__forceStop: # If it's forced to stop player + self.__forceStop = False + return None - with self.__playlistLock: song = self.__playlist.next_song() - if song is not None: - self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') - else: - with self.__playlistLock: + if song is not None: + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + else: self.__playlist.loop_off() - self.__playingSong = None - self.__playing = False + self.__playingSong = None + self.__playing = False + # Send a command to the main process put this one to sleep + sleepCommand = VCommands(VCommandsType.SLEEPING) + self.__queueSend.put(sleepCommand) + # Release the semaphore to finish the process + self.__semStopPlaying.release() async def __playPrev(self, voiceChannelID: int) -> None: with self.__playlistLock: song = self.__playlist.prev_song() - if song is not None: - if self.__guild.voice_client is None: # If not connect, connect to the user voice channel - self.__voiceChannelID = voiceChannelID - self.__voiceChannel = self.__guild.get_channel(self.__voiceChannelID) - await self.__connectToVoiceChannel() + with self.__playerLock: + if song is not None: + if self.__guild.voice_client is None: # If not connect, connect to the user voice channel + self.__voiceChannelID = voiceChannelID + self.__voiceChannel = self.__guild.get_channel(self.__voiceChannelID) + await self.__connectToVoiceChannel() - # If already playing, stop the current play - if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): - # Will forbidden next_song to execute after stopping current player - self.__forceStop = True - self.__guild.voice_client.stop() - self.__playing = False + # If already playing, stop the current play + if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): + # Will forbidden next_song to execute after stopping current player + self.__forceStop = True + self.__guild.voice_client.stop() + self.__playing = False - self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') def __commandsReceiver(self) -> None: while True: - command: VCommands = self.__queue.get() + command: VCommands = self.__queueReceive.get() type = command.getType() args = command.getArgs() @@ -235,9 +246,11 @@ class PlayerProcess(Process): if self.__guild.voice_client is not None: if self.__guild.voice_client.is_connected(): with self.__playlistLock: - self.__playlist.clear() self.__playlist.loop_off() + # Send a command to the main process put this to sleep + sleepCommand = VCommands(VCommandsType.SLEEPING) + self.__queueSend.put(sleepCommand) self.__guild.voice_client.stop() self.__playingSong = None await self.__guild.voice_client.disconnect() @@ -269,30 +282,13 @@ class PlayerProcess(Process): self.__playlist.clear() self.__playlist.loop_off() - async def __createBotInstance(self) -> Client: - """Load a new bot instance that should not be directly called. - Get the guild, voice and text Channel in discord API using IDs passed in constructor - """ - intents = Intents.default() - intents.members = True - bot = Bot(command_prefix='Rafael', - pm_help=True, - case_insensitive=True, - intents=intents) - bot.remove_command('help') + async def __createBotInstance(self) -> VulkanBot: + """Load a new bot instance that should not be directly called.""" + initializer = VulkanInitializer(willListen=False) + bot = initializer.getBot() - # Add the Cogs for this bot too - for filename in listdir(f'./{self.__configs.COMMANDS_PATH}'): - if filename.endswith('.py'): - bot.load_extension(f'{self.__configs.COMMANDS_PATH}.{filename[:-3]}') - - # Login and connect the bot instance to discord API - task = self.__loop.create_task(bot.login(token=self.__configs.BOT_TOKEN, bot=True)) - await task - self.__loop.create_task(bot.connect(reconnect=True)) - # Sleep to wait connection to be established + await bot.startBotCoro(self.__loop) await self.__ensureDiscordConnection(bot) - return bot async def __timeoutHandler(self) -> None: @@ -301,21 +297,36 @@ class PlayerProcess(Process): return if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): - self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) + if not self.__isBotAloneInChannel(): # If bot is not alone continue to play + self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) + return - elif self.__guild.voice_client.is_connected(): + # Finish the process + if self.__guild.voice_client.is_connected(): with self.__playerLock: with self.__playlistLock: - self.__playlist.clear() self.__playlist.loop_off() self.__playing = False await self.__guild.voice_client.disconnect() + # Send command to main process to finish this one + sleepCommand = VCommands(VCommandsType.SLEEPING) + self.__queueSend.put(sleepCommand) # Release semaphore to finish process self.__semStopPlaying.release() except Exception as e: print(f'[Error in Timeout] -> {e}') - async def __ensureDiscordConnection(self, bot: Client) -> None: + def __isBotAloneInChannel(self) -> bool: + try: + if len(self.__guild.voice_client.channel.members) <= 1: + return True + else: + return False + except Exception as e: + print(f'[ERROR IN CHECK BOT ALONE] -> {e}') + return False + + async def __ensureDiscordConnection(self, bot: VulkanBot) -> None: """Await in this point until connection to discord is established""" guild = None while guild is None: @@ -335,46 +346,3 @@ class PlayerProcess(Process): for member in guild_members: if member.id == self.__bot.user.id: return member - - async def __showNowPlaying(self) -> None: - # Get the lock of the playlist - with self.__playlistLock: - if not self.__playing or self.__playingSong is None: - embed = self.__embeds.NOT_PLAYING() - await self.__textChannel.send(embed=embed) - return - - if self.__playlist.isLoopingOne(): - title = self.__messages.ONE_SONG_LOOPING - else: - title = self.__messages.SONG_PLAYING - - info = self.__playingSong.info - embed = self.__embeds.SONG_INFO(info, title) - await self.__textChannel.send(embed=embed) - self.__messagesToDelete.append(await self.__getSendedMessage()) - - async def __deletePrevNowPlaying(self) -> None: - for message in self.__messagesToDelete: - try: - await message.delete() - except: - pass - self.__messagesToDelete.clear() - - async def __getSendedMessage(self) -> Message: - stringToIdentify = 'Uploader:' - last_messages: List[Message] = await self.__textChannel.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/Parallelism/ProcessInfo.py b/Parallelism/ProcessInfo.py index 0f8bf83..011f638 100644 --- a/Parallelism/ProcessInfo.py +++ b/Parallelism/ProcessInfo.py @@ -1,4 +1,5 @@ from multiprocessing import Process, Queue, Lock +from discord import TextChannel from Music.Playlist import Playlist @@ -7,11 +8,13 @@ class ProcessInfo: Class to store the reference to all structures to maintain a player process """ - def __init__(self, process: Process, queue: Queue, playlist: Playlist, lock: Lock) -> None: + def __init__(self, process: Process, queueToPlayer: Queue, queueToMain: Queue, playlist: Playlist, lock: Lock, textChannel: TextChannel) -> None: self.__process = process - self.__queue = queue + self.__queueToPlayer = queueToPlayer + self.__queueToMain = queueToMain self.__playlist = playlist self.__lock = lock + self.__textChannel = textChannel def setProcess(self, newProcess: Process) -> None: self.__process = newProcess @@ -19,11 +22,17 @@ class ProcessInfo: def getProcess(self) -> Process: return self.__process - def getQueue(self) -> Queue: - return self.__queue + def getQueueToPlayer(self) -> Queue: + return self.__queueToPlayer + + def getQueueToMain(self) -> Queue: + return self.__queueToMain def getPlaylist(self) -> Playlist: return self.__playlist def getLock(self) -> Lock: return self.__lock + + def getTextChannel(self) -> TextChannel: + return self.__textChannel diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index 6df1b73..3136474 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -1,13 +1,19 @@ -from multiprocessing import Queue, Lock +import asyncio +from multiprocessing import Lock, Queue from multiprocessing.managers import BaseManager, NamespaceProxy -from typing import Dict +from queue import Empty +from threading import Thread +from typing import Dict, Tuple, Union from Config.Singleton import Singleton -from discord import Guild +from discord import Guild, Interaction from discord.ext.commands import Context +from Music.MessagesController import MessagesController +from Music.Song import Song from Parallelism.PlayerProcess import PlayerProcess from Music.Playlist import Playlist from Parallelism.ProcessInfo import ProcessInfo from Parallelism.Commands import VCommands, VCommandsType +from Music.VulkanBot import VulkanBot class ProcessManager(Singleton): @@ -16,25 +22,28 @@ class ProcessManager(Singleton): Deal with the creation of shared memory """ - def __init__(self) -> None: + def __init__(self, bot: VulkanBot = None) -> None: if not super().created: + self.__bot = bot VManager.register('Playlist', Playlist) self.__manager = VManager() self.__manager.start() self.__playersProcess: Dict[Guild, ProcessInfo] = {} + self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {} + self.__playersMessages: Dict[Guild, MessagesController] = {} - def setPlayerContext(self, guild: Guild, context: ProcessInfo): - self.__playersProcess[guild.id] = context + def setPlayerInfo(self, guild: Guild, info: ProcessInfo): + self.__playersProcess[guild.id] = info - def getPlayerInfo(self, guild: Guild, context: Context) -> ProcessInfo: - """Return the process info for the guild, if not, create one""" + def getOrCreatePlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: + """Return the process info for the guild, the user in context must be connected to a voice_channel""" try: if guild.id not in self.__playersProcess.keys(): - self.__playersProcess[guild.id] = self.__createProcessInfo(context) + self.__playersProcess[guild.id] = self.__createProcessInfo(guild, context) else: # If the process has ended create a new one if not self.__playersProcess[guild.id].getProcess().is_alive(): - self.__playersProcess[guild.id] = self.__recreateProcess(context) + self.__playersProcess[guild.id] = self.__recreateProcess(guild, context) return self.__playersProcess[guild.id] except Exception as e: @@ -46,11 +55,11 @@ class ProcessManager(Singleton): return None # Recreate the process keeping the playlist - newProcessInfo = self.__recreateProcess(context) + newProcessInfo = self.__recreateProcess(guild, context) newProcessInfo.getProcess().start() # Start the process # Send a command to start the play again playCommand = VCommands(VCommandsType.PLAY) - newProcessInfo.getQueue().put(playCommand) + newProcessInfo.getQueueToPlayer().put(playCommand) self.__playersProcess[guild.id] = newProcessInfo def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo: @@ -60,7 +69,7 @@ class ProcessManager(Singleton): return self.__playersProcess[guild.id] - def __createProcessInfo(self, context: Context) -> ProcessInfo: + def __createProcessInfo(self, guild: Guild, context: Context) -> ProcessInfo: guildID: int = context.guild.id textID: int = context.channel.id voiceID: int = context.author.voice.channel.id @@ -68,30 +77,105 @@ class ProcessManager(Singleton): playlist: Playlist = self.__manager.Playlist() lock = Lock() - queue = Queue() - process = PlayerProcess(context.guild.name, playlist, lock, queue, - guildID, textID, voiceID, authorID) - processInfo = ProcessInfo(process, queue, playlist, lock) + queueToListen = Queue() + queueToSend = Queue() + process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, + queueToListen, guildID, textID, voiceID, authorID) + processInfo = ProcessInfo(process, queueToSend, queueToListen, + playlist, lock, context.channel) + + # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async + thread = Thread(target=self.__listenToCommands, + args=(queueToListen, guild), daemon=True) + self.__playersListeners[guildID] = (thread, False) + thread.start() + + # Create a Message Controller for this player + self.__playersMessages[guildID] = MessagesController(self.__bot) return processInfo - def __recreateProcess(self, context: Context) -> ProcessInfo: + def __recreateProcess(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: """Create a new process info using previous playlist""" guildID: int = context.guild.id textID: int = context.channel.id - voiceID: int = context.author.voice.channel.id - authorID: int = context.author.id + if isinstance(context, Interaction): + authorID: int = context.user.id + voiceID: int = context.user.voice.channel.id + else: + authorID: int = context.author.id + voiceID: int = context.author.voice.channel.id playlist: Playlist = self.__playersProcess[guildID].getPlaylist() lock = Lock() - queue = Queue() + queueToListen = Queue() + queueToSend = Queue() + process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, + queueToListen, guildID, textID, voiceID, authorID) + processInfo = ProcessInfo(process, queueToSend, queueToListen, + playlist, lock, context.channel) - process = PlayerProcess(context.guild.name, playlist, lock, queue, - guildID, textID, voiceID, authorID) - processInfo = ProcessInfo(process, queue, playlist, lock) + # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async + thread = Thread(target=self.__listenToCommands, + args=(queueToListen, guild), daemon=True) + self.__playersListeners[guildID] = (thread, False) + thread.start() return processInfo + def __listenToCommands(self, queue: Queue, guild: Guild) -> None: + guildID = guild.id + while True: + shouldEnd = self.__playersListeners[guildID][1] + if shouldEnd: + break + + try: + command: VCommands = queue.get(timeout=5) + commandType = command.getType() + args = command.getArgs() + + print(f'Process {guild.name} sended command {commandType}') + if commandType == VCommandsType.NOW_PLAYING: + asyncio.run_coroutine_threadsafe(self.showNowPlaying( + guild.id, args), self.__bot.loop) + elif commandType == VCommandsType.TERMINATE: + # Delete the process elements and return, to finish task + self.__terminateProcess(guildID) + return + elif commandType == VCommandsType.SLEEPING: + # The process might be used again + self.__sleepingProcess(guildID) + return + else: + print(f'[ERROR] -> Unknown Command Received from Process: {commandType}') + except Empty: + continue + except Exception as e: + print(f'[ERROR IN LISTENING PROCESS] -> {guild.name} - {e}') + + def __terminateProcess(self, guildID: int) -> None: + # Delete all structures associated with the Player + del self.__playersProcess[guildID] + del self.__playersMessages[guildID] + threadListening = self.__playersListeners[guildID] + threadListening._stop() + del self.__playersListeners[guildID] + + def __sleepingProcess(self, guildID: int) -> None: + # Disable all process structures, except Playlist + queue1 = self.__playersProcess[guildID].getQueueToMain() + queue2 = self.__playersProcess[guildID].getQueueToPlayer() + queue1.close() + queue1.join_thread() + queue2.close() + queue2.join_thread() + + async def showNowPlaying(self, guildID: int, song: Song) -> None: + messagesController = self.__playersMessages[guildID] + processInfo = self.__playersProcess[guildID] + await messagesController.sendNowPlaying(processInfo, song) + class VManager(BaseManager): pass diff --git a/README.md b/README.md index e7bf7f3..ccc634e 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,48 @@ -# **Vulkan** - -A Music Discord bot, written in Python, that plays *Youtube*, *Spotify* and *Deezer* links. Vulkan was designed so that anyone can fork this project, follow the instructions and use it in their own way, Vulkan can also be configured in Heroku to work 24/7. +
+
+
+
+