diff --git a/Config/Configs.py b/Config/Configs.py index ef3eecc..742a3e3 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -16,7 +16,7 @@ class Configs(Singleton): '[ERROR] -> You must create and .env file with all required fields, see documentation for help') self.CLEANER_MESSAGES_QUANT = 5 - self.COMMANDS_PATH = 'Commands' + self.COMMANDS_PATH = 'DiscordCogs' self.VC_TIMEOUT = 600 self.MAX_PLAYLIST_LENGTH = 50 diff --git a/Config/Messages.py b/Config/Messages.py index e89bc56..883925e 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -42,6 +42,8 @@ class Messages(Singleton): 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.SONGS_SHUFFLED = '🔀 Songs shuffled successfully' self.ERROR_SHUFFLING = '❌ Error while shuffling the songs' diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 8d32b4b..6b3baec 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -21,7 +21,6 @@ from Handlers.QueueHandler import QueueHandler from Handlers.LoopHandler import LoopHandler from Views.EmoteView import EmoteView from Views.EmbedView import EmbedView -from Parallelism.ProcessManager import ProcessManager helper = Helper() @@ -120,7 +119,7 @@ class MusicCog(commands.Cog): await view1.run() await view2.run() - @commands.command(name='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico', 'h']) + @commands.command(name='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico', 'anteriores', 'hist']) async def history(self, ctx: Context) -> None: controller = HistoryHandler(ctx, self.__bot) @@ -158,7 +157,7 @@ class MusicCog(commands.Cog): await view1.run() await view2.run() - @commands.command(name='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio']) + @commands.command(name='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio', 'misturar']) async def shuffle(self, ctx: Context) -> None: controller = ShuffleHandler(ctx, self.__bot) diff --git a/DiscordCogs/RandomCog.py b/DiscordCogs/RandomCog.py index 52f09f5..b2ec3fc 100644 --- a/DiscordCogs/RandomCog.py +++ b/DiscordCogs/RandomCog.py @@ -13,7 +13,7 @@ class RandomCog(Cog): def __init__(self, bot: Client): self.__embeds = Embeds() - @command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG, aliases=['rand', 'aleatorio']) + @command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG, aliases=['rand']) async def random(self, ctx: Context, arg: str) -> None: try: arg = int(arg) diff --git a/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py index 515001f..ffa2255 100644 --- a/Handlers/HistoryHandler.py +++ b/Handlers/HistoryHandler.py @@ -3,6 +3,7 @@ from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Utils.Utils import Utils +from Parallelism.ProcessManager import ProcessManager class HistoryHandler(AbstractHandler): @@ -10,11 +11,18 @@ class HistoryHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - history = self.player.playlist.getSongsHistory() + # Get the current process of the guild + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if processContext: + with processContext.getLock(): + playlist = processContext.getPlaylist() + history = playlist.getSongsHistory() + else: + history = [] if len(history) == 0: text = self.messages.HISTORY_EMPTY - else: text = f'\n📜 History Length: {len(history)} | Max: {self.config.MAX_SONGS_HISTORY}\n' for pos, song in enumerate(history, start=1): diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index 45334dc..64c5fb4 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -3,6 +3,7 @@ from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage +from Parallelism.ProcessManager import ProcessManager class LoopHandler(AbstractHandler): @@ -10,30 +11,41 @@ class LoopHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self, args: str) -> HandlerResponse: - if args == '' or args is None: - self.player.playlist.loop_all() - embed = self.embeds.LOOP_ALL_ACTIVATED() - return HandlerResponse(self.ctx, embed) - - args = args.lower() - if self.player.playlist.getCurrentSong() is None: + # Get the current process of the guild + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if not processContext: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() return HandlerResponse(self.ctx, embed, error) - if args == 'one': - self.player.playlist.loop_one() - embed = self.embeds.LOOP_ONE_ACTIVATED() - return HandlerResponse(self.ctx, embed) - elif args == 'all': - self.player.playlist.loop_all() - embed = self.embeds.LOOP_ALL_ACTIVATED() - return HandlerResponse(self.ctx, embed) - elif args == 'off': - self.player.playlist.loop_off() - embed = self.embeds.LOOP_DISABLE() - return HandlerResponse(self.ctx, embed) - else: - error = BadCommandUsage() - embed = self.embeds.BAD_LOOP_USE() - return HandlerResponse(self.ctx, embed, error) + playlist = processContext.getPlaylist() + + with processContext.getLock(): + if args == '' or args is None: + playlist.loop_all() + embed = self.embeds.LOOP_ALL_ACTIVATED() + return HandlerResponse(self.ctx, embed) + + args = args.lower() + if playlist.getCurrentSong() is None: + embed = self.embeds.NOT_PLAYING() + error = BadCommandUsage() + return HandlerResponse(self.ctx, embed, error) + + if args == 'one': + playlist.loop_one() + embed = self.embeds.LOOP_ONE_ACTIVATED() + return HandlerResponse(self.ctx, embed) + elif args == 'all': + playlist.loop_all() + embed = self.embeds.LOOP_ALL_ACTIVATED() + return HandlerResponse(self.ctx, embed) + elif args == 'off': + playlist.loop_off() + embed = self.embeds.LOOP_DISABLE() + return HandlerResponse(self.ctx, embed) + else: + error = BadCommandUsage() + embed = self.embeds.BAD_LOOP_USE() + return HandlerResponse(self.ctx, embed, error) diff --git a/Handlers/NowPlayingHandler.py b/Handlers/NowPlayingHandler.py index 664a427..cef508b 100644 --- a/Handlers/NowPlayingHandler.py +++ b/Handlers/NowPlayingHandler.py @@ -3,6 +3,7 @@ from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Utils.Cleaner import Cleaner +from Parallelism.ProcessManager import ProcessManager class NowPlayingHandler(AbstractHandler): @@ -11,16 +12,24 @@ class NowPlayingHandler(AbstractHandler): self.__cleaner = Cleaner() async def run(self) -> HandlerResponse: - if not self.player.playing: + # Get the current process of the guild + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if not processContext: embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) - if self.player.playlist.isLoopingOne(): + playlist = processContext.getPlaylist() + if playlist.getCurrentSong() is None: + embed = self.embeds.NOT_PLAYING() + return HandlerResponse(self.ctx, embed) + + if playlist.isLoopingOne(): title = self.messages.ONE_SONG_LOOPING else: title = self.messages.SONG_PLAYING await self.__cleaner.clean_messages(self.ctx, self.config.CLEANER_MESSAGES_QUANT) - info = self.player.playlist.getCurrentSong().info + info = playlist.getCurrentSong().info embed = self.embeds.SONG_INFO(info, title) return HandlerResponse(self.ctx, embed) diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index 85996c1..8f3a986 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -58,7 +58,7 @@ class PlayHandler(AbstractHandler): response = HandlerResponse(self.ctx, embed) # Get the process context for the current guild - manager = ProcessManager(self.bot) + manager = ProcessManager() processContext = manager.getPlayerContext(self.guild, self.ctx) # Add the downloaded song to the process playlist # All access to shared memory should be protect by acquire the Lock diff --git a/Handlers/RemoveHandler.py b/Handlers/RemoveHandler.py index 5dfaf39..6e0b663 100644 --- a/Handlers/RemoveHandler.py +++ b/Handlers/RemoveHandler.py @@ -4,6 +4,8 @@ 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 class RemoveHandler(AbstractHandler): @@ -11,24 +13,34 @@ class RemoveHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self, position: str) -> HandlerResponse: - if not self.player.playlist: + # Get the current process of the guild + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if not processContext: + # Clear the playlist embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() return HandlerResponse(self.ctx, embed, error) - error = self.__validate_input(position) + playlist = processContext.getPlaylist() + if playlist.getCurrentSong() is None: + embed = self.embeds.NOT_PLAYING() + error = BadCommandUsage() + return HandlerResponse(self.ctx, embed, error) + + error = self.__validateInput(position) if error: embed = self.embeds.ERROR_EMBED(error.message) return HandlerResponse(self.ctx, embed, error) - position = self.__sanitize_input(position) - if not self.player.playlist.validate_position(position): + position = self.__sanitizeInput(playlist, position) + if not playlist.validate_position(position): error = InvalidInput() embed = self.embeds.PLAYLIST_RANGE_ERROR() return HandlerResponse(self.ctx, embed, error) try: - song = self.player.playlist.remove_song(position) + song = playlist.remove_song(position) name = song.title if song.title else song.identifier embed = self.embeds.SONG_REMOVED(name) @@ -38,15 +50,15 @@ class RemoveHandler(AbstractHandler): embed = self.embeds.ERROR_REMOVING() return HandlerResponse(self.ctx, embed, error) - def __validate_input(self, position: str) -> Union[VulkanError, None]: + def __validateInput(self, position: str) -> Union[VulkanError, None]: try: position = int(position) except: return NumberRequired(self.messages.ERROR_NUMBER) - def __sanitize_input(self, position: str) -> int: + def __sanitizeInput(self, playlist: Playlist, position: str) -> int: position = int(position) if position == -1: - position = len(self.player.playlist) + position = len(playlist) return position diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py index 114ce97..c2e3d3a 100644 --- a/Handlers/ResetHandler.py +++ b/Handlers/ResetHandler.py @@ -2,19 +2,21 @@ from discord.ext.commands import Context from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Handlers.PlayersController import PlayersController +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType class ResetHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - self.__controller = PlayersController(self.bot) async def run(self) -> HandlerResponse: - try: - await self.player.force_stop() - await self.bot_member.move_to(None) - self.__controller.reset_player(self.guild) - return HandlerResponse(self.ctx) - except Exception as e: - print(f'DEVELOPER NOTE -> Reset Error: {e}') + # Get the current process of the guild + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if processContext: + command = VCommands(VCommandsType.RESET, None) + queue = processContext.getQueue() + queue.put(command) + + return HandlerResponse(self.ctx) diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py index c881b5c..0f2baae 100644 --- a/Handlers/ShuffleHandler.py +++ b/Handlers/ShuffleHandler.py @@ -1,27 +1,33 @@ -import asyncio 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 Music.Downloader import Downloader +from Parallelism.ProcessManager import ProcessManager class ShuffleHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - self.__down = Downloader() async def run(self) -> HandlerResponse: - try: - self.player.playlist.shuffle() - songs = self.player.playlist.getSongsToPreload() + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if processContext: + try: + with processContext.getLock(): + playlist = processContext.getPlaylist() + playlist.shuffle() - asyncio.create_task(self.__down.preload(songs)) - embed = self.embeds.SONGS_SHUFFLED() + embed = self.embeds.SONGS_SHUFFLED() + return HandlerResponse(self.ctx, embed) + + except Exception as e: + print(f'DEVELOPER NOTE -> Error Shuffling: {e}') + error = UnknownError() + embed = self.embeds.ERROR_SHUFFLING() + + return HandlerResponse(self.ctx, embed, error) + else: + embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) - except Exception as e: - print(f'DEVELOPER NOTE -> Error Shuffling: {e}') - error = UnknownError() - embed = self.embeds.ERROR_SHUFFLING() - return HandlerResponse(self.ctx, embed, error) diff --git a/Parallelism/Commands.py b/Parallelism/Commands.py index 110ddce..e5dae3f 100644 --- a/Parallelism/Commands.py +++ b/Parallelism/Commands.py @@ -10,6 +10,7 @@ class VCommandsType(Enum): CONTEXT = 'Context' PLAY = 'Play' STOP = 'Stop' + RESET = 'Reset' class VCommands: diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index c07d83d..62bc735 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -1,10 +1,10 @@ import asyncio from os import listdir -from discord import Intents, User +from discord import Intents, User, Member from asyncio import AbstractEventLoop, Semaphore from multiprocessing import Process, Queue from threading import Lock, Thread -from typing import Callable +from typing import Callable, List from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel from discord.ext.commands import Context from Music.Playlist import Playlist @@ -53,6 +53,7 @@ class PlayerProcess(Process): self.__voiceChannel: VoiceChannel = None self.__textChannel: TextChannel = None self.__author: User = None + self.__botMember: Member = None self.__configs: Configs = None self.__playing = False @@ -79,6 +80,7 @@ class PlayerProcess(Process): self.__voiceChannel = self.__bot.get_channel(self.__voiceChannelID) self.__textChannel = self.__bot.get_channel(self.__textChannelID) self.__author = self.__bot.get_channel(self.__authorID) + self.__botMember = self.__getBotMember() # Connect to voice Channel await self.__connectToVoiceChannel() @@ -93,6 +95,8 @@ class PlayerProcess(Process): # Try to acquire a semaphore, it'll be release when timeout function trigger, we use the Semaphore # from the asyncio lib to not block the event loop await self.__semStopPlaying.acquire() + # In this point the process should finalize + self.__timer.cancel() async def __playPlaylistSongs(self) -> None: print(f'Playing: {self.__playing}') @@ -153,6 +157,8 @@ class PlayerProcess(Process): self.__resume() elif type == VCommandsType.SKIP: self.__skip() + elif type == VCommandsType.RESET: + self.__loop.create_task(self.__reset()) elif type == VCommandsType.STOP: self.__loop.create_task(self.__stop()) else: @@ -163,6 +169,18 @@ class PlayerProcess(Process): if self.__guild.voice_client.is_playing(): self.__guild.voice_client.pause() + async def __reset(self) -> None: + if self.__guild.voice_client is None: + return + # Reset the bot + self.__guild.voice_client.stop() + await self.__guild.voice_client.disconnect() + self.__playlist.clear() + self.__playlist.loop_off() + await self.__botMember.move_to(None) + # Release semaphore to finish the current player process + self.__semStopPlaying.release() + async def __stop(self) -> None: if self.__guild.voice_client is not None: if self.__guild.voice_client.is_connected(): @@ -236,15 +254,12 @@ class PlayerProcess(Process): try: print('TimeoutHandler') if self.__guild.voice_client is None: - print('return') return if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): - print('Resetting') self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) elif self.__guild.voice_client.is_connected(): - print('Finish') with self.__lock: self.__playlist.clear() self.__playlist.loop_off() @@ -270,3 +285,9 @@ class PlayerProcess(Process): return True except: return False + + def __getBotMember(self) -> Member: + guild_members: List[Member] = self.__guild.members + for member in guild_members: + if member.id == self.__bot.user.id: + return member