From fc7de9cb4f81c7246bf2520aff5e20aaf7c47289 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Fri, 22 Jul 2022 16:03:51 -0300 Subject: [PATCH 01/10] Starting using multiprocessing module in Vulkan, now creating a new bot for each guild when played, multiple issues yet --- .gitignore | 1 + Commands/Control.py | 2 +- Commands/Music.py | 5 +- Controllers/AbstractController.py | 2 +- Controllers/PlayController.py | 32 ++- ...ayerController.py => PlayersController.py} | 1 + Controllers/ResetController.py | 4 +- Music/Playlist.py | 9 +- Parallelism/Commands.py | 23 ++ Parallelism/PlayerProcess.py | 236 ++++++++++++++++++ Parallelism/ProcessContext.py | 22 ++ Parallelism/ProcessManager.py | 45 ++++ main.py | 5 +- 13 files changed, 367 insertions(+), 20 deletions(-) rename Controllers/{PlayerController.py => PlayersController.py} (98%) create mode 100644 Parallelism/Commands.py create mode 100644 Parallelism/PlayerProcess.py create mode 100644 Parallelism/ProcessContext.py create mode 100644 Parallelism/ProcessManager.py diff --git a/.gitignore b/.gitignore index 3799cf3..40aa9d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode assets/ __pycache__ .env diff --git a/Commands/Control.py b/Commands/Control.py index 58d5167..31a370a 100644 --- a/Commands/Control.py +++ b/Commands/Control.py @@ -44,7 +44,7 @@ class Control(commands.Cog): await ctx.send(embed=embed) else: - print(f'DEVELOPER NOTE -> Comand Error: {error}') + print(f'DEVELOPER NOTE -> Command Error: {error}') embed = self.__embeds.UNKNOWN_ERROR() await ctx.send(embed=embed) diff --git a/Commands/Music.py b/Commands/Music.py index 759b26a..57fbdf9 100644 --- a/Commands/Music.py +++ b/Commands/Music.py @@ -6,7 +6,7 @@ from Controllers.ClearController import ClearController from Controllers.MoveController import MoveController from Controllers.NowPlayingController import NowPlayingController from Controllers.PlayController import PlayController -from Controllers.PlayerController import PlayersController +from Controllers.PlayersController import PlayersController from Controllers.PrevController import PrevController from Controllers.RemoveController import RemoveController from Controllers.ResetController import ResetController @@ -21,7 +21,7 @@ from Controllers.QueueController import QueueController from Controllers.LoopController import LoopController from Views.EmoteView import EmoteView from Views.EmbedView import EmbedView - +from Parallelism.ProcessManager import ProcessManager helper = Helper() @@ -29,6 +29,7 @@ helper = Helper() class Music(commands.Cog): def __init__(self, bot) -> None: self.__bot: Client = bot + self.__processManager = ProcessManager(bot) self.__cleaner = Cleaner(self.__bot) self.__controller = PlayersController(self.__bot) diff --git a/Controllers/AbstractController.py b/Controllers/AbstractController.py index 81c69b5..ab05b6b 100644 --- a/Controllers/AbstractController.py +++ b/Controllers/AbstractController.py @@ -3,7 +3,7 @@ from typing import List from discord.ext.commands import Context from discord import Client, Guild, ClientUser, Member from Config.Messages import Messages -from Controllers.PlayerController import PlayersController +from Controllers.PlayersController import PlayersController from Music.Player import Player from Controllers.ControllerResponse import ControllerResponse from Config.Configs import Configs diff --git a/Controllers/PlayController.py b/Controllers/PlayController.py index c0c73db..b66cc24 100644 --- a/Controllers/PlayController.py +++ b/Controllers/PlayController.py @@ -1,4 +1,3 @@ -import asyncio from Exceptions.Exceptions import DownloadingError, InvalidInput, VulkanError from discord.ext.commands import Context from discord import Client @@ -8,6 +7,8 @@ from Controllers.ControllerResponse import ControllerResponse from Music.Downloader import Downloader from Music.Searcher import Searcher from Music.Song import Song +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType class PlayController(AbstractController): @@ -25,13 +26,6 @@ class PlayController(AbstractController): embed = self.embeds.NO_CHANNEL() return ControllerResponse(self.ctx, embed, error) - if not self.__is_connected(): - success = await self.__connect() - if not success: - error = UnknownError() - embed = self.embeds.UNKNOWN_ERROR() - return ControllerResponse(self.ctx, embed, error) - try: musics = await self.__searcher.search(track) if musics is None or len(musics) == 0: @@ -63,7 +57,26 @@ class PlayController(AbstractController): embed = self.embeds.SONGS_ADDED(quant) response = ControllerResponse(self.ctx, embed) - asyncio.create_task(self.player.play(self.ctx)) + # Get the process context for the current guild + manager = ProcessManager(self.bot) + 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 + with processContext.getLock(): + processContext.getPlaylist().add_song(song) + + # If process already started send a command to the player process by queue + process = processContext.getProcess() + queue = processContext.getQueue() + if process.is_alive(): + command = VCommands(VCommandsType.PLAY) + queue.put(command) + else: + # Start the process + command = VCommands(VCommandsType.CONTEXT, self.ctx) + queue.put(command) + process.start() + return response except Exception as err: @@ -72,6 +85,7 @@ class PlayController(AbstractController): error = err embed = self.embeds.CUSTOM_ERROR(error) else: + print(f'DEVELOPER NOTE -> PlayController Error: {err}') error = UnknownError() embed = self.embeds.UNKNOWN_ERROR() diff --git a/Controllers/PlayerController.py b/Controllers/PlayersController.py similarity index 98% rename from Controllers/PlayerController.py rename to Controllers/PlayersController.py index 28293cc..723b971 100644 --- a/Controllers/PlayerController.py +++ b/Controllers/PlayersController.py @@ -1,3 +1,4 @@ +from multiprocessing import Process from typing import Dict, List, Union from Config.Singleton import Singleton from discord import Guild, Client, VoiceClient, Member diff --git a/Controllers/ResetController.py b/Controllers/ResetController.py index 43e0aa6..c15284e 100644 --- a/Controllers/ResetController.py +++ b/Controllers/ResetController.py @@ -1,8 +1,8 @@ from discord.ext.commands import Context -from discord import Client, Member +from discord import Client from Controllers.AbstractController import AbstractController from Controllers.ControllerResponse import ControllerResponse -from Controllers.PlayerController import PlayersController +from Controllers.PlayersController import PlayersController class ResetController(AbstractController): diff --git a/Music/Playlist.py b/Music/Playlist.py index 53fa14a..29db64c 100644 --- a/Music/Playlist.py +++ b/Music/Playlist.py @@ -8,7 +8,7 @@ import random class Playlist: def __init__(self) -> None: - self.__config = Configs() + self.__configs = Configs() self.__queue = deque() # Store the musics to play self.__songs_history = deque() # Store the musics played @@ -17,6 +17,9 @@ class Playlist: self.__current: Song = None + def getSongs(self) -> deque[Song]: + return self.__queue + def validate_position(self, position: int) -> bool: if position not in range(1, len(self.__queue) + 1): return False @@ -47,7 +50,7 @@ class Playlist: @property def songs_to_preload(self) -> List[Song]: - return list(self.__queue)[:self.__config.MAX_PRELOAD_SONGS] + return list(self.__queue)[:self.__configs.MAX_PRELOAD_SONGS] def __len__(self) -> int: return len(self.__queue) @@ -64,7 +67,7 @@ class Playlist: if played_song.problematic == False: self.__songs_history.appendleft(played_song) - if len(self.__songs_history) > self.__config.MAX_SONGS_HISTORY: + if len(self.__songs_history) > self.__configs.MAX_SONGS_HISTORY: self.__songs_history.pop() # Remove the older elif self.__looping_one: # Insert the current song to play again diff --git a/Parallelism/Commands.py b/Parallelism/Commands.py new file mode 100644 index 0000000..6c133f3 --- /dev/null +++ b/Parallelism/Commands.py @@ -0,0 +1,23 @@ +from enum import Enum +from typing import Tuple + + +class VCommandsType(Enum): + PLAY_PREV = 'Play Prev' + SKIP = 'Skip' + PAUSE = 'Pause' + RESUME = 'Resume' + CONTEXT = 'Context' + PLAY = 'Play' + + +class VCommands: + def __init__(self, type: VCommandsType, args=None) -> None: + self.__type = type + self.__args = args + + def getType(self) -> VCommandsType: + return self.__type + + def getArgs(self) -> Tuple: + return self.__args diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py new file mode 100644 index 0000000..9541d8b --- /dev/null +++ b/Parallelism/PlayerProcess.py @@ -0,0 +1,236 @@ +import asyncio +from os import listdir +from discord import Intents +from asyncio import AbstractEventLoop, Semaphore +from multiprocessing import Process, Queue +from threading import Lock, Thread +from typing import Callable, Text +from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel +from discord.ext.commands import Context +from Music.Playlist import Playlist +from Music.Song import Song +from Config.Configs import Configs +from discord.ext.commands import Bot +from Parallelism.Commands import VCommands, VCommandsType + + +class TimeoutClock: + def __init__(self, callback: Callable, loop: asyncio.AbstractEventLoop): + self.__callback = callback + self.__task = loop.create_task(self.__executor()) + + async def __executor(self): + await asyncio.sleep(Configs().VC_TIMEOUT) + await self.__callback() + + def cancel(self): + self.__task.cancel() + + +class PlayerProcess(Process): + """Process that will play songs, receive commands by a received Queue""" + + def __init__(self, playlist: Playlist, lock: Lock, queue: Queue) -> None: + Process.__init__(self, group=None, target=None, args=(), kwargs={}) + self.__playlist: Playlist = playlist + self.__lock: Lock = lock + self.__queue: Queue = queue + + # All information of discord context will be retrieved directly with discord API + self.__guild: Guild = None + self.__bot: Client = None + self.__voiceChannel: VoiceChannel = None + self.__textChannel: TextChannel = None + self.__loop: AbstractEventLoop = None + self.__configs: Configs = None + + self.__playing = False + + # Flag to control if the player should stop totally the playing + self.__forceStop = False + self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', + 'options': '-vn'} + + def run(self) -> None: + """Function called in process.start(), this will exec the actually _run method it in event loop""" + print('Run') + + self.__loop = asyncio.get_event_loop() + self.__configs = Configs() + + # self.__loop = self.__bot.loop + self.__semStopPlaying = Semaphore(0) + self.__stopped = asyncio.Event() + # task = self.__loop.create_task(self._run()) + self.__loop.run_until_complete(self._run()) + + async def _run(self) -> None: + # Recreate the bot instance in this new process + self.__bot = await self.__createBotInstance() + + # Start the timeout function + self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) + # Thread that will receive commands to execute in this Process + self.__commandsReceiver = Thread(target=self.__commandsReceiver, daemon=True) + self.__commandsReceiver.start() + + # Start a Task to play songs + self.__loop.create_task(self.__playPlaylistSongs()) + # 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() + + async def __playPlaylistSongs(self) -> None: + if not self.__playing: + with self.__lock: + song = self.__playlist.next_song() + + await self.__playSong(song) + + async def __playSong(self, song: Song) -> None: + try: + source = await self.__ensureSource(song) + if source is None: + self.__playNext(None, self.__context) + self.__playing = True + + player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) + voice = self.__guild.voice_client + voice.play(player, after=lambda e: self.__playNext(e, self.__context)) + + self.__timer.cancel() + self.__timer = TimeoutClock(self.__timeout_handler) + + await self.__context.invoke(self.__bot.get_command('np')) + except: + self.__playNext(None) + + def __playNext(self, error) -> None: + if self.__forceStop: # If it's forced to stop player + self.__forceStop = False + return None + + with self.__lock: + song = self.__playlist.next_song() + + if song is not None: + coro = self.__playSong(song) + self.__bot.loop.create_task(coro) + else: + self.__playing = False + + def __commandsReceiver(self) -> None: + for x in range(2): + command: VCommands = self.__queue.get() + type = command.getType() + args = command.getArgs() + + if type == VCommandsType.PAUSE: + self.pause() + elif type == VCommandsType.PLAY: + self.__loop.create_task(self.__playPlaylistSongs()) + elif type == VCommandsType.PLAY_PREV: + self.__playPrev() + elif type == VCommandsType.RESUME: + pass + elif type == VCommandsType.SKIP: + pass + else: + print(f'[ERROR] -> Unknown Command Received: {command}') + + def pause(self): + print(id(self)) + + async def __playPrev(self, ctx: Context) -> None: + with self.__lock: + song = self.__playlist.prev_song() + if song is not None: + 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 + + await self.__playSong(ctx, song) + + async def __forceStop(self) -> None: + try: + if self.__guild.voice_client is None: + return + + self.__guild.voice_client.stop() + await self.__guild.voice_client.disconnect() + with self.__lock: + self.__playlist.clear() + self.__playlist.loop_off() + except Exception as e: + print(f'DEVELOPER NOTE -> Force Stop Error: {e}') + + async def __createBotInstance(self) -> Client: + # Load a new bot instance, this bot should not receive commands directly + intents = Intents.default() + intents.members = True + bot = Bot(command_prefix='Rafael', + pm_help=True, + case_insensitive=True, + intents=intents) + bot.remove_command('help') + + # Add the Cogs for this bot too + for filename in listdir(f'./{self.__configs.COMMANDS_PATH}'): + print(filename) + 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 asyncio.sleep(1) + + self.__guild: Guild = bot.get_guild(651983781258985484) + self.__voiceChannel = self.__bot.get_channel(933437427350118450) + + return bot + + async def __timeoutHandler(self) -> None: + if self.__guild.voice_client is None: + return + + if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): + self.__timer = TimeoutClock(self.__timeoutHandler) + + elif self.__guild.voice_client.is_connected(): + with self.__lock: + self.__playlist.clear() + self.__playlist.loop_off() + await self.__guild.voice_client.disconnect() + # Release semaphore to finish process + self.__semStopPlaying.release() + + async def __ensureSource(self, song: Song) -> str: + while True: + if song.source is not None: # If song got downloaded + return song.source + + if song.problematic: # If song got any error + return None + + await asyncio.sleep(0.1) + + def __is_connected(self) -> bool: + try: + if not self.__voiceChannel.is_connected(): + return False + else: + return True + except: + return False + + async def __connect(self) -> bool: + try: + await self.__voiceChannel.connect(reconnect=True, timeout=None) + return True + except: + return False diff --git a/Parallelism/ProcessContext.py b/Parallelism/ProcessContext.py new file mode 100644 index 0000000..e288a97 --- /dev/null +++ b/Parallelism/ProcessContext.py @@ -0,0 +1,22 @@ +from multiprocessing import Process, Queue, Lock +from Music.Playlist import Playlist + + +class ProcessContext: + def __init__(self, process: Process, queue: Queue, playlist: Playlist, lock: Lock) -> None: + self.__process = process + self.__queue = queue + self.__playlist = playlist + self.__lock = lock + + def getProcess(self) -> Process: + return self.__process + + def getQueue(self) -> Queue: + return self.__queue + + def getPlaylist(self) -> Playlist: + return self.__playlist + + def getLock(self) -> Lock: + return self.__lock diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py new file mode 100644 index 0000000..d1c757a --- /dev/null +++ b/Parallelism/ProcessManager.py @@ -0,0 +1,45 @@ +from multiprocessing import Queue, Lock +from multiprocessing.managers import BaseManager, NamespaceProxy +from typing import Dict +from Config.Singleton import Singleton +from discord import Guild, Client +from discord.ext.commands import Context +from Parallelism.PlayerProcess import PlayerProcess +from Music.Playlist import Playlist +from Parallelism.ProcessContext import ProcessContext + + +class ProcessManager(Singleton): + def __init__(self, bot: Client = None) -> None: + if not super().created: + Manager.register('Playlist', Playlist) + self.__manager = Manager() + self.__manager.start() + if bot is not None: + self.__bot: Client = bot + self.__playersProcess: Dict[Guild, ProcessContext] = {} + + def setPlayerContext(self, guild: Guild, context: ProcessContext): + self.__playersProcess[guild] = context + + def getPlayerContext(self, guild: Guild, context: Context) -> ProcessContext: + try: + print('Get') + if guild not in self.__playersProcess.keys(): + playlist: Playlist = self.__manager.Playlist() + lock = Lock() + queue = Queue() + process = PlayerProcess(playlist, lock, queue) + processContext = ProcessContext(process, queue, playlist, lock) + self.__playersProcess[guild] = processContext + return self.__playersProcess[guild] + except Exception as e: + print(e) + + +class Manager(BaseManager): + pass + + +class ProxyBase(NamespaceProxy): + _exposed_ = ('__getattribute__', '__setattr__', '__delattr__') diff --git a/main.py b/main.py index edb1f39..e4b6269 100644 --- a/main.py +++ b/main.py @@ -33,5 +33,6 @@ class VulkanInitializer: self.__bot.run(self.__config.BOT_TOKEN, bot=True, reconnect=True) -vulkan = VulkanInitializer() -vulkan.run() +if __name__ == '__main__': + vulkan = VulkanInitializer() + vulkan.run() From 19ae59c5b8fb8b4ebafa3d40e1f7cf79a5415fbd Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Fri, 22 Jul 2022 18:07:44 -0300 Subject: [PATCH 02/10] Changing some methods signature in Playlist to be compatible in Shared Memory, now finishing process after some time --- Controllers/HistoryController.py | 2 +- Controllers/LoopController.py | 2 +- Controllers/MoveController.py | 2 +- Controllers/NowPlayingController.py | 4 +- Controllers/PlayController.py | 27 ++--------- Controllers/PrevController.py | 2 +- Controllers/QueueController.py | 54 +++++++++++++--------- Controllers/ShuffleController.py | 2 +- Controllers/SkipController.py | 2 +- Database/Database.py | 3 -- Music/Player.py | 2 +- Music/Playlist.py | 15 +++---- Parallelism/PlayerProcess.py | 69 +++++++++++++++++------------ Parallelism/ProcessManager.py | 33 ++++++++++---- 14 files changed, 116 insertions(+), 103 deletions(-) delete mode 100644 Database/Database.py diff --git a/Controllers/HistoryController.py b/Controllers/HistoryController.py index 4a40b47..d5ed417 100644 --- a/Controllers/HistoryController.py +++ b/Controllers/HistoryController.py @@ -10,7 +10,7 @@ class HistoryController(AbstractController): super().__init__(ctx, bot) async def run(self) -> ControllerResponse: - history = self.player.playlist.songs_history + history = self.player.playlist.getSongsHistory() if len(history) == 0: text = self.messages.HISTORY_EMPTY diff --git a/Controllers/LoopController.py b/Controllers/LoopController.py index 2a2853c..7053912 100644 --- a/Controllers/LoopController.py +++ b/Controllers/LoopController.py @@ -16,7 +16,7 @@ class LoopController(AbstractController): return ControllerResponse(self.ctx, embed) args = args.lower() - if self.player.playlist.current is None: + if self.player.playlist.getCurrentSong() is None: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() return ControllerResponse(self.ctx, embed, error) diff --git a/Controllers/MoveController.py b/Controllers/MoveController.py index e5292f0..56b7f1e 100644 --- a/Controllers/MoveController.py +++ b/Controllers/MoveController.py @@ -33,7 +33,7 @@ class MoveController(AbstractController): try: song = self.player.playlist.move_songs(pos1, pos2) - songs = self.player.playlist.songs_to_preload + songs = self.player.playlist.getSongsToPreload() await self.__down.preload(songs) song_name = song.title if song.title else song.identifier diff --git a/Controllers/NowPlayingController.py b/Controllers/NowPlayingController.py index 35b684e..addaa9f 100644 --- a/Controllers/NowPlayingController.py +++ b/Controllers/NowPlayingController.py @@ -15,12 +15,12 @@ class NowPlayingController(AbstractController): embed = self.embeds.NOT_PLAYING() return ControllerResponse(self.ctx, embed) - if self.player.playlist.looping_one: + if self.player.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.current.info + info = self.player.playlist.getCurrentSong().info embed = self.embeds.SONG_INFO(info, title) return ControllerResponse(self.ctx, embed) diff --git a/Controllers/PlayController.py b/Controllers/PlayController.py index b66cc24..f119d1a 100644 --- a/Controllers/PlayController.py +++ b/Controllers/PlayController.py @@ -21,7 +21,7 @@ class PlayController(AbstractController): track = " ".join(args) requester = self.ctx.author.name - if not self.__user_connected(): + if not self.__isUserConnected(): error = ImpossibleMove() embed = self.embeds.NO_CHANNEL() return ControllerResponse(self.ctx, embed, error) @@ -36,7 +36,7 @@ class PlayController(AbstractController): self.player.playlist.add_song(song) quant = len(musics) - songs_preload = self.player.playlist.songs_to_preload + songs_preload = self.player.playlist.getSongsToPreload() await self.__down.preload(songs_preload) if quant == 1: @@ -73,8 +73,6 @@ class PlayController(AbstractController): queue.put(command) else: # Start the process - command = VCommands(VCommandsType.CONTEXT, self.ctx) - queue.put(command) process.start() return response @@ -91,27 +89,8 @@ class PlayController(AbstractController): return ControllerResponse(self.ctx, embed, error) - def __user_connected(self) -> bool: + def __isUserConnected(self) -> bool: if self.ctx.author.voice: return True else: return False - - def __is_connected(self) -> bool: - try: - voice_channel = self.guild.voice_client.channel - - if not self.guild.voice_client.is_connected(): - return False - else: - return True - except: - return False - - async def __connect(self) -> bool: - # if self.guild.voice_client is None: - try: - await self.ctx.author.voice.channel.connect(reconnect=True, timeout=None) - return True - except: - return False diff --git a/Controllers/PrevController.py b/Controllers/PrevController.py index 8be250a..bbb71a6 100644 --- a/Controllers/PrevController.py +++ b/Controllers/PrevController.py @@ -27,7 +27,7 @@ class PrevController(AbstractController): embed = self.embeds.UNKNOWN_ERROR() return ControllerResponse(self.ctx, embed, error) - if self.player.playlist.looping_all or self.player.playlist.looping_one: + if self.player.playlist.isLoopingAll() or self.player.playlist.isLoopingOne(): error = BadCommandUsage() embed = self.embeds.FAIL_DUE_TO_LOOP_ON() return ControllerResponse(self.ctx, embed, error) diff --git a/Controllers/QueueController.py b/Controllers/QueueController.py index 8a5014f..0c09940 100644 --- a/Controllers/QueueController.py +++ b/Controllers/QueueController.py @@ -5,6 +5,7 @@ from Controllers.AbstractController import AbstractController from Controllers.ControllerResponse import ControllerResponse from Music.Downloader import Downloader from Utils.Utils import Utils +from Parallelism.ProcessManager import ProcessManager class QueueController(AbstractController): @@ -13,32 +14,43 @@ class QueueController(AbstractController): self.__down = Downloader() async def run(self) -> ControllerResponse: - if self.player.playlist.looping_one: - song = self.player.playlist.current - embed = self.embeds.ONE_SONG_LOOPING(song.info) - return ControllerResponse(self.ctx, embed) - - songs_preload = self.player.playlist.songs_to_preload - if len(songs_preload) == 0: + # Retrieve the process of the guild + process = ProcessManager() + processContext = process.getRunningPlayerContext(self.guild) + if not processContext: # If no process return empty list embed = self.embeds.EMPTY_QUEUE() return ControllerResponse(self.ctx, embed) - asyncio.create_task(self.__down.preload(songs_preload)) + # Acquire the Lock to manipulate the playlist + with processContext.getLock(): + playlist = processContext.getPlaylist() - if self.player.playlist.looping_all: - title = self.messages.ALL_SONGS_LOOPING - else: - title = self.messages.QUEUE_TITLE + if playlist.isLoopingOne(): + song = playlist.getCurrentSong() + embed = self.embeds.ONE_SONG_LOOPING(song.info) + return ControllerResponse(self.ctx, embed) - total_time = Utils.format_time(sum([int(song.duration if song.duration else 0) - for song in songs_preload])) - total_songs = len(self.player.playlist) + songs_preload = playlist.getSongsToPreload() + if len(songs_preload) == 0: + embed = self.embeds.EMPTY_QUEUE() + return ControllerResponse(self.ctx, embed) - text = f'📜 Queue length: {total_songs} | ⌛ Duration: `{total_time}` downloaded \n\n' + asyncio.create_task(self.__down.preload(songs_preload)) - for pos, song in enumerate(songs_preload, start=1): - song_name = song.title if song.title else self.messages.SONG_DOWNLOADING - text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n" + if playlist.isLoopingAll(): + title = self.messages.ALL_SONGS_LOOPING + else: + title = self.messages.QUEUE_TITLE - embed = self.embeds.QUEUE(title, text) - return ControllerResponse(self.ctx, embed) + total_time = Utils.format_time(sum([int(song.duration if song.duration else 0) + for song in songs_preload])) + total_songs = len(playlist.getSongs()) + + text = f'📜 Queue length: {total_songs} | ⌛ Duration: `{total_time}` downloaded \n\n' + + for pos, song in enumerate(songs_preload, start=1): + song_name = song.title if song.title else self.messages.SONG_DOWNLOADING + text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n" + + embed = self.embeds.QUEUE(title, text) + return ControllerResponse(self.ctx, embed) diff --git a/Controllers/ShuffleController.py b/Controllers/ShuffleController.py index 5ad3ac3..773aa03 100644 --- a/Controllers/ShuffleController.py +++ b/Controllers/ShuffleController.py @@ -15,7 +15,7 @@ class ShuffleController(AbstractController): async def run(self) -> ControllerResponse: try: self.player.playlist.shuffle() - songs = self.player.playlist.songs_to_preload + songs = self.player.playlist.getSongsToPreload() asyncio.create_task(self.__down.preload(songs)) embed = self.embeds.SONGS_SHUFFLED() diff --git a/Controllers/SkipController.py b/Controllers/SkipController.py index 8c6298b..43abbfd 100644 --- a/Controllers/SkipController.py +++ b/Controllers/SkipController.py @@ -10,7 +10,7 @@ class SkipController(AbstractController): super().__init__(ctx, bot) async def run(self) -> ControllerResponse: - if self.player.playlist.looping_one: + if self.player.playlist.isLoopingOne(): embed = self.embeds.ERROR_DUE_LOOP_ONE_ON() error = BadCommandUsage() return ControllerResponse(self.ctx, embed, error) diff --git a/Database/Database.py b/Database/Database.py deleted file mode 100644 index b16ecf3..0000000 --- a/Database/Database.py +++ /dev/null @@ -1,3 +0,0 @@ -class Database: - def __init__(self) -> None: - pass diff --git a/Music/Player.py b/Music/Player.py index a9c9722..036a0e2 100644 --- a/Music/Player.py +++ b/Music/Player.py @@ -89,7 +89,7 @@ class Player(commands.Cog): await ctx.invoke(self.__bot.get_command('np')) - songs = self.__playlist.songs_to_preload + songs = self.__playlist.getSongsToPreload() asyncio.create_task(self.__down.preload(songs)) except: self.__play_next(None, ctx) diff --git a/Music/Playlist.py b/Music/Playlist.py index 29db64c..32aeb3b 100644 --- a/Music/Playlist.py +++ b/Music/Playlist.py @@ -32,24 +32,19 @@ class Playlist: return False return True - @property - def songs_history(self) -> deque: + def getSongsHistory(self) -> deque: return self.__songs_history - @property - def looping_one(self) -> bool: + def isLoopingOne(self) -> bool: return self.__looping_one - @property - def looping_all(self) -> bool: + def isLoopingAll(self) -> bool: return self.__looping_all - @property - def current(self) -> Song: + def getCurrentSong(self) -> Song: return self.__current - @property - def songs_to_preload(self) -> List[Song]: + def getSongsToPreload(self) -> List[Song]: return list(self.__queue)[:self.__configs.MAX_PRELOAD_SONGS] def __len__(self) -> int: diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 9541d8b..42210b8 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -1,10 +1,10 @@ import asyncio from os import listdir -from discord import Intents +from discord import Intents, User from asyncio import AbstractEventLoop, Semaphore from multiprocessing import Process, Queue from threading import Lock, Thread -from typing import Callable, Text +from typing import Callable from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel from discord.ext.commands import Context from Music.Playlist import Playlist @@ -30,47 +30,58 @@ class TimeoutClock: class PlayerProcess(Process): """Process that will play songs, receive commands by a received Queue""" - def __init__(self, playlist: Playlist, lock: Lock, queue: Queue) -> None: + def __init__(self, playlist: Playlist, lock: Lock, queue: 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 + """ Process.__init__(self, group=None, target=None, args=(), kwargs={}) + # Synchronization objects self.__playlist: Playlist = playlist self.__lock: Lock = lock self.__queue: Queue = queue - + self.__semStopPlaying: Semaphore = None + self.__loop: AbstractEventLoop = None + # Discord context ID + self.__textChannelID = textID + self.__guildID = guildID + self.__voiceChannelID = voiceID + self.__authorID = authorID # All information of discord context will be retrieved directly with discord API self.__guild: Guild = None self.__bot: Client = None self.__voiceChannel: VoiceChannel = None self.__textChannel: TextChannel = None - self.__loop: AbstractEventLoop = None + self.__author: User = None + self.__configs: Configs = None - self.__playing = False - - # Flag to control if the player should stop totally the playing self.__forceStop = False self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn'} def run(self) -> None: - """Function called in process.start(), this will exec the actually _run method it in event loop""" - print('Run') - + """Method called by process.start(), this will exec the actually _run method in a event loop""" self.__loop = asyncio.get_event_loop() self.__configs = Configs() - # self.__loop = self.__bot.loop self.__semStopPlaying = Semaphore(0) self.__stopped = asyncio.Event() - # task = self.__loop.create_task(self._run()) self.__loop.run_until_complete(self._run()) async def _run(self) -> None: - # Recreate the bot instance in this new process + # Recreate the bot instance and objects using discord API self.__bot = await self.__createBotInstance() + self.__guild = self.__bot.get_guild(self.__guildID) + self.__voiceChannel = self.__bot.get_channel(self.__voiceChannelID) + self.__textChannel = self.__bot.get_channel(self.__textChannelID) + self.__author = self.__bot.get_channel(self.__authorID) + # Connect to voice Channel + await self.__connectToVoiceChannel() # Start the timeout function self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) - # Thread that will receive commands to execute in this Process + # Thread that will receive commands to be executed in this Process self.__commandsReceiver = Thread(target=self.__commandsReceiver, daemon=True) self.__commandsReceiver.start() @@ -81,8 +92,10 @@ class PlayerProcess(Process): await self.__semStopPlaying.acquire() async def __playPlaylistSongs(self) -> None: + print(f'Playing: {self.__playing}') if not self.__playing: with self.__lock: + print('Next Song Aqui') song = self.__playlist.next_song() await self.__playSong(song) @@ -91,18 +104,19 @@ class PlayerProcess(Process): try: source = await self.__ensureSource(song) if source is None: - self.__playNext(None, self.__context) + self.__playNext(None) self.__playing = True player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) voice = self.__guild.voice_client - voice.play(player, after=lambda e: self.__playNext(e, self.__context)) + voice.play(player, after=lambda e: self.__playNext(e)) self.__timer.cancel() - self.__timer = TimeoutClock(self.__timeout_handler) + self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) - await self.__context.invoke(self.__bot.get_command('np')) - except: + # await self.__context.invoke(self.__bot.get_command('np')) + except Exception as e: + print(f'[ERROR IN PLAY SONG] -> {e}') self.__playNext(None) def __playNext(self, error) -> None: @@ -120,11 +134,12 @@ class PlayerProcess(Process): self.__playing = False def __commandsReceiver(self) -> None: - for x in range(2): + while True: command: VCommands = self.__queue.get() type = command.getType() args = command.getArgs() + print(f'Command Received: {type}') if type == VCommandsType.PAUSE: self.pause() elif type == VCommandsType.PLAY: @@ -167,7 +182,9 @@ class PlayerProcess(Process): print(f'DEVELOPER NOTE -> Force Stop Error: {e}') async def __createBotInstance(self) -> Client: - # Load a new bot instance, this bot should not receive commands directly + """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', @@ -178,7 +195,6 @@ class PlayerProcess(Process): # Add the Cogs for this bot too for filename in listdir(f'./{self.__configs.COMMANDS_PATH}'): - print(filename) if filename.endswith('.py'): bot.load_extension(f'{self.__configs.COMMANDS_PATH}.{filename[:-3]}') @@ -187,10 +203,7 @@ class PlayerProcess(Process): await task self.__loop.create_task(bot.connect(reconnect=True)) # Sleep to wait connection to be established - await asyncio.sleep(1) - - self.__guild: Guild = bot.get_guild(651983781258985484) - self.__voiceChannel = self.__bot.get_channel(933437427350118450) + await asyncio.sleep(2) return bot @@ -228,7 +241,7 @@ class PlayerProcess(Process): except: return False - async def __connect(self) -> bool: + async def __connectToVoiceChannel(self) -> bool: try: await self.__voiceChannel.connect(reconnect=True, timeout=None) return True diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index d1c757a..e744de9 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -24,17 +24,34 @@ class ProcessManager(Singleton): def getPlayerContext(self, guild: Guild, context: Context) -> ProcessContext: try: - print('Get') if guild not in self.__playersProcess.keys(): - playlist: Playlist = self.__manager.Playlist() - lock = Lock() - queue = Queue() - process = PlayerProcess(playlist, lock, queue) - processContext = ProcessContext(process, queue, playlist, lock) - self.__playersProcess[guild] = processContext + self.__playersProcess[guild] = self.__createProcess(context) + else: + if not self.__playersProcess[guild].getProcess().is_alive(): + self.__playersProcess[guild] = self.__createProcess(context) + return self.__playersProcess[guild] except Exception as e: - print(e) + print(f'[Error In GetPlayerContext] -> {e}') + + def getRunningPlayerContext(self, guild: Guild) -> ProcessContext: + if guild not in self.__playersProcess.keys(): + return None + + return self.__playersProcess[guild] + + def __createProcess(self, context: Context): + guildID: int = context.guild.id + textID: int = context.channel.id + voiceID: int = context.author.voice.channel.id + authorID: int = context.author.id + + playlist: Playlist = self.__manager.Playlist() + lock = Lock() + queue = Queue() + process = PlayerProcess(playlist, lock, queue, guildID, textID, voiceID, authorID) + processContext = ProcessContext(process, queue, playlist, lock) + return processContext class Manager(BaseManager): From 1ce6deaa48abc90160effb935c99c41e5bb4278b Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Fri, 22 Jul 2022 22:57:27 -0300 Subject: [PATCH 03/10] Modifying clear and skip commands to work with process --- Controllers/ClearController.py | 11 ++++- Controllers/SkipController.py | 25 ++++++----- Parallelism/PlayerProcess.py | 76 +++++++++++++++++++--------------- 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/Controllers/ClearController.py b/Controllers/ClearController.py index ab54f38..5c9188b 100644 --- a/Controllers/ClearController.py +++ b/Controllers/ClearController.py @@ -2,6 +2,7 @@ from discord.ext.commands import Context from discord import Client from Controllers.AbstractController import AbstractController from Controllers.ControllerResponse import ControllerResponse +from Parallelism.ProcessManager import ProcessManager class ClearController(AbstractController): @@ -9,5 +10,13 @@ class ClearController(AbstractController): super().__init__(ctx, bot) async def run(self) -> ControllerResponse: - self.player.playlist.clear() + # Get the current process of the guild + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if processContext: + # Clear the playlist + playlist = processContext.getPlaylist() + with processContext.getLock(): + playlist.clear() + return ControllerResponse(self.ctx) diff --git a/Controllers/SkipController.py b/Controllers/SkipController.py index 43abbfd..bb203fe 100644 --- a/Controllers/SkipController.py +++ b/Controllers/SkipController.py @@ -3,6 +3,8 @@ from discord import Client from Controllers.AbstractController import AbstractController from Exceptions.Exceptions import BadCommandUsage from Controllers.ControllerResponse import ControllerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType class SkipController(AbstractController): @@ -10,14 +12,17 @@ class SkipController(AbstractController): super().__init__(ctx, bot) async def run(self) -> ControllerResponse: - if self.player.playlist.isLoopingOne(): - embed = self.embeds.ERROR_DUE_LOOP_ONE_ON() - error = BadCommandUsage() - return ControllerResponse(self.ctx, embed, error) + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if processContext: # Verify if there is a running process + playlist = processContext.getPlaylist() + if playlist.isLoopingOne(): + embed = self.embeds.ERROR_DUE_LOOP_ONE_ON() + error = BadCommandUsage() + return ControllerResponse(self.ctx, embed, error) - voice = self.controller.get_guild_voice(self.guild) - if voice is None: - return ControllerResponse(self.ctx) - else: - voice.stop() - return ControllerResponse(self.ctx) + # Send a command to the player process to skip the music + command = VCommands(VCommandsType.SKIP, None) + queue = processContext.getQueue() + queue.put(command) + return ControllerResponse(self.ctx) diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 42210b8..4f39e3d 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -62,12 +62,15 @@ class PlayerProcess(Process): def run(self) -> None: """Method called by process.start(), this will exec the actually _run method in a event loop""" - self.__loop = asyncio.get_event_loop() - self.__configs = Configs() + try: + self.__loop = asyncio.get_event_loop() + self.__configs = Configs() - self.__semStopPlaying = Semaphore(0) - self.__stopped = asyncio.Event() - self.__loop.run_until_complete(self._run()) + self.__semStopPlaying = Semaphore(0) + self.__stopped = asyncio.Event() + self.__loop.run_until_complete(self._run()) + except Exception as e: + print(f'[Error in Process {self.name}] -> {e}') async def _run(self) -> None: # Recreate the bot instance and objects using discord API @@ -102,9 +105,9 @@ class PlayerProcess(Process): async def __playSong(self, song: Song) -> None: try: - source = await self.__ensureSource(song) - if source is None: - self.__playNext(None) + if song.source is None: + return self.__playNext(None) + self.__playing = True player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) @@ -141,20 +144,27 @@ class PlayerProcess(Process): print(f'Command Received: {type}') if type == VCommandsType.PAUSE: - self.pause() + self.__pause() elif type == VCommandsType.PLAY: self.__loop.create_task(self.__playPlaylistSongs()) elif type == VCommandsType.PLAY_PREV: self.__playPrev() elif type == VCommandsType.RESUME: - pass + self.__resume() elif type == VCommandsType.SKIP: - pass + self.__skip() else: print(f'[ERROR] -> Unknown Command Received: {command}') - def pause(self): - print(id(self)) + def __pause(self) -> None: + pass + + def __resume(self) -> None: + pass + + def __skip(self) -> None: + if self.__guild.voice_client is not None: + self.__guild.voice_client.stop() async def __playPrev(self, ctx: Context) -> None: with self.__lock: @@ -208,29 +218,27 @@ class PlayerProcess(Process): return bot async def __timeoutHandler(self) -> None: - if self.__guild.voice_client is None: - return + 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(): - self.__timer = TimeoutClock(self.__timeoutHandler) + 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(): - with self.__lock: - self.__playlist.clear() - self.__playlist.loop_off() - await self.__guild.voice_client.disconnect() - # Release semaphore to finish process - self.__semStopPlaying.release() - - async def __ensureSource(self, song: Song) -> str: - while True: - if song.source is not None: # If song got downloaded - return song.source - - if song.problematic: # If song got any error - return None - - await asyncio.sleep(0.1) + elif self.__guild.voice_client.is_connected(): + print('Finish') + with self.__lock: + self.__playlist.clear() + self.__playlist.loop_off() + self.__playing = False + await self.__guild.voice_client.disconnect() + # Release semaphore to finish process + self.__semStopPlaying.release() + except Exception as e: + print(f'[Error in Timeout] -> {e}') def __is_connected(self) -> bool: try: From cd3eddb1259363c744c077db6c88a56c95c6c4e7 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sat, 23 Jul 2022 00:37:11 -0300 Subject: [PATCH 04/10] Modifying pause, resume and move commands to work with process --- Controllers/MoveController.py | 58 ++++++++++++++++----------------- Controllers/PauseController.py | 12 +++++-- Controllers/ResumeController.py | 12 +++++-- Controllers/StopController.py | 21 ++++++------ Parallelism/Commands.py | 1 + Parallelism/PlayerProcess.py | 15 ++++++++- 6 files changed, 72 insertions(+), 47 deletions(-) diff --git a/Controllers/MoveController.py b/Controllers/MoveController.py index 56b7f1e..3d6e437 100644 --- a/Controllers/MoveController.py +++ b/Controllers/MoveController.py @@ -4,60 +4,60 @@ from discord import Client from Controllers.AbstractController import AbstractController from Controllers.ControllerResponse import ControllerResponse from Exceptions.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError -from Music.Downloader import Downloader +from Music.Playlist import Playlist +from Parallelism.ProcessManager import ProcessManager class MoveController(AbstractController): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - self.__down = Downloader() async def run(self, pos1: str, pos2: str) -> ControllerResponse: - if not self.player.playing: + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if not processContext: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() return ControllerResponse(self.ctx, embed, error) - error = self.__validate_input(pos1, pos2) - if error: - embed = self.embeds.ERROR_EMBED(error.message) - return ControllerResponse(self.ctx, embed, error) + with processContext.getLock(): + error = self.__validateInput(pos1, pos2) + if error: + embed = self.embeds.ERROR_EMBED(error.message) + return ControllerResponse(self.ctx, embed, error) - pos1, pos2 = self.__sanitize_input(pos1, pos2) - playlist = self.player.playlist + playlist = processContext.getPlaylist() + pos1, pos2 = self.__sanitizeInput(playlist, pos1, pos2) - if not playlist.validate_position(pos1) or not playlist.validate_position(pos2): - error = InvalidInput() - embed = self.embeds.PLAYLIST_RANGE_ERROR() - return ControllerResponse(self.ctx, embed, error) - try: - song = self.player.playlist.move_songs(pos1, pos2) + if not playlist.validate_position(pos1) or not playlist.validate_position(pos2): + error = InvalidInput() + embed = self.embeds.PLAYLIST_RANGE_ERROR() + return ControllerResponse(self.ctx, embed, error) + try: + song = playlist.move_songs(pos1, pos2) - songs = self.player.playlist.getSongsToPreload() - await self.__down.preload(songs) + song_name = song.title if song.title else song.identifier + embed = self.embeds.SONG_MOVED(song_name, pos1, pos2) + return ControllerResponse(self.ctx, embed) + except: + embed = self.embeds.ERROR_MOVING() + error = UnknownError() + return ControllerResponse(self.ctx, embed, error) - song_name = song.title if song.title else song.identifier - embed = self.embeds.SONG_MOVED(song_name, pos1, pos2) - return ControllerResponse(self.ctx, embed) - except: - embed = self.embeds.ERROR_MOVING() - error = UnknownError() - return ControllerResponse(self.ctx, embed, error) - - def __validate_input(self, pos1: str, pos2: str) -> Union[VulkanError, None]: + def __validateInput(self, pos1: str, pos2: str) -> Union[VulkanError, None]: try: pos1 = int(pos1) pos2 = int(pos2) except: return NumberRequired(self.messages.ERROR_NUMBER) - def __sanitize_input(self, pos1: int, pos2: int) -> tuple: + def __sanitizeInput(self, playlist: Playlist, pos1: int, pos2: int) -> tuple: pos1 = int(pos1) pos2 = int(pos2) if pos1 == -1: - pos1 = len(self.player.playlist) + pos1 = len(playlist.getSongs()) if pos2 == -1: - pos2 = len(self.player.playlist) + pos2 = len(playlist.getSongs()) return pos1, pos2 diff --git a/Controllers/PauseController.py b/Controllers/PauseController.py index 58a43bc..ce25462 100644 --- a/Controllers/PauseController.py +++ b/Controllers/PauseController.py @@ -2,6 +2,8 @@ from discord.ext.commands import Context from discord import Client from Controllers.AbstractController import AbstractController from Controllers.ControllerResponse import ControllerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType class PauseController(AbstractController): @@ -9,8 +11,12 @@ class PauseController(AbstractController): super().__init__(ctx, bot) async def run(self) -> ControllerResponse: - if self.guild.voice_client is not None: - if self.guild.voice_client.is_playing(): - self.guild.voice_client.pause() + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if processContext: + # Send Pause command to be execute by player process + command = VCommands(VCommandsType.PAUSE, None) + queue = processContext.getQueue() + queue.put(command) return ControllerResponse(self.ctx) diff --git a/Controllers/ResumeController.py b/Controllers/ResumeController.py index f319adc..aadee0b 100644 --- a/Controllers/ResumeController.py +++ b/Controllers/ResumeController.py @@ -2,6 +2,8 @@ from discord.ext.commands import Context from discord import Client from Controllers.AbstractController import AbstractController from Controllers.ControllerResponse import ControllerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType class ResumeController(AbstractController): @@ -9,8 +11,12 @@ class ResumeController(AbstractController): super().__init__(ctx, bot) async def run(self) -> ControllerResponse: - if self.guild.voice_client is not None: - if self.guild.voice_client.is_paused(): - self.guild.voice_client.resume() + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if processContext: + # Send Resume command to be execute by player process + command = VCommands(VCommandsType.RESUME, None) + queue = processContext.getQueue() + queue.put(command) return ControllerResponse(self.ctx) diff --git a/Controllers/StopController.py b/Controllers/StopController.py index f3ed2c8..5ef0760 100644 --- a/Controllers/StopController.py +++ b/Controllers/StopController.py @@ -2,6 +2,8 @@ from discord.ext.commands import Context from discord import Client from Controllers.AbstractController import AbstractController from Controllers.ControllerResponse import ControllerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType class StopController(AbstractController): @@ -9,15 +11,12 @@ class StopController(AbstractController): super().__init__(ctx, bot) async def run(self) -> ControllerResponse: - if self.guild.voice_client is None: + processManager = ProcessManager() + processContext = processManager.getRunningPlayerContext(self.guild) + if processContext: + # Send command to player process stop + command = VCommands(VCommandsType.STOP, None) + queue = processContext.getQueue() + queue.put(command) + return ControllerResponse(self.ctx) - - if self.guild.voice_client.is_connected(): - self.player.playlist.clear() - self.player.playlist.loop_off() - self.guild.voice_client.stop() - await self.guild.voice_client.disconnect() - return ControllerResponse(self.ctx) - - - \ No newline at end of file diff --git a/Parallelism/Commands.py b/Parallelism/Commands.py index 6c133f3..110ddce 100644 --- a/Parallelism/Commands.py +++ b/Parallelism/Commands.py @@ -9,6 +9,7 @@ class VCommandsType(Enum): RESUME = 'Resume' CONTEXT = 'Context' PLAY = 'Play' + STOP = 'Stop' class VCommands: diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 4f39e3d..945670a 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -153,11 +153,24 @@ class PlayerProcess(Process): self.__resume() elif type == VCommandsType.SKIP: self.__skip() + elif type == VCommandsType.STOP: + self.__loop.create_task(self.__stop()) else: print(f'[ERROR] -> Unknown Command Received: {command}') def __pause(self) -> None: - pass + if self.__guild.voice_client is not None: + if self.__guild.voice_client.is_playing(): + self.__guild.voice_client.pause() + + async def __stop(self) -> None: + if self.__guild.voice_client is not None: + if self.__guild.voice_client.is_connected(): + with self.__lock: + self.__playlist.clear() + self.__playlist.loop_off() + self.__guild.voice_client.stop() + await self.__guild.voice_client.disconnect() def __resume(self) -> None: pass From 3eab6176c3983f02127eac712c5f22ed5a9323f7 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sat, 23 Jul 2022 09:52:20 -0300 Subject: [PATCH 05/10] Code style upgrade --- {Exceptions => Config}/Exceptions.py | 0 .../Control.py => DiscordCogs/ControlCog.py | 10 +-- Commands/Music.py => DiscordCogs/MusicCog.py | 87 ++++++++++--------- .../Random.py => DiscordCogs/RandomCog.py | 15 ++-- .../AbstractHandler.py | 8 +- .../ClearHandler.py | 10 +-- .../HandlerResponse.py | 4 +- .../HistoryHandler.py | 10 +-- .../LoopHandler.py | 22 ++--- .../MoveHandler.py | 20 ++--- .../NowPlayingHandler.py | 12 +-- .../PauseHandler.py | 10 +-- .../PlayHandler.py | 24 ++--- .../PlayersController.py | 1 - .../PrevHandler.py | 18 ++-- .../QueueHandler.py | 16 ++-- .../RemoveHandler.py | 20 ++--- .../ResetHandler.py | 12 +-- .../ResumeHandler.py | 10 +-- .../ShuffleHandler.py | 14 +-- .../SkipHandler.py | 14 +-- .../StopHandler.py | 10 +-- Music/DeezerSearcher.py | 2 +- Music/Downloader.py | 2 +- Music/Searcher.py | 4 +- Music/{Spotify.py => SpotifySearcher.py} | 2 +- Parallelism/PlayerProcess.py | 6 +- .../{ProcessContext.py => ProcessInfo.py} | 6 +- Parallelism/ProcessManager.py | 38 ++++---- Tests/VDeezerTests.py | 2 +- Tests/VSpotifyTests.py | 2 +- Utils/Sender.py | 12 --- Views/AbstractView.py | 8 +- Views/EmbedView.py | 4 +- Views/Embeds.py | 2 +- Views/EmoteView.py | 4 +- 36 files changed, 221 insertions(+), 220 deletions(-) rename {Exceptions => Config}/Exceptions.py (100%) rename Commands/Control.py => DiscordCogs/ControlCog.py (95%) rename Commands/Music.py => DiscordCogs/MusicCog.py (70%) rename Commands/Random.py => DiscordCogs/RandomCog.py (86%) rename Controllers/AbstractController.py => Handlers/AbstractHandler.py (91%) rename Controllers/ClearController.py => Handlers/ClearHandler.py (68%) rename Controllers/ControllerResponse.py => Handlers/HandlerResponse.py (90%) rename Controllers/HistoryController.py => Handlers/HistoryHandler.py (70%) rename Controllers/LoopController.py => Handlers/LoopHandler.py (60%) rename Controllers/MoveController.py => Handlers/MoveHandler.py (73%) rename Controllers/NowPlayingController.py => Handlers/NowPlayingHandler.py (67%) rename Controllers/PauseController.py => Handlers/PauseHandler.py (70%) rename Controllers/PlayController.py => Handlers/PlayHandler.py (80%) rename {Controllers => Handlers}/PlayersController.py (98%) rename Controllers/PrevController.py => Handlers/PrevHandler.py (73%) rename Controllers/QueueController.py => Handlers/QueueHandler.py (81%) rename Controllers/RemoveController.py => Handlers/RemoveHandler.py (67%) rename Controllers/ResetController.py => Handlers/ResetHandler.py (59%) rename Controllers/ResumeController.py => Handlers/ResumeHandler.py (70%) rename Controllers/ShuffleController.py => Handlers/ShuffleHandler.py (63%) rename Controllers/SkipController.py => Handlers/SkipHandler.py (69%) rename Controllers/StopController.py => Handlers/StopHandler.py (69%) rename Music/{Spotify.py => SpotifySearcher.py} (98%) rename Parallelism/{ProcessContext.py => ProcessInfo.py} (83%) delete mode 100644 Utils/Sender.py diff --git a/Exceptions/Exceptions.py b/Config/Exceptions.py similarity index 100% rename from Exceptions/Exceptions.py rename to Config/Exceptions.py diff --git a/Commands/Control.py b/DiscordCogs/ControlCog.py similarity index 95% rename from Commands/Control.py rename to DiscordCogs/ControlCog.py index 31a370a..e791353 100644 --- a/Commands/Control.py +++ b/DiscordCogs/ControlCog.py @@ -1,5 +1,5 @@ from discord import Client, Game, Status, Embed -from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument, UserInputError +from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument from discord.ext import commands from Config.Configs import Configs from Config.Helper import Helper @@ -10,7 +10,8 @@ from Views.Embeds import Embeds helper = Helper() -class Control(commands.Cog): +class ControlCog(commands.Cog): + """Class to handle discord events""" def __init__(self, bot: Client): self.__bot = bot @@ -99,10 +100,9 @@ class Control(commands.Cog): embedhelp.set_thumbnail(url=self.__bot.user.avatar_url) await ctx.send(embed=embedhelp) - @commands.command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG) + @commands.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) - print(invite_url) txt = self.__config.INVITE_MESSAGE.format(invite_url, invite_url) embed = Embed( @@ -115,4 +115,4 @@ class Control(commands.Cog): def setup(bot): - bot.add_cog(Control(bot)) + bot.add_cog(ControlCog(bot)) diff --git a/Commands/Music.py b/DiscordCogs/MusicCog.py similarity index 70% rename from Commands/Music.py rename to DiscordCogs/MusicCog.py index 57fbdf9..8d32b4b 100644 --- a/Commands/Music.py +++ b/DiscordCogs/MusicCog.py @@ -2,23 +2,23 @@ from discord import Guild, Client from discord.ext import commands from discord.ext.commands import Context from Config.Helper import Helper -from Controllers.ClearController import ClearController -from Controllers.MoveController import MoveController -from Controllers.NowPlayingController import NowPlayingController -from Controllers.PlayController import PlayController -from Controllers.PlayersController import PlayersController -from Controllers.PrevController import PrevController -from Controllers.RemoveController import RemoveController -from Controllers.ResetController import ResetController -from Controllers.ShuffleController import ShuffleController +from Handlers.ClearHandler import ClearHandler +from Handlers.MoveHandler import MoveHandler +from Handlers.NowPlayingHandler import NowPlayingHandler +from Handlers.PlayHandler import PlayHandler +from Handlers.PlayersController import PlayersController +from Handlers.PrevHandler import PrevHandler +from Handlers.RemoveHandler import RemoveHandler +from Handlers.ResetHandler import ResetHandler +from Handlers.ShuffleHandler import ShuffleHandler from Utils.Cleaner import Cleaner -from Controllers.SkipController import SkipController -from Controllers.PauseController import PauseController -from Controllers.StopController import StopController -from Controllers.ResumeController import ResumeController -from Controllers.HistoryController import HistoryController -from Controllers.QueueController import QueueController -from Controllers.LoopController import LoopController +from Handlers.SkipHandler import SkipHandler +from Handlers.PauseHandler import PauseHandler +from Handlers.StopHandler import StopHandler +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 Parallelism.ProcessManager import ProcessManager @@ -26,10 +26,15 @@ from Parallelism.ProcessManager import ProcessManager helper = Helper() -class Music(commands.Cog): +class MusicCog(commands.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 - self.__processManager = ProcessManager(bot) self.__cleaner = Cleaner(self.__bot) self.__controller = PlayersController(self.__bot) @@ -43,7 +48,7 @@ class Music(commands.Cog): @commands.command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) async def play(self, ctx: Context, *args) -> None: - controller = PlayController(ctx, self.__bot) + controller = PlayHandler(ctx, self.__bot) response = await controller.run(args) if response is not None: @@ -52,17 +57,17 @@ class Music(commands.Cog): await view1.run() await view2.run() - @commands.command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila']) + @commands.command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas']) async def queue(self, ctx: Context) -> None: - controller = QueueController(ctx, self.__bot) + controller = QueueHandler(ctx, self.__bot) response = await controller.run() view2 = EmbedView(response) await view2.run() - @commands.command(name="skip", help=helper.HELP_SKIP, description=helper.HELP_SKIP_LONG, aliases=['s', 'pular']) + @commands.command(name="skip", help=helper.HELP_SKIP, description=helper.HELP_SKIP_LONG, aliases=['s', 'pular', 'next']) async def skip(self, ctx: Context) -> None: - controller = SkipController(ctx, self.__bot) + controller = SkipHandler(ctx, self.__bot) response = await controller.run() if response.success: @@ -74,7 +79,7 @@ class Music(commands.Cog): @commands.command(name='stop', help=helper.HELP_STOP, description=helper.HELP_STOP_LONG, aliases=['parar']) async def stop(self, ctx: Context) -> None: - controller = StopController(ctx, self.__bot) + controller = StopHandler(ctx, self.__bot) response = await controller.run() if response.success: @@ -84,9 +89,9 @@ class Music(commands.Cog): await view.run() - @commands.command(name='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar']) + @commands.command(name='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar', 'pare']) async def pause(self, ctx: Context) -> None: - controller = PauseController(ctx, self.__bot) + controller = PauseHandler(ctx, self.__bot) response = await controller.run() view1 = EmoteView(response) @@ -94,9 +99,9 @@ class Music(commands.Cog): await view1.run() await view2.run() - @commands.command(name='resume', help=helper.HELP_RESUME, description=helper.HELP_RESUME_LONG, aliases=['soltar']) + @commands.command(name='resume', help=helper.HELP_RESUME, description=helper.HELP_RESUME_LONG, aliases=['soltar', 'despausar']) async def resume(self, ctx: Context) -> None: - controller = ResumeController(ctx, self.__bot) + controller = ResumeHandler(ctx, self.__bot) response = await controller.run() view1 = EmoteView(response) @@ -104,9 +109,9 @@ class Music(commands.Cog): await view1.run() await view2.run() - @commands.command(name='prev', help=helper.HELP_PREV, description=helper.HELP_PREV_LONG, aliases=['anterior']) + @commands.command(name='prev', help=helper.HELP_PREV, description=helper.HELP_PREV_LONG, aliases=['anterior', 'return', 'previous']) async def prev(self, ctx: Context) -> None: - controller = PrevController(ctx, self.__bot) + controller = PrevHandler(ctx, self.__bot) response = await controller.run() if response is not None: @@ -115,9 +120,9 @@ class Music(commands.Cog): await view1.run() await view2.run() - @commands.command(name='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico']) + @commands.command(name='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico', 'h']) async def history(self, ctx: Context) -> None: - controller = HistoryController(ctx, self.__bot) + controller = HistoryHandler(ctx, self.__bot) response = await controller.run() view1 = EmbedView(response) @@ -127,7 +132,7 @@ class Music(commands.Cog): @commands.command(name='loop', help=helper.HELP_LOOP, description=helper.HELP_LOOP_LONG, aliases=['l', 'repeat']) async def loop(self, ctx: Context, args='') -> None: - controller = LoopController(ctx, self.__bot) + controller = LoopHandler(ctx, self.__bot) response = await controller.run(args) view1 = EmoteView(response) @@ -137,15 +142,15 @@ class Music(commands.Cog): @commands.command(name='clear', help=helper.HELP_CLEAR, description=helper.HELP_CLEAR_LONG, aliases=['c', 'limpar']) async def clear(self, ctx: Context) -> None: - controller = ClearController(ctx, self.__bot) + controller = ClearHandler(ctx, self.__bot) response = await controller.run() view = EmoteView(response) await view.run() - @commands.command(name='np', help=helper.HELP_NP, description=helper.HELP_NP_LONG, aliases=['playing', 'now']) + @commands.command(name='np', help=helper.HELP_NP, description=helper.HELP_NP_LONG, aliases=['playing', 'now', 'this']) async def now_playing(self, ctx: Context) -> None: - controller = NowPlayingController(ctx, self.__bot) + controller = NowPlayingHandler(ctx, self.__bot) response = await controller.run() view1 = EmbedView(response) @@ -155,7 +160,7 @@ class Music(commands.Cog): @commands.command(name='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio']) async def shuffle(self, ctx: Context) -> None: - controller = ShuffleController(ctx, self.__bot) + controller = ShuffleHandler(ctx, self.__bot) response = await controller.run() view1 = EmbedView(response) @@ -165,7 +170,7 @@ class Music(commands.Cog): @commands.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: - controller = MoveController(ctx, self.__bot) + controller = MoveHandler(ctx, self.__bot) response = await controller.run(pos1, pos2) view1 = EmbedView(response) @@ -175,7 +180,7 @@ class Music(commands.Cog): @commands.command(name='remove', help=helper.HELP_REMOVE, description=helper.HELP_REMOVE_LONG, aliases=['remover']) async def remove(self, ctx: Context, position) -> None: - controller = RemoveController(ctx, self.__bot) + controller = RemoveHandler(ctx, self.__bot) response = await controller.run(position) view1 = EmbedView(response) @@ -185,7 +190,7 @@ class Music(commands.Cog): @commands.command(name='reset', help=helper.HELP_RESET, description=helper.HELP_RESET_LONG, aliases=['resetar']) async def reset(self, ctx: Context) -> None: - controller = ResetController(ctx, self.__bot) + controller = ResetHandler(ctx, self.__bot) response = await controller.run() view1 = EmbedView(response) @@ -195,4 +200,4 @@ class Music(commands.Cog): def setup(bot): - bot.add_cog(Music(bot)) + bot.add_cog(MusicCog(bot)) diff --git a/Commands/Random.py b/DiscordCogs/RandomCog.py similarity index 86% rename from Commands/Random.py rename to DiscordCogs/RandomCog.py index 13c0479..52f09f5 100644 --- a/Commands/Random.py +++ b/DiscordCogs/RandomCog.py @@ -1,22 +1,19 @@ from random import randint, random from discord import Client from discord.ext.commands import Context, command, Cog -from Config.Colors import Colors -from Config.Configs import Configs from Config.Helper import Helper from Views.Embeds import Embeds helper = Helper() -class Random(Cog): +class RandomCog(Cog): + """Class to listen to commands of type Random""" def __init__(self, bot: Client): - self.__config = Configs() - self.__colors = Colors() self.__embeds = Embeds() - @command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG) + @command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG, aliases=['rand', 'aleatorio']) async def random(self, ctx: Context, arg: str) -> None: try: arg = int(arg) @@ -37,7 +34,7 @@ class Random(Cog): embed = self.__embeds.RANDOM_NUMBER(a, b, x) await ctx.send(embed=embed) - @command(name='cara', help=helper.HELP_CARA, description=helper.HELP_CARA_LONG) + @command(name='cara', help=helper.HELP_CARA, description=helper.HELP_CARA_LONG, aliases=['coroa']) async def cara(self, ctx: Context) -> None: x = random() if x < 0.5: @@ -48,7 +45,7 @@ class Random(Cog): embed = self.__embeds.CARA_COROA(result) await ctx.send(embed=embed) - @command(name='choose', help=helper.HELP_CHOOSE, description=helper.HELP_CHOOSE_LONG) + @command(name='choose', help=helper.HELP_CHOOSE, description=helper.HELP_CHOOSE_LONG, aliases=['escolha', 'pick']) async def choose(self, ctx, *args: str) -> None: try: user_input = " ".join(args) @@ -64,4 +61,4 @@ class Random(Cog): def setup(bot): - bot.add_cog(Random(bot)) + bot.add_cog(RandomCog(bot)) diff --git a/Controllers/AbstractController.py b/Handlers/AbstractHandler.py similarity index 91% rename from Controllers/AbstractController.py rename to Handlers/AbstractHandler.py index ab05b6b..d038045 100644 --- a/Controllers/AbstractController.py +++ b/Handlers/AbstractHandler.py @@ -3,15 +3,15 @@ from typing import List from discord.ext.commands import Context from discord import Client, Guild, ClientUser, Member from Config.Messages import Messages -from Controllers.PlayersController import PlayersController +from Handlers.PlayersController import PlayersController from Music.Player import Player -from Controllers.ControllerResponse import ControllerResponse +from Handlers.HandlerResponse import HandlerResponse from Config.Configs import Configs from Config.Helper import Helper from Views.Embeds import Embeds -class AbstractController(ABC): +class AbstractHandler(ABC): def __init__(self, ctx: Context, bot: Client) -> None: self.__bot: Client = bot self.__controller = PlayersController(self.__bot) @@ -27,7 +27,7 @@ class AbstractController(ABC): self.__bot_member: Member = self.__get_member() @abstractmethod - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: pass @property diff --git a/Controllers/ClearController.py b/Handlers/ClearHandler.py similarity index 68% rename from Controllers/ClearController.py rename to Handlers/ClearHandler.py index 5c9188b..3bc27a9 100644 --- a/Controllers/ClearController.py +++ b/Handlers/ClearHandler.py @@ -1,15 +1,15 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager -class ClearController(AbstractController): +class ClearHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: # Get the current process of the guild processManager = ProcessManager() processContext = processManager.getRunningPlayerContext(self.guild) @@ -19,4 +19,4 @@ class ClearController(AbstractController): with processContext.getLock(): playlist.clear() - return ControllerResponse(self.ctx) + return HandlerResponse(self.ctx) diff --git a/Controllers/ControllerResponse.py b/Handlers/HandlerResponse.py similarity index 90% rename from Controllers/ControllerResponse.py rename to Handlers/HandlerResponse.py index 09cf7f3..42c5fdf 100644 --- a/Controllers/ControllerResponse.py +++ b/Handlers/HandlerResponse.py @@ -1,10 +1,10 @@ from typing import Union from discord.ext.commands import Context -from Exceptions.Exceptions import VulkanError +from Config.Exceptions import VulkanError from discord import Embed -class ControllerResponse: +class HandlerResponse: def __init__(self, ctx: Context, embed: Embed = None, error: VulkanError = None) -> None: self.__ctx: Context = ctx self.__error: VulkanError = error diff --git a/Controllers/HistoryController.py b/Handlers/HistoryHandler.py similarity index 70% rename from Controllers/HistoryController.py rename to Handlers/HistoryHandler.py index d5ed417..515001f 100644 --- a/Controllers/HistoryController.py +++ b/Handlers/HistoryHandler.py @@ -1,15 +1,15 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse from Utils.Utils import Utils -class HistoryController(AbstractController): +class HistoryHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: history = self.player.playlist.getSongsHistory() if len(history) == 0: @@ -21,4 +21,4 @@ class HistoryController(AbstractController): text += f"**`{pos}` - ** {song.title} - `{Utils.format_time(song.duration)}`\n" embed = self.embeds.HISTORY(text) - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) diff --git a/Controllers/LoopController.py b/Handlers/LoopHandler.py similarity index 60% rename from Controllers/LoopController.py rename to Handlers/LoopHandler.py index 7053912..45334dc 100644 --- a/Controllers/LoopController.py +++ b/Handlers/LoopHandler.py @@ -1,39 +1,39 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse -from Exceptions.Exceptions import BadCommandUsage +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Config.Exceptions import BadCommandUsage -class LoopController(AbstractController): +class LoopHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self, args: str) -> ControllerResponse: + 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 ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) args = args.lower() if self.player.playlist.getCurrentSong() is None: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) if args == 'one': self.player.playlist.loop_one() embed = self.embeds.LOOP_ONE_ACTIVATED() - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) elif args == 'all': self.player.playlist.loop_all() embed = self.embeds.LOOP_ALL_ACTIVATED() - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) elif args == 'off': self.player.playlist.loop_off() embed = self.embeds.LOOP_DISABLE() - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) else: error = BadCommandUsage() embed = self.embeds.BAD_LOOP_USE() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) diff --git a/Controllers/MoveController.py b/Handlers/MoveHandler.py similarity index 73% rename from Controllers/MoveController.py rename to Handlers/MoveHandler.py index 3d6e437..6e251d3 100644 --- a/Controllers/MoveController.py +++ b/Handlers/MoveHandler.py @@ -1,30 +1,30 @@ from typing import Union from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse -from Exceptions.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError +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 -class MoveController(AbstractController): +class MoveHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self, pos1: str, pos2: str) -> ControllerResponse: + async def run(self, pos1: str, pos2: str) -> HandlerResponse: processManager = ProcessManager() processContext = processManager.getRunningPlayerContext(self.guild) if not processContext: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) with processContext.getLock(): error = self.__validateInput(pos1, pos2) if error: embed = self.embeds.ERROR_EMBED(error.message) - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) playlist = processContext.getPlaylist() pos1, pos2 = self.__sanitizeInput(playlist, pos1, pos2) @@ -32,17 +32,17 @@ class MoveController(AbstractController): if not playlist.validate_position(pos1) or not playlist.validate_position(pos2): error = InvalidInput() embed = self.embeds.PLAYLIST_RANGE_ERROR() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) try: song = playlist.move_songs(pos1, pos2) song_name = song.title if song.title else song.identifier embed = self.embeds.SONG_MOVED(song_name, pos1, pos2) - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) except: embed = self.embeds.ERROR_MOVING() error = UnknownError() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) def __validateInput(self, pos1: str, pos2: str) -> Union[VulkanError, None]: try: diff --git a/Controllers/NowPlayingController.py b/Handlers/NowPlayingHandler.py similarity index 67% rename from Controllers/NowPlayingController.py rename to Handlers/NowPlayingHandler.py index addaa9f..664a427 100644 --- a/Controllers/NowPlayingController.py +++ b/Handlers/NowPlayingHandler.py @@ -1,19 +1,19 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse from Utils.Cleaner import Cleaner -class NowPlayingController(AbstractController): +class NowPlayingHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) self.__cleaner = Cleaner() - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: if not self.player.playing: embed = self.embeds.NOT_PLAYING() - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) if self.player.playlist.isLoopingOne(): title = self.messages.ONE_SONG_LOOPING @@ -23,4 +23,4 @@ class NowPlayingController(AbstractController): info = self.player.playlist.getCurrentSong().info embed = self.embeds.SONG_INFO(info, title) - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) diff --git a/Controllers/PauseController.py b/Handlers/PauseHandler.py similarity index 70% rename from Controllers/PauseController.py rename to Handlers/PauseHandler.py index ce25462..40c77cf 100644 --- a/Controllers/PauseController.py +++ b/Handlers/PauseHandler.py @@ -1,16 +1,16 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType -class PauseController(AbstractController): +class PauseHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: processManager = ProcessManager() processContext = processManager.getRunningPlayerContext(self.guild) if processContext: @@ -19,4 +19,4 @@ class PauseController(AbstractController): queue = processContext.getQueue() queue.put(command) - return ControllerResponse(self.ctx) + return HandlerResponse(self.ctx) diff --git a/Controllers/PlayController.py b/Handlers/PlayHandler.py similarity index 80% rename from Controllers/PlayController.py rename to Handlers/PlayHandler.py index f119d1a..85996c1 100644 --- a/Controllers/PlayController.py +++ b/Handlers/PlayHandler.py @@ -1,9 +1,9 @@ -from Exceptions.Exceptions import DownloadingError, InvalidInput, VulkanError +from Config.Exceptions import DownloadingError, InvalidInput, VulkanError from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Exceptions.Exceptions import ImpossibleMove, UnknownError -from Controllers.ControllerResponse import ControllerResponse +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 @@ -11,20 +11,20 @@ from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType -class PlayController(AbstractController): +class PlayHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) self.__searcher = Searcher() self.__down = Downloader() - async def run(self, args: str) -> ControllerResponse: + async def run(self, args: str) -> HandlerResponse: track = " ".join(args) requester = self.ctx.author.name if not self.__isUserConnected(): error = ImpossibleMove() embed = self.embeds.NO_CHANNEL() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) try: musics = await self.__searcher.search(track) @@ -45,17 +45,17 @@ class PlayController(AbstractController): if song.problematic: embed = self.embeds.SONG_PROBLEMATIC() error = DownloadingError() - response = ControllerResponse(self.ctx, embed, error) + response = HandlerResponse(self.ctx, embed, error) elif not self.player.playing: embed = self.embeds.SONG_ADDED(song.title) - response = ControllerResponse(self.ctx, embed) + response = HandlerResponse(self.ctx, embed) else: embed = self.embeds.SONG_ADDED_TWO(song.info, pos) - response = ControllerResponse(self.ctx, embed) + response = HandlerResponse(self.ctx, embed) else: embed = self.embeds.SONGS_ADDED(quant) - response = ControllerResponse(self.ctx, embed) + response = HandlerResponse(self.ctx, embed) # Get the process context for the current guild manager = ProcessManager(self.bot) @@ -87,7 +87,7 @@ class PlayController(AbstractController): error = UnknownError() embed = self.embeds.UNKNOWN_ERROR() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) def __isUserConnected(self) -> bool: if self.ctx.author.voice: diff --git a/Controllers/PlayersController.py b/Handlers/PlayersController.py similarity index 98% rename from Controllers/PlayersController.py rename to Handlers/PlayersController.py index 723b971..28293cc 100644 --- a/Controllers/PlayersController.py +++ b/Handlers/PlayersController.py @@ -1,4 +1,3 @@ -from multiprocessing import Process from typing import Dict, List, Union from Config.Singleton import Singleton from discord import Guild, Client, VoiceClient, Member diff --git a/Controllers/PrevController.py b/Handlers/PrevHandler.py similarity index 73% rename from Controllers/PrevController.py rename to Handlers/PrevHandler.py index bbb71a6..222f666 100644 --- a/Controllers/PrevController.py +++ b/Handlers/PrevHandler.py @@ -1,36 +1,36 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Exceptions.Exceptions import BadCommandUsage, ImpossibleMove, UnknownError -from Controllers.ControllerResponse import ControllerResponse +from Handlers.AbstractHandler import AbstractHandler +from Config.Exceptions import BadCommandUsage, ImpossibleMove, UnknownError +from Handlers.HandlerResponse import HandlerResponse -class PrevController(AbstractController): +class PrevHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: if len(self.player.playlist.history()) == 0: error = ImpossibleMove() embed = self.embeds.NOT_PREVIOUS_SONG() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) if not self.__user_connected(): error = ImpossibleMove() embed = self.embeds.NO_CHANNEL() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) if not self.__is_connected(): success = await self.__connect() if not success: error = UnknownError() embed = self.embeds.UNKNOWN_ERROR() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) if self.player.playlist.isLoopingAll() or self.player.playlist.isLoopingOne(): error = BadCommandUsage() embed = self.embeds.FAIL_DUE_TO_LOOP_ON() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) await self.player.play_prev(self.ctx) diff --git a/Controllers/QueueController.py b/Handlers/QueueHandler.py similarity index 81% rename from Controllers/QueueController.py rename to Handlers/QueueHandler.py index 0c09940..9b853a1 100644 --- a/Controllers/QueueController.py +++ b/Handlers/QueueHandler.py @@ -1,25 +1,25 @@ import asyncio from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse +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 -class QueueController(AbstractController): +class QueueHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) self.__down = Downloader() - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: # Retrieve the process of the guild process = ProcessManager() processContext = process.getRunningPlayerContext(self.guild) if not processContext: # If no process return empty list embed = self.embeds.EMPTY_QUEUE() - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) # Acquire the Lock to manipulate the playlist with processContext.getLock(): @@ -28,12 +28,12 @@ class QueueController(AbstractController): if playlist.isLoopingOne(): song = playlist.getCurrentSong() embed = self.embeds.ONE_SONG_LOOPING(song.info) - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) songs_preload = playlist.getSongsToPreload() if len(songs_preload) == 0: embed = self.embeds.EMPTY_QUEUE() - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) asyncio.create_task(self.__down.preload(songs_preload)) @@ -53,4 +53,4 @@ class QueueController(AbstractController): text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n" embed = self.embeds.QUEUE(title, text) - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) diff --git a/Controllers/RemoveController.py b/Handlers/RemoveHandler.py similarity index 67% rename from Controllers/RemoveController.py rename to Handlers/RemoveHandler.py index 97bf73d..5dfaf39 100644 --- a/Controllers/RemoveController.py +++ b/Handlers/RemoveHandler.py @@ -1,42 +1,42 @@ from typing import Union from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse -from Exceptions.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired -class RemoveController(AbstractController): +class RemoveHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self, position: str) -> ControllerResponse: + async def run(self, position: str) -> HandlerResponse: if not self.player.playlist: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) error = self.__validate_input(position) if error: embed = self.embeds.ERROR_EMBED(error.message) - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) position = self.__sanitize_input(position) if not self.player.playlist.validate_position(position): error = InvalidInput() embed = self.embeds.PLAYLIST_RANGE_ERROR() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) try: song = self.player.playlist.remove_song(position) name = song.title if song.title else song.identifier embed = self.embeds.SONG_REMOVED(name) - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) except: error = ErrorRemoving() embed = self.embeds.ERROR_REMOVING() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) def __validate_input(self, position: str) -> Union[VulkanError, None]: try: diff --git a/Controllers/ResetController.py b/Handlers/ResetHandler.py similarity index 59% rename from Controllers/ResetController.py rename to Handlers/ResetHandler.py index c15284e..114ce97 100644 --- a/Controllers/ResetController.py +++ b/Handlers/ResetHandler.py @@ -1,20 +1,20 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse -from Controllers.PlayersController import PlayersController +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Handlers.PlayersController import PlayersController -class ResetController(AbstractController): +class ResetHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) self.__controller = PlayersController(self.bot) - async def run(self) -> ControllerResponse: + 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 ControllerResponse(self.ctx) + return HandlerResponse(self.ctx) except Exception as e: print(f'DEVELOPER NOTE -> Reset Error: {e}') diff --git a/Controllers/ResumeController.py b/Handlers/ResumeHandler.py similarity index 70% rename from Controllers/ResumeController.py rename to Handlers/ResumeHandler.py index aadee0b..58e98a8 100644 --- a/Controllers/ResumeController.py +++ b/Handlers/ResumeHandler.py @@ -1,16 +1,16 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType -class ResumeController(AbstractController): +class ResumeHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: processManager = ProcessManager() processContext = processManager.getRunningPlayerContext(self.guild) if processContext: @@ -19,4 +19,4 @@ class ResumeController(AbstractController): queue = processContext.getQueue() queue.put(command) - return ControllerResponse(self.ctx) + return HandlerResponse(self.ctx) diff --git a/Controllers/ShuffleController.py b/Handlers/ShuffleHandler.py similarity index 63% rename from Controllers/ShuffleController.py rename to Handlers/ShuffleHandler.py index 773aa03..c881b5c 100644 --- a/Controllers/ShuffleController.py +++ b/Handlers/ShuffleHandler.py @@ -1,27 +1,27 @@ import asyncio from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse -from Exceptions.Exceptions import UnknownError +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Config.Exceptions import UnknownError from Music.Downloader import Downloader -class ShuffleController(AbstractController): +class ShuffleHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) self.__down = Downloader() - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: try: self.player.playlist.shuffle() songs = self.player.playlist.getSongsToPreload() asyncio.create_task(self.__down.preload(songs)) embed = self.embeds.SONGS_SHUFFLED() - return ControllerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed) except Exception as e: print(f'DEVELOPER NOTE -> Error Shuffling: {e}') error = UnknownError() embed = self.embeds.ERROR_SHUFFLING() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) diff --git a/Controllers/SkipController.py b/Handlers/SkipHandler.py similarity index 69% rename from Controllers/SkipController.py rename to Handlers/SkipHandler.py index bb203fe..a8d260d 100644 --- a/Controllers/SkipController.py +++ b/Handlers/SkipHandler.py @@ -1,17 +1,17 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Exceptions.Exceptions import BadCommandUsage -from Controllers.ControllerResponse import ControllerResponse +from Handlers.AbstractHandler import AbstractHandler +from Config.Exceptions import BadCommandUsage +from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType -class SkipController(AbstractController): +class SkipHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: processManager = ProcessManager() processContext = processManager.getRunningPlayerContext(self.guild) if processContext: # Verify if there is a running process @@ -19,10 +19,10 @@ class SkipController(AbstractController): if playlist.isLoopingOne(): embed = self.embeds.ERROR_DUE_LOOP_ONE_ON() error = BadCommandUsage() - return ControllerResponse(self.ctx, embed, error) + return HandlerResponse(self.ctx, embed, error) # Send a command to the player process to skip the music command = VCommands(VCommandsType.SKIP, None) queue = processContext.getQueue() queue.put(command) - return ControllerResponse(self.ctx) + return HandlerResponse(self.ctx) diff --git a/Controllers/StopController.py b/Handlers/StopHandler.py similarity index 69% rename from Controllers/StopController.py rename to Handlers/StopHandler.py index 5ef0760..3dd5747 100644 --- a/Controllers/StopController.py +++ b/Handlers/StopHandler.py @@ -1,16 +1,16 @@ from discord.ext.commands import Context from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType -class StopController(AbstractController): +class StopHandler(AbstractHandler): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: processManager = ProcessManager() processContext = processManager.getRunningPlayerContext(self.guild) if processContext: @@ -19,4 +19,4 @@ class StopController(AbstractController): queue = processContext.getQueue() queue.put(command) - return ControllerResponse(self.ctx) + return HandlerResponse(self.ctx) diff --git a/Music/DeezerSearcher.py b/Music/DeezerSearcher.py index 19048ef..0d86f9f 100644 --- a/Music/DeezerSearcher.py +++ b/Music/DeezerSearcher.py @@ -1,5 +1,5 @@ import deezer -from Exceptions.Exceptions import DeezerError +from Config.Exceptions import DeezerError from Config.Messages import DeezerMessages diff --git a/Music/Downloader.py b/Music/Downloader.py index d5dcdbf..67cd643 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -7,7 +7,7 @@ from Music.Song import Song from Utils.Utils import Utils, run_async -class Downloader(): +class Downloader: config = Configs() __YDL_OPTIONS = {'format': 'bestaudio/best', 'default_search': 'auto', diff --git a/Music/Searcher.py b/Music/Searcher.py index d92343e..37386e7 100644 --- a/Music/Searcher.py +++ b/Music/Searcher.py @@ -1,7 +1,7 @@ -from Exceptions.Exceptions import DeezerError, InvalidInput, SpotifyError, YoutubeError +from Config.Exceptions import DeezerError, InvalidInput, SpotifyError, YoutubeError from Music.Downloader import Downloader from Music.Types import Provider -from Music.Spotify import SpotifySearch +from Music.SpotifySearcher import SpotifySearch from Music.DeezerSearcher import DeezerSearcher from Utils.Utils import Utils from Utils.UrlAnalyzer import URLAnalyzer diff --git a/Music/Spotify.py b/Music/SpotifySearcher.py similarity index 98% rename from Music/Spotify.py rename to Music/SpotifySearcher.py index 5e575f4..f663b3a 100644 --- a/Music/Spotify.py +++ b/Music/SpotifySearcher.py @@ -1,7 +1,7 @@ from spotipy import Spotify from spotipy.oauth2 import SpotifyClientCredentials from spotipy.exceptions import SpotifyException -from Exceptions.Exceptions import SpotifyError +from Config.Exceptions import SpotifyError from Config.Configs import Configs from Config.Messages import SpotifyMessages diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 945670a..c07d83d 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -28,7 +28,7 @@ class TimeoutClock: class PlayerProcess(Process): - """Process that will play songs, receive commands by a received Queue""" + """Process that will play songs, receive commands from the main process by a Queue""" def __init__(self, playlist: Playlist, lock: Lock, queue: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None: """ @@ -173,7 +173,9 @@ class PlayerProcess(Process): await self.__guild.voice_client.disconnect() def __resume(self) -> None: - pass + if self.__guild.voice_client is not None: + if self.__guild.voice_client.is_paused(): + self.__guild.voice_client.resume() def __skip(self) -> None: if self.__guild.voice_client is not None: diff --git a/Parallelism/ProcessContext.py b/Parallelism/ProcessInfo.py similarity index 83% rename from Parallelism/ProcessContext.py rename to Parallelism/ProcessInfo.py index e288a97..fbec5b7 100644 --- a/Parallelism/ProcessContext.py +++ b/Parallelism/ProcessInfo.py @@ -2,7 +2,11 @@ from multiprocessing import Process, Queue, Lock from Music.Playlist import Playlist -class ProcessContext: +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: self.__process = process self.__queue = queue diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index e744de9..68244a3 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -2,27 +2,31 @@ from multiprocessing import Queue, Lock from multiprocessing.managers import BaseManager, NamespaceProxy from typing import Dict from Config.Singleton import Singleton -from discord import Guild, Client +from discord import Guild from discord.ext.commands import Context from Parallelism.PlayerProcess import PlayerProcess from Music.Playlist import Playlist -from Parallelism.ProcessContext import ProcessContext +from Parallelism.ProcessInfo import ProcessInfo class ProcessManager(Singleton): - def __init__(self, bot: Client = None) -> None: - if not super().created: - Manager.register('Playlist', Playlist) - self.__manager = Manager() - self.__manager.start() - if bot is not None: - self.__bot: Client = bot - self.__playersProcess: Dict[Guild, ProcessContext] = {} + """ + Manage all running player process, creating and storing them for future calls + Deal with the creation of shared memory + """ - def setPlayerContext(self, guild: Guild, context: ProcessContext): + def __init__(self) -> None: + if not super().created: + VManager.register('Playlist', Playlist) + self.__manager = VManager() + self.__manager.start() + self.__playersProcess: Dict[Guild, ProcessInfo] = {} + + def setPlayerContext(self, guild: Guild, context: ProcessInfo): self.__playersProcess[guild] = context - def getPlayerContext(self, guild: Guild, context: Context) -> ProcessContext: + def getPlayerContext(self, guild: Guild, context: Context) -> ProcessInfo: + """Return the process info for the guild, if not, create one""" try: if guild not in self.__playersProcess.keys(): self.__playersProcess[guild] = self.__createProcess(context) @@ -34,7 +38,8 @@ class ProcessManager(Singleton): except Exception as e: print(f'[Error In GetPlayerContext] -> {e}') - def getRunningPlayerContext(self, guild: Guild) -> ProcessContext: + def getRunningPlayerContext(self, guild: Guild) -> ProcessInfo: + """Return the process info for the guild, if not, return None""" if guild not in self.__playersProcess.keys(): return None @@ -50,13 +55,14 @@ class ProcessManager(Singleton): lock = Lock() queue = Queue() process = PlayerProcess(playlist, lock, queue, guildID, textID, voiceID, authorID) - processContext = ProcessContext(process, queue, playlist, lock) + processContext = ProcessInfo(process, queue, playlist, lock) + return processContext -class Manager(BaseManager): +class VManager(BaseManager): pass -class ProxyBase(NamespaceProxy): +class VProxy(NamespaceProxy): _exposed_ = ('__getattribute__', '__setattr__', '__delattr__') diff --git a/Tests/VDeezerTests.py b/Tests/VDeezerTests.py index 063e790..c7f0c73 100644 --- a/Tests/VDeezerTests.py +++ b/Tests/VDeezerTests.py @@ -1,5 +1,5 @@ from Tests.TestBase import VulkanTesterBase -from Exceptions.Exceptions import DeezerError +from Config.Exceptions import DeezerError class VulkanDeezerTest(VulkanTesterBase): diff --git a/Tests/VSpotifyTests.py b/Tests/VSpotifyTests.py index 7220319..47ac2e5 100644 --- a/Tests/VSpotifyTests.py +++ b/Tests/VSpotifyTests.py @@ -1,5 +1,5 @@ from Tests.TestBase import VulkanTesterBase -from Exceptions.Exceptions import SpotifyError +from Config.Exceptions import SpotifyError class VulkanSpotifyTest(VulkanTesterBase): diff --git a/Utils/Sender.py b/Utils/Sender.py deleted file mode 100644 index 63d800c..0000000 --- a/Utils/Sender.py +++ /dev/null @@ -1,12 +0,0 @@ -from discord.ext.commands import Context -from discord import Embed - - -class Sender: - @classmethod - async def send_embed(cls, ctx: Context, embed: Embed) -> None: - pass - - @classmethod - async def send_message(cls, ctx: Context, message: Embed) -> None: - pass diff --git a/Views/AbstractView.py b/Views/AbstractView.py index 8a3fd82..87b6d19 100644 --- a/Views/AbstractView.py +++ b/Views/AbstractView.py @@ -1,18 +1,18 @@ from abc import ABC, abstractmethod -from Controllers.ControllerResponse import ControllerResponse +from Handlers.HandlerResponse import HandlerResponse from discord.ext.commands import Context from discord import Client, Message class AbstractView(ABC): - def __init__(self, response: ControllerResponse) -> None: - self.__response: ControllerResponse = response + def __init__(self, response: HandlerResponse) -> None: + self.__response: HandlerResponse = response self.__context: Context = response.ctx self.__message: Message = response.ctx.message self.__bot: Client = response.ctx.bot @property - def response(self) -> ControllerResponse: + def response(self) -> HandlerResponse: return self.__response @property diff --git a/Views/EmbedView.py b/Views/EmbedView.py index 6be0b33..0e21d5d 100644 --- a/Views/EmbedView.py +++ b/Views/EmbedView.py @@ -1,9 +1,9 @@ from Views.AbstractView import AbstractView -from Controllers.ControllerResponse import ControllerResponse +from Handlers.HandlerResponse import HandlerResponse class EmbedView(AbstractView): - def __init__(self, response: ControllerResponse) -> None: + def __init__(self, response: HandlerResponse) -> None: super().__init__(response) async def run(self) -> None: diff --git a/Views/Embeds.py b/Views/Embeds.py index 23571ee..447866a 100644 --- a/Views/Embeds.py +++ b/Views/Embeds.py @@ -1,5 +1,5 @@ from Config.Messages import Messages -from Exceptions.Exceptions import VulkanError +from Config.Exceptions import VulkanError from discord import Embed from Config.Configs import Configs from Config.Colors import Colors diff --git a/Views/EmoteView.py b/Views/EmoteView.py index 12ba8f7..a70202d 100644 --- a/Views/EmoteView.py +++ b/Views/EmoteView.py @@ -1,10 +1,10 @@ from Views.AbstractView import AbstractView -from Controllers.ControllerResponse import ControllerResponse +from Handlers.HandlerResponse import HandlerResponse class EmoteView(AbstractView): - def __init__(self, response: ControllerResponse) -> None: + def __init__(self, response: HandlerResponse) -> None: super().__init__(response) async def run(self) -> None: From 7efed8ab894e9f61140e6b14bd6a338a1c52c0d3 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sat, 23 Jul 2022 13:57:47 -0300 Subject: [PATCH 06/10] Adapting History, Loop, Now Playing, Remove, Shuffle and Reset commands to work with process --- Config/Configs.py | 2 +- Config/Messages.py | 2 ++ DiscordCogs/MusicCog.py | 5 ++- DiscordCogs/RandomCog.py | 2 +- Handlers/HistoryHandler.py | 12 ++++++-- Handlers/LoopHandler.py | 58 +++++++++++++++++++++-------------- Handlers/NowPlayingHandler.py | 15 +++++++-- Handlers/PlayHandler.py | 2 +- Handlers/RemoveHandler.py | 28 ++++++++++++----- Handlers/ResetHandler.py | 20 ++++++------ Handlers/ShuffleHandler.py | 32 +++++++++++-------- Parallelism/Commands.py | 1 + Parallelism/PlayerProcess.py | 31 ++++++++++++++++--- 13 files changed, 141 insertions(+), 69 deletions(-) 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 From 56456bf2ed7859cfd6aa753cc28b895dc17ce691 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sat, 23 Jul 2022 15:51:13 -0300 Subject: [PATCH 07/10] Modifying play and prev commands to work with Process --- Config/Configs.py | 2 +- Handlers/ClearHandler.py | 8 +-- Handlers/HistoryHandler.py | 8 +-- Handlers/LoopHandler.py | 8 +-- Handlers/MoveHandler.py | 8 +-- Handlers/NowPlayingHandler.py | 6 +- Handlers/PauseHandler.py | 6 +- Handlers/PlayHandler.py | 109 ++++++++++++++++++++-------------- Handlers/PrevHandler.py | 47 ++++++--------- Handlers/QueueHandler.py | 11 ++-- Handlers/RemoveHandler.py | 8 +-- Handlers/ResetHandler.py | 6 +- Handlers/ResumeHandler.py | 6 +- Handlers/ShuffleHandler.py | 8 +-- Handlers/SkipHandler.py | 8 +-- Handlers/StopHandler.py | 6 +- Music/Playlist.py | 2 +- Parallelism/Commands.py | 2 +- Parallelism/PlayerProcess.py | 60 ++++++++++++------- Parallelism/ProcessManager.py | 9 +-- 20 files changed, 178 insertions(+), 150 deletions(-) diff --git a/Config/Configs.py b/Config/Configs.py index 742a3e3..29139cb 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -21,7 +21,7 @@ class Configs(Singleton): self.MAX_PLAYLIST_LENGTH = 50 self.MAX_PLAYLIST_FORCED_LENGTH = 5 - self.MAX_PRELOAD_SONGS = 10 + self.MAX_PRELOAD_SONGS = 15 self.MAX_SONGS_HISTORY = 15 self.INVITE_MESSAGE = """To invite Vulkan to your own server, click [here]({}). diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index 3bc27a9..89572d3 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -12,11 +12,11 @@ class ClearHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: # Clear the playlist - playlist = processContext.getPlaylist() - with processContext.getLock(): + playlist = processInfo.getPlaylist() + with processInfo.getLock(): playlist.clear() return HandlerResponse(self.ctx) diff --git a/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py index ffa2255..c1989a9 100644 --- a/Handlers/HistoryHandler.py +++ b/Handlers/HistoryHandler.py @@ -13,10 +13,10 @@ class HistoryHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if processContext: - with processContext.getLock(): - playlist = processContext.getPlaylist() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: + with processInfo.getLock(): + playlist = processInfo.getPlaylist() history = playlist.getSongsHistory() else: history = [] diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index 64c5fb4..6654841 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -13,15 +13,15 @@ class LoopHandler(AbstractHandler): async def run(self, args: str) -> HandlerResponse: # Get the current process of the guild processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if not processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if not processInfo: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() return HandlerResponse(self.ctx, embed, error) - playlist = processContext.getPlaylist() + playlist = processInfo.getPlaylist() - with processContext.getLock(): + with processInfo.getLock(): if args == '' or args is None: playlist.loop_all() embed = self.embeds.LOOP_ALL_ACTIVATED() diff --git a/Handlers/MoveHandler.py b/Handlers/MoveHandler.py index 6e251d3..fc04b50 100644 --- a/Handlers/MoveHandler.py +++ b/Handlers/MoveHandler.py @@ -14,19 +14,19 @@ class MoveHandler(AbstractHandler): async def run(self, pos1: str, pos2: str) -> HandlerResponse: processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if not processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if not processInfo: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() return HandlerResponse(self.ctx, embed, error) - with processContext.getLock(): + with processInfo.getLock(): error = self.__validateInput(pos1, pos2) if error: embed = self.embeds.ERROR_EMBED(error.message) return HandlerResponse(self.ctx, embed, error) - playlist = processContext.getPlaylist() + playlist = processInfo.getPlaylist() pos1, pos2 = self.__sanitizeInput(playlist, pos1, pos2) if not playlist.validate_position(pos1) or not playlist.validate_position(pos2): diff --git a/Handlers/NowPlayingHandler.py b/Handlers/NowPlayingHandler.py index cef508b..93095c9 100644 --- a/Handlers/NowPlayingHandler.py +++ b/Handlers/NowPlayingHandler.py @@ -14,12 +14,12 @@ class NowPlayingHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if not processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if not processInfo: embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) - playlist = processContext.getPlaylist() + playlist = processInfo.getPlaylist() if playlist.getCurrentSong() is None: embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py index 40c77cf..793e2fe 100644 --- a/Handlers/PauseHandler.py +++ b/Handlers/PauseHandler.py @@ -12,11 +12,11 @@ class PauseHandler(AbstractHandler): async def run(self) -> HandlerResponse: processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: # Send Pause command to be execute by player process command = VCommands(VCommandsType.PAUSE, None) - queue = processContext.getQueue() + queue = processInfo.getQueue() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index 8f3a986..8e31276 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -1,3 +1,5 @@ +import asyncio +from typing import List from Config.Exceptions import DownloadingError, InvalidInput, VulkanError from discord.ext.commands import Context from discord import Client @@ -8,6 +10,7 @@ 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 @@ -27,55 +30,56 @@ class PlayHandler(AbstractHandler): return HandlerResponse(self.ctx, embed, error) try: - musics = await self.__searcher.search(track) - if musics is None or len(musics) == 0: + # Search for musics and get the name of each song + musicsInfo = await self.__searcher.search(track) + if musicsInfo is None or len(musicsInfo) == 0: raise InvalidInput(self.messages.INVALID_INPUT, self.messages.ERROR_TITLE) - for music in musics: - song = Song(music, self.player.playlist, requester) - self.player.playlist.add_song(song) - quant = len(musics) - - songs_preload = self.player.playlist.getSongsToPreload() - await self.__down.preload(songs_preload) - - if quant == 1: - pos = len(self.player.playlist) - song = self.__down.finish_one_song(song) - if song.problematic: - embed = self.embeds.SONG_PROBLEMATIC() - error = DownloadingError() - response = HandlerResponse(self.ctx, embed, error) - - elif not self.player.playing: - embed = self.embeds.SONG_ADDED(song.title) - response = HandlerResponse(self.ctx, embed) - else: - embed = self.embeds.SONG_ADDED_TWO(song.info, pos) - response = HandlerResponse(self.ctx, embed) - else: - embed = self.embeds.SONGS_ADDED(quant) - response = HandlerResponse(self.ctx, embed) - # Get the process context for the current guild 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 - with processContext.getLock(): - processContext.getPlaylist().add_song(song) - - # If process already started send a command to the player process by queue - process = processContext.getProcess() - queue = processContext.getQueue() - if process.is_alive(): - command = VCommands(VCommandsType.PLAY) - queue.put(command) - else: - # Start the process + processInfo = manager.getPlayerContext(self.guild, self.ctx) + playlist = processInfo.getPlaylist() + process = processInfo.getProcess() + if not process.is_alive(): # If process has not yet started, start + print('Starting process') process.start() - return response + # Create the Songs objects + songs: List[Song] = [] + for musicInfo in musicsInfo: + songs.append(Song(musicInfo, playlist, requester)) + + if len(songs) == 1: + # If only one music, download it directly + song = self.__down.finish_one_song(songs[0]) + if song.problematic: # If error in download song return + embed = self.embeds.SONG_PROBLEMATIC() + error = DownloadingError() + return HandlerResponse(self.ctx, embed, error) + + # If not playing + if not playlist.getCurrentSong(): + embed = self.embeds.SONG_ADDED(song.title) + response = HandlerResponse(self.ctx, embed) + else: # If already playing + pos = len(playlist.getSongs()) + embed = self.embeds.SONG_ADDED_TWO(song.info, pos) + response = HandlerResponse(self.ctx, embed) + + # Add the unique song to the playlist and send a command to player process + with processInfo.getLock(): + playlist.add_song(song) + queue = processInfo.getQueue() + playCommand = VCommands(VCommandsType.PLAY, None) + queue.put(playCommand) + return response + + else: # If multiple songs added + # Trigger a task to download all songs and then store them in the process playlist + asyncio.create_task(self.__downloadSongsAndStore(songs, processInfo)) + + embed = self.embeds.SONGS_ADDED(len(songs)) + return HandlerResponse(self.ctx, embed) except Exception as err: if isinstance(err, VulkanError): # If error was already processed @@ -89,6 +93,25 @@ class PlayHandler(AbstractHandler): return HandlerResponse(self.ctx, embed, error) + async def __downloadSongsAndStore(self, songs: List[Song], processInfo: ProcessInfo) -> None: + playlist = processInfo.getPlaylist() + queue = processInfo.getQueue() + playCommand = VCommands(VCommandsType.PLAY, None) + # Trigger a task for each song to be downloaded + tasks: List[asyncio.Task] = [] + for song in songs: + task = asyncio.create_task(self.__down.download_song(song)) + tasks.append(task) + + # In the original order, await for the task and then if successfully downloaded add in the playlist + 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 + with processInfo.getLock(): + playlist.add_song(song) + queue.put(playCommand) + def __isUserConnected(self) -> bool: if self.ctx.author.voice: return True diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index 222f666..1c44f31 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -1,8 +1,10 @@ from discord.ext.commands import Context from discord import Client from Handlers.AbstractHandler import AbstractHandler -from Config.Exceptions import BadCommandUsage, ImpossibleMove, UnknownError +from Config.Exceptions import BadCommandUsage, ImpossibleMove from Handlers.HandlerResponse import HandlerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType class PrevHandler(AbstractHandler): @@ -10,7 +12,15 @@ class PrevHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - if len(self.player.playlist.history()) == 0: + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if not processInfo: + embed = self.embeds.NOT_PLAYING() + error = BadCommandUsage() + return HandlerResponse(self.ctx, embed, error) + + playlist = processInfo.getPlaylist() + if len(playlist.getHistory()) == 0: error = ImpossibleMove() embed = self.embeds.NOT_PREVIOUS_SONG() return HandlerResponse(self.ctx, embed, error) @@ -20,41 +30,18 @@ class PrevHandler(AbstractHandler): embed = self.embeds.NO_CHANNEL() return HandlerResponse(self.ctx, embed, error) - if not self.__is_connected(): - success = await self.__connect() - if not success: - error = UnknownError() - embed = self.embeds.UNKNOWN_ERROR() - return HandlerResponse(self.ctx, embed, error) - - if self.player.playlist.isLoopingAll() or self.player.playlist.isLoopingOne(): + if playlist.isLoopingAll() or playlist.isLoopingOne(): error = BadCommandUsage() embed = self.embeds.FAIL_DUE_TO_LOOP_ON() return HandlerResponse(self.ctx, embed, error) - await self.player.play_prev(self.ctx) + # Send a prev command, together with the user voice channel + prevCommand = VCommands(VCommandsType.PREV, self.ctx.author.voice.channel.id) + queue = processInfo.getQueue() + queue.put(prevCommand) def __user_connected(self) -> bool: if self.ctx.author.voice: return True else: return False - - def __is_connected(self) -> bool: - try: - voice_channel = self.guild.voice_client.channel - - if not self.guild.voice_client.is_connected(): - return False - else: - return True - except: - return False - - async def __connect(self) -> bool: - # if self.guild.voice_client is None: - try: - await self.ctx.author.voice.channel.connect(reconnect=True, timeout=None) - return True - except: - return False diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index 9b853a1..d25a1e3 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -16,14 +16,14 @@ class QueueHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Retrieve the process of the guild process = ProcessManager() - processContext = process.getRunningPlayerContext(self.guild) - if not processContext: # If no process return empty list + processInfo = process.getRunningPlayerInfo(self.guild) + if not processInfo: # If no process return empty list embed = self.embeds.EMPTY_QUEUE() return HandlerResponse(self.ctx, embed) # Acquire the Lock to manipulate the playlist - with processContext.getLock(): - playlist = processContext.getPlaylist() + with processInfo.getLock(): + playlist = processInfo.getPlaylist() if playlist.isLoopingOne(): song = playlist.getCurrentSong() @@ -31,6 +31,7 @@ class QueueHandler(AbstractHandler): return HandlerResponse(self.ctx, embed) songs_preload = playlist.getSongsToPreload() + allSongs = playlist.getSongs() if len(songs_preload) == 0: embed = self.embeds.EMPTY_QUEUE() return HandlerResponse(self.ctx, embed) @@ -43,7 +44,7 @@ class QueueHandler(AbstractHandler): title = self.messages.QUEUE_TITLE total_time = Utils.format_time(sum([int(song.duration if song.duration else 0) - for song in songs_preload])) + for song in allSongs])) total_songs = len(playlist.getSongs()) text = f'📜 Queue length: {total_songs} | ⌛ Duration: `{total_time}` downloaded \n\n' diff --git a/Handlers/RemoveHandler.py b/Handlers/RemoveHandler.py index 6e0b663..c611f90 100644 --- a/Handlers/RemoveHandler.py +++ b/Handlers/RemoveHandler.py @@ -15,14 +15,14 @@ class RemoveHandler(AbstractHandler): async def run(self, position: str) -> HandlerResponse: # Get the current process of the guild processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if not processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if not processInfo: # Clear the playlist embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() return HandlerResponse(self.ctx, embed, error) - playlist = processContext.getPlaylist() + playlist = processInfo.getPlaylist() if playlist.getCurrentSong() is None: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() @@ -60,5 +60,5 @@ class RemoveHandler(AbstractHandler): position = int(position) if position == -1: - position = len(playlist) + position = len(playlist.getSongs()) return position diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py index c2e3d3a..abe1c16 100644 --- a/Handlers/ResetHandler.py +++ b/Handlers/ResetHandler.py @@ -13,10 +13,10 @@ class ResetHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: command = VCommands(VCommandsType.RESET, None) - queue = processContext.getQueue() + queue = processInfo.getQueue() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/ResumeHandler.py b/Handlers/ResumeHandler.py index 58e98a8..83b83f1 100644 --- a/Handlers/ResumeHandler.py +++ b/Handlers/ResumeHandler.py @@ -12,11 +12,11 @@ class ResumeHandler(AbstractHandler): async def run(self) -> HandlerResponse: processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: # Send Resume command to be execute by player process command = VCommands(VCommandsType.RESUME, None) - queue = processContext.getQueue() + queue = processInfo.getQueue() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py index 0f2baae..99bc730 100644 --- a/Handlers/ShuffleHandler.py +++ b/Handlers/ShuffleHandler.py @@ -12,11 +12,11 @@ class ShuffleHandler(AbstractHandler): async def run(self) -> HandlerResponse: processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: try: - with processContext.getLock(): - playlist = processContext.getPlaylist() + with processInfo.getLock(): + playlist = processInfo.getPlaylist() playlist.shuffle() embed = self.embeds.SONGS_SHUFFLED() diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index a8d260d..f03276b 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -13,9 +13,9 @@ class SkipHandler(AbstractHandler): async def run(self) -> HandlerResponse: processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if processContext: # Verify if there is a running process - playlist = processContext.getPlaylist() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: # Verify if there is a running process + playlist = processInfo.getPlaylist() if playlist.isLoopingOne(): embed = self.embeds.ERROR_DUE_LOOP_ONE_ON() error = BadCommandUsage() @@ -23,6 +23,6 @@ class SkipHandler(AbstractHandler): # Send a command to the player process to skip the music command = VCommands(VCommandsType.SKIP, None) - queue = processContext.getQueue() + queue = processInfo.getQueue() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/StopHandler.py b/Handlers/StopHandler.py index 3dd5747..434d842 100644 --- a/Handlers/StopHandler.py +++ b/Handlers/StopHandler.py @@ -12,11 +12,11 @@ class StopHandler(AbstractHandler): async def run(self) -> HandlerResponse: processManager = ProcessManager() - processContext = processManager.getRunningPlayerContext(self.guild) - if processContext: + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: # Send command to player process stop command = VCommands(VCommandsType.STOP, None) - queue = processContext.getQueue() + queue = processInfo.getQueue() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Music/Playlist.py b/Music/Playlist.py index 32aeb3b..4d50162 100644 --- a/Music/Playlist.py +++ b/Music/Playlist.py @@ -133,7 +133,7 @@ class Playlist: return song - def history(self) -> list: + def getHistory(self) -> list: titles = [] for song in self.__songs_history: title = song.title if song.title else 'Unknown' diff --git a/Parallelism/Commands.py b/Parallelism/Commands.py index e5dae3f..d6c8baa 100644 --- a/Parallelism/Commands.py +++ b/Parallelism/Commands.py @@ -3,7 +3,7 @@ from typing import Tuple class VCommandsType(Enum): - PLAY_PREV = 'Play Prev' + PREV = 'Prev' SKIP = 'Skip' PAUSE = 'Pause' RESUME = 'Resume' diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 62bc735..267ca83 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -6,7 +6,6 @@ from multiprocessing import Process, Queue from threading import Lock, Thread from typing import Callable, List from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel -from discord.ext.commands import Context from Music.Playlist import Playlist from Music.Song import Song from Config.Configs import Configs @@ -30,12 +29,12 @@ class TimeoutClock: class PlayerProcess(Process): """Process that will play songs, receive commands from the main process by a Queue""" - def __init__(self, playlist: Playlist, lock: Lock, queue: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None: + def __init__(self, name: str, playlist: Playlist, lock: Lock, queue: 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 """ - Process.__init__(self, group=None, target=None, args=(), kwargs={}) + Process.__init__(self, name=name, group=None, target=None, args=(), kwargs={}) # Synchronization objects self.__playlist: Playlist = playlist self.__lock: Lock = lock @@ -64,6 +63,7 @@ class PlayerProcess(Process): def run(self) -> None: """Method called by process.start(), this will exec the actually _run method in a event loop""" try: + print(f'Starting Process {self.name}') self.__loop = asyncio.get_event_loop() self.__configs = Configs() @@ -99,16 +99,17 @@ class PlayerProcess(Process): self.__timer.cancel() async def __playPlaylistSongs(self) -> None: - print(f'Playing: {self.__playing}') if not self.__playing: with self.__lock: - print('Next Song Aqui') song = self.__playlist.next_song() await self.__playSong(song) async def __playSong(self, song: Song) -> None: try: + if song is None: + return + if song.source is None: return self.__playNext(None) @@ -140,19 +141,38 @@ class PlayerProcess(Process): else: self.__playing = False + async def __playPrev(self, voiceChannelID: int) -> None: + with self.__lock: + 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) + 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 + + await self.__playSong(song) + def __commandsReceiver(self) -> None: while True: command: VCommands = self.__queue.get() type = command.getType() args = command.getArgs() - print(f'Command Received: {type}') + print(f'Process {self.name} receive Command: {type} with Args: {args}') if type == VCommandsType.PAUSE: self.__pause() elif type == VCommandsType.PLAY: self.__loop.create_task(self.__playPlaylistSongs()) - elif type == VCommandsType.PLAY_PREV: - self.__playPrev() + elif type == VCommandsType.PREV: + self.__loop.create_task(self.__playPrev(args)) elif type == VCommandsType.RESUME: self.__resume() elif type == VCommandsType.SKIP: @@ -199,18 +219,6 @@ class PlayerProcess(Process): if self.__guild.voice_client is not None: self.__guild.voice_client.stop() - async def __playPrev(self, ctx: Context) -> None: - with self.__lock: - song = self.__playlist.prev_song() - if song is not None: - 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 - - await self.__playSong(ctx, song) - async def __forceStop(self) -> None: try: if self.__guild.voice_client is None: @@ -246,7 +254,7 @@ class PlayerProcess(Process): await task self.__loop.create_task(bot.connect(reconnect=True)) # Sleep to wait connection to be established - await asyncio.sleep(2) + await self.__ensureDiscordConnection(bot) return bot @@ -279,11 +287,19 @@ class PlayerProcess(Process): except: return False + async def __ensureDiscordConnection(self, bot: Client) -> None: + """Await in this point until connection to discord is established""" + guild = None + while guild is None: + guild = bot.get_guild(self.__guildID) + await asyncio.sleep(0.2) + async def __connectToVoiceChannel(self) -> bool: try: await self.__voiceChannel.connect(reconnect=True, timeout=None) return True - except: + except Exception as e: + print(f'[ERROR CONNECTING TO VC] -> {e}') return False def __getBotMember(self) -> Member: diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index 68244a3..f036535 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -38,7 +38,7 @@ class ProcessManager(Singleton): except Exception as e: print(f'[Error In GetPlayerContext] -> {e}') - def getRunningPlayerContext(self, guild: Guild) -> ProcessInfo: + def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo: """Return the process info for the guild, if not, return None""" if guild not in self.__playersProcess.keys(): return None @@ -54,10 +54,11 @@ class ProcessManager(Singleton): playlist: Playlist = self.__manager.Playlist() lock = Lock() queue = Queue() - process = PlayerProcess(playlist, lock, queue, guildID, textID, voiceID, authorID) - processContext = ProcessInfo(process, queue, playlist, lock) + process = PlayerProcess(context.guild.name, playlist, lock, queue, + guildID, textID, voiceID, authorID) + processInfo = ProcessInfo(process, queue, playlist, lock) - return processContext + return processInfo class VManager(BaseManager): From b904c75caa2972264cf42634cec3dd0180fda1df Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sun, 24 Jul 2022 14:25:44 -0300 Subject: [PATCH 08/10] Fixing erros due to processing --- Config/Messages.py | 6 +- DiscordCogs/ControlCog.py | 6 +- DiscordCogs/MusicCog.py | 12 --- Handlers/AbstractHandler.py | 12 --- Handlers/PlayHandler.py | 34 ++++---- Handlers/PlayersController.py | 56 -------------- Handlers/PrevHandler.py | 7 +- Music/Downloader.py | 28 ++++--- Music/Player.py | 116 ---------------------------- Music/Playlist.py | 4 + Music/Searcher.py | 7 +- Music/Song.py | 1 + Parallelism/PlayerProcess.py | 141 ++++++++++++++++++++++------------ Parallelism/ProcessInfo.py | 3 + Parallelism/ProcessManager.py | 38 ++++++--- Utils/Utils.py | 13 ---- 16 files changed, 179 insertions(+), 305 deletions(-) delete mode 100644 Handlers/PlayersController.py delete mode 100644 Music/Player.py diff --git a/Config/Messages.py b/Config/Messages.py index 883925e..7e24672 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -14,8 +14,8 @@ class Messages(Singleton): self.SONGINFO_REQUESTER = 'Requester: ' self.SONGINFO_POSITION = 'Position: ' - self.SONGS_ADDED = 'You added {} songs to the queue' - self.SONG_ADDED = 'You added the song `{}` to the queue' + 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' @@ -61,7 +61,7 @@ class Messages(Singleton): self.NO_CHANNEL = 'To play some music, connect to any voice channel first.' self.NO_GUILD = f'This server does not has a Player, try {configs.BOT_PREFIX}reset' self.INVALID_INPUT = f'This URL was too strange, try something better or type {configs.BOT_PREFIX}help play' - self.DOWNLOADING_ERROR = '❌ An error occurred while downloading' + self.DOWNLOADING_ERROR = "❌ It's impossible to download and play this video" self.EXTRACTING_ERROR = '❌ An error ocurred while searching for the songs' self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose' diff --git a/DiscordCogs/ControlCog.py b/DiscordCogs/ControlCog.py index e791353..6a9e022 100644 --- a/DiscordCogs/ControlCog.py +++ b/DiscordCogs/ControlCog.py @@ -19,7 +19,7 @@ class ControlCog(commands.Cog): self.__messages = Messages() self.__colors = Colors() self.__embeds = Embeds() - self.__comandos = { + self.__commands = { 'MUSIC': ['resume', 'pause', 'loop', 'stop', 'skip', 'play', 'queue', 'clear', 'np', 'shuffle', 'move', 'remove', @@ -80,10 +80,10 @@ class ControlCog(commands.Cog): help_help = '👾 `HELP`\n' for command in self.__bot.commands: - if command.name in self.__comandos['MUSIC']: + if command.name in self.__commands['MUSIC']: help_music += f'**{command}** - {command.help}\n' - elif command.name in self.__comandos['RANDOM']: + elif command.name in self.__commands['RANDOM']: help_random += f'**{command}** - {command.help}\n' else: diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 6b3baec..2158f84 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -6,12 +6,10 @@ from Handlers.ClearHandler import ClearHandler from Handlers.MoveHandler import MoveHandler from Handlers.NowPlayingHandler import NowPlayingHandler from Handlers.PlayHandler import PlayHandler -from Handlers.PlayersController import PlayersController from Handlers.PrevHandler import PrevHandler from Handlers.RemoveHandler import RemoveHandler from Handlers.ResetHandler import ResetHandler from Handlers.ShuffleHandler import ShuffleHandler -from Utils.Cleaner import Cleaner from Handlers.SkipHandler import SkipHandler from Handlers.PauseHandler import PauseHandler from Handlers.StopHandler import StopHandler @@ -34,16 +32,6 @@ class MusicCog(commands.Cog): def __init__(self, bot) -> None: self.__bot: Client = bot - self.__cleaner = Cleaner(self.__bot) - self.__controller = PlayersController(self.__bot) - - @commands.Cog.listener() - async def on_ready(self) -> None: - self.__controller = PlayersController(self.__bot) - - @commands.Cog.listener() - async def on_guild_join(self, guild: Guild) -> None: - self.__controller.create_player(guild) @commands.command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) async def play(self, ctx: Context, *args) -> None: diff --git a/Handlers/AbstractHandler.py b/Handlers/AbstractHandler.py index d038045..93e7e58 100644 --- a/Handlers/AbstractHandler.py +++ b/Handlers/AbstractHandler.py @@ -3,8 +3,6 @@ from typing import List from discord.ext.commands import Context from discord import Client, Guild, ClientUser, Member from Config.Messages import Messages -from Handlers.PlayersController import PlayersController -from Music.Player import Player from Handlers.HandlerResponse import HandlerResponse from Config.Configs import Configs from Config.Helper import Helper @@ -14,8 +12,6 @@ from Views.Embeds import Embeds class AbstractHandler(ABC): def __init__(self, ctx: Context, bot: Client) -> None: self.__bot: Client = bot - self.__controller = PlayersController(self.__bot) - self.__player: Player = self.__controller.get_player(ctx.guild) self.__guild: Guild = ctx.guild self.__ctx: Context = ctx self.__bot_user: ClientUser = self.__bot.user @@ -46,14 +42,6 @@ class AbstractHandler(ABC): def guild(self) -> Guild: return self.__guild - @property - def player(self) -> Player: - return self.__player - - @property - def controller(self) -> PlayersController: - return self.__controller - @property def bot(self) -> Client: return self.__bot diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index 8e31276..26e652e 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -37,11 +37,10 @@ class PlayHandler(AbstractHandler): # Get the process context for the current guild manager = ProcessManager() - processInfo = manager.getPlayerContext(self.guild, self.ctx) + processInfo = manager.getPlayerInfo(self.guild, self.ctx) playlist = processInfo.getPlaylist() process = processInfo.getProcess() if not process.is_alive(): # If process has not yet started, start - print('Starting process') process.start() # Create the Songs objects @@ -57,22 +56,21 @@ class PlayHandler(AbstractHandler): error = DownloadingError() return HandlerResponse(self.ctx, embed, error) - # If not playing - if not playlist.getCurrentSong(): - embed = self.embeds.SONG_ADDED(song.title) - response = HandlerResponse(self.ctx, embed) - else: # If already playing - pos = len(playlist.getSongs()) - embed = self.embeds.SONG_ADDED_TWO(song.info, pos) - response = HandlerResponse(self.ctx, embed) - # Add the unique song to the playlist and send a command to player process with processInfo.getLock(): playlist.add_song(song) queue = processInfo.getQueue() playCommand = VCommands(VCommandsType.PLAY, None) queue.put(playCommand) - return response + + # If not playing + if not playlist.getCurrentSong(): + embed = self.embeds.SONG_ADDED(song.title) + return HandlerResponse(self.ctx, embed) + else: # If already playing + pos = len(playlist.getSongs()) + embed = self.embeds.SONG_ADDED_TWO(song.info, pos) + return HandlerResponse(self.ctx, embed) else: # If multiple songs added # Trigger a task to download all songs and then store them in the process playlist @@ -81,13 +79,15 @@ class PlayHandler(AbstractHandler): embed = self.embeds.SONGS_ADDED(len(songs)) return HandlerResponse(self.ctx, embed) - except Exception as err: - if isinstance(err, VulkanError): # If error was already processed - print(f'DEVELOPER NOTE -> PlayController Error: {err.message}') - error = err + except DownloadingError as error: + embed = self.embeds.DOWNLOADING_ERROR() + return HandlerResponse(self.ctx, embed, error) + except Exception as error: + if isinstance(error, VulkanError): # If error was already processed + print(f'DEVELOPER NOTE -s> PlayController Error: {error.message}', {type(error)}) embed = self.embeds.CUSTOM_ERROR(error) else: - print(f'DEVELOPER NOTE -> PlayController Error: {err}') + print(f'DEVELOPER NOTE -> PlayController Error: {error}, {type(error)}') error = UnknownError() embed = self.embeds.UNKNOWN_ERROR() diff --git a/Handlers/PlayersController.py b/Handlers/PlayersController.py deleted file mode 100644 index 28293cc..0000000 --- a/Handlers/PlayersController.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Dict, List, Union -from Config.Singleton import Singleton -from discord import Guild, Client, VoiceClient, Member -from Music.Player import Player - - -class PlayersController(Singleton): - def __init__(self, bot: Client = None) -> None: - if not super().created: - self.__bot: Client = bot - if bot is not None: - self.__players: Dict[Guild, Player] = self.__create_players() - - def set_bot(self, bot: Client) -> None: - self.__bot: Client = bot - self.__players: Dict[Guild, Player] = self.__create_players() - - def get_player(self, guild: Guild) -> Player: - if guild not in self.__players.keys(): - player = Player(self.__bot, guild) - self.__players[guild] = player - - return self.__players[guild] - - def reset_player(self, guild: Guild) -> None: - if isinstance(guild, Guild): - player = Player(self.__bot, guild) - self.__players[guild] == player - - def get_guild_voice(self, guild: Guild) -> Union[VoiceClient, None]: - if guild.voice_client is None: - return None - else: - return guild.voice_client - - def create_player(self, guild: Guild) -> None: - player = Player(self.__bot, guild) - self.__players[guild] = player - print(f'Player for guild {guild.name} created') - - def __create_players(self) -> Dict[Guild, Player]: - list_guilds: List[Guild] = self.__bot.guilds - players: Dict[Guild, Player] = {} - - for guild in list_guilds: - player = Player(self.__bot, guild) - players[guild] = player - print(f'Player for guild {guild.name} created') - - return players - - def __get_guild_bot_member(self, guild: Guild) -> Member: - members: List[Member] = guild.members - for member in members: - if member.id == self.__bot.user.id: - return member diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index 1c44f31..503e910 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -13,7 +13,7 @@ class PrevHandler(AbstractHandler): async def run(self) -> HandlerResponse: processManager = ProcessManager() - processInfo = processManager.getRunningPlayerInfo(self.guild) + processInfo = processManager.getPlayerInfo(self.guild, self.ctx) if not processInfo: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() @@ -35,6 +35,11 @@ class PrevHandler(AbstractHandler): embed = self.embeds.FAIL_DUE_TO_LOOP_ON() return HandlerResponse(self.ctx, embed, error) + # If not started, start the player process + process = processInfo.getProcess() + if not process.is_alive(): + 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() diff --git a/Music/Downloader.py b/Music/Downloader.py index 67cd643..ad8f417 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -1,10 +1,11 @@ import asyncio from typing import List from Config.Configs import Configs -from yt_dlp import YoutubeDL +from yt_dlp import YoutubeDL, DownloadError from concurrent.futures import ThreadPoolExecutor from Music.Song import Song from Utils.Utils import Utils, run_async +from Config.Exceptions import DownloadingError class Downloader: @@ -40,16 +41,20 @@ class Downloader: self.__playlist_keys = ['entries'] def finish_one_song(self, song: Song) -> Song: - if song.identifier is None: - return None + try: + if song.identifier is None: + return None - if Utils.is_url(song.identifier): - song_info = self.__download_url(song.identifier) - else: - song_info = self.__download_title(song.identifier) + if Utils.is_url(song.identifier): + song_info = self.__download_url(song.identifier) + else: + song_info = self.__download_title(song.identifier) - song.finish_down(song_info) - return song + song.finish_down(song_info) + return song + # Convert yt_dlp error to my own error + except DownloadError: + raise DownloadingError() async def preload(self, songs: List[Song]) -> None: for song in songs: @@ -81,8 +86,11 @@ class Downloader: else: # Failed to extract the songs print(f'DEVELOPER NOTE -> Failed to Extract URL {url}') return [] + # Convert the yt_dlp download error to own error + except DownloadError: + raise DownloadingError() except Exception as e: - print(f'DEVELOPER NOTE -> Error Extracting Music: {e}') + print(f'DEVELOPER NOTE -> Error Extracting Music: {e}, {type(e)}') raise e else: return [] diff --git a/Music/Player.py b/Music/Player.py deleted file mode 100644 index 036a0e2..0000000 --- a/Music/Player.py +++ /dev/null @@ -1,116 +0,0 @@ -import asyncio -from discord.ext import commands -from discord import Client, Guild, FFmpegPCMAudio -from discord.ext.commands import Context -from Music.Downloader import Downloader -from Music.Playlist import Playlist -from Music.Song import Song -from Utils.Utils import Timer - - -class Player(commands.Cog): - def __init__(self, bot: Client, guild: Guild): - self.__down: Downloader = Downloader() - self.__playlist: Playlist = Playlist() - self.__bot: Client = bot - self.__guild: Guild = guild - - self.__timer = Timer(self.__timeout_handler) - self.__playing = False - - # Flag to control if the player should stop totally the playing - self.__force_stop = False - self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', - 'options': '-vn'} - - @property - def playing(self) -> bool: - return self.__playing - - @property - def playlist(self) -> Playlist: - return self.__playlist - - async def play(self, ctx: Context) -> str: - if not self.__playing: - first_song = self.__playlist.next_song() - await self.__play_music(ctx, first_song) - - async def play_prev(self, ctx: Context) -> None: - song = self.__playlist.prev_song() - if song is not None: - 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.__force_stop = True - self.__guild.voice_client.stop() - self.__playing = False - - await self.__play_music(ctx, song) - - async def force_stop(self) -> None: - try: - if self.__guild.voice_client is None: - return - - self.__guild.voice_client.stop() - await self.__guild.voice_client.disconnect() - self.__playlist.clear() - self.__playlist.loop_off() - except Exception as e: - print(f'DEVELOPER NOTE -> Force Stop Error: {e}') - - def __play_next(self, error, ctx: Context) -> None: - if self.__force_stop: # If it's forced to stop player - self.__force_stop = False - return None - - song = self.__playlist.next_song() - - if song is not None: - coro = self.__play_music(ctx, song) - self.__bot.loop.create_task(coro) - else: - self.__playing = False - - async def __play_music(self, ctx: Context, song: Song) -> None: - try: - source = await self.__ensure_source(song) - if source is None: - self.__play_next(None, ctx) - - self.__playing = True - - player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) - voice = self.__guild.voice_client - voice.play(player, after=lambda e: self.__play_next(e, ctx)) - - self.__timer.cancel() - self.__timer = Timer(self.__timeout_handler) - - await ctx.invoke(self.__bot.get_command('np')) - - songs = self.__playlist.getSongsToPreload() - asyncio.create_task(self.__down.preload(songs)) - except: - self.__play_next(None, ctx) - - async def __timeout_handler(self) -> None: - if self.__guild.voice_client is None: - return - - if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): - self.__timer = Timer(self.__timeout_handler) - - elif self.__guild.voice_client.is_connected(): - self.__playlist.clear() - self.__playlist.loop_off() - await self.__guild.voice_client.disconnect() - - async def __ensure_source(self, song: Song) -> str: - while True: - await asyncio.sleep(0.1) - if song.source is not None: # If song got downloaded - return song.source - - if song.problematic: # If song got any error - return None diff --git a/Music/Playlist.py b/Music/Playlist.py index 4d50162..33bcbce 100644 --- a/Music/Playlist.py +++ b/Music/Playlist.py @@ -44,6 +44,9 @@ class Playlist: def getCurrentSong(self) -> Song: return self.__current + def setCurrentSong(self, song: Song) -> Song: + self.__current = song + def getSongsToPreload(self) -> List[Song]: return list(self.__queue)[:self.__configs.MAX_PRELOAD_SONGS] @@ -73,6 +76,7 @@ class Playlist: # Get the new song if len(self.__queue) == 0: + self.__current = None return None self.__current = self.__queue.popleft() diff --git a/Music/Searcher.py b/Music/Searcher.py index 37386e7..c9a6155 100644 --- a/Music/Searcher.py +++ b/Music/Searcher.py @@ -1,4 +1,4 @@ -from Config.Exceptions import DeezerError, InvalidInput, SpotifyError, YoutubeError +from Config.Exceptions import DeezerError, InvalidInput, SpotifyError, VulkanError, YoutubeError from Music.Downloader import Downloader from Music.Types import Provider from Music.SpotifySearcher import SpotifySearch @@ -25,7 +25,10 @@ class Searcher(): track = self.__cleanYoutubeInput(track) musics = await self.__down.extract_info(track) return musics - except: + except VulkanError as error: + raise error + except Exception as error: + print(f'[Error in Searcher] -> {error}, {type(error)}') raise YoutubeError(self.__messages.YOUTUBE_NOT_FOUND, self.__messages.GENERIC_TITLE) elif provider == Provider.Spotify: diff --git a/Music/Song.py b/Music/Song.py index 6c407a0..20f627d 100644 --- a/Music/Song.py +++ b/Music/Song.py @@ -23,6 +23,7 @@ class Song: else: print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}') self.destroy() + return for key in self.__useful_keys: if key in info.keys(): diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 267ca83..a62a8f9 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -2,14 +2,16 @@ import asyncio from os import listdir from discord import Intents, User, Member from asyncio import AbstractEventLoop, Semaphore -from multiprocessing import Process, Queue +from multiprocessing import Process, Queue, RLock from threading import Lock, Thread from typing import Callable, List from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel from Music.Playlist import Playlist from Music.Song import Song from Config.Configs import Configs +from Config.Messages import Messages from discord.ext.commands import Bot +from Views.Embeds import Embeds from Parallelism.Commands import VCommands, VCommandsType @@ -37,7 +39,8 @@ class PlayerProcess(Process): Process.__init__(self, name=name, group=None, target=None, args=(), kwargs={}) # Synchronization objects self.__playlist: Playlist = playlist - self.__lock: Lock = lock + self.__playlistLock: Lock = lock + self.__playerLock: RLock = None self.__queue: Queue = queue self.__semStopPlaying: Semaphore = None self.__loop: AbstractEventLoop = None @@ -55,6 +58,8 @@ class PlayerProcess(Process): self.__botMember: Member = None self.__configs: Configs = None + self.__embeds: Embeds = None + self.__messages: Messages = None self.__playing = False self.__forceStop = False self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', @@ -64,11 +69,14 @@ class PlayerProcess(Process): """Method called by process.start(), this will exec the actually _run method in a event loop""" try: print(f'Starting Process {self.name}') + self.__playerLock = RLock() self.__loop = asyncio.get_event_loop() + self.__configs = Configs() + self.__messages = Messages() + self.__embeds = Embeds() self.__semStopPlaying = Semaphore(0) - self.__stopped = asyncio.Event() self.__loop.run_until_complete(self._run()) except Exception as e: print(f'[Error in Process {self.name}] -> {e}') @@ -100,12 +108,14 @@ class PlayerProcess(Process): async def __playPlaylistSongs(self) -> None: if not self.__playing: - with self.__lock: + with self.__playlistLock: song = self.__playlist.next_song() - await self.__playSong(song) + if song is not None: + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') async def __playSong(self, song: Song) -> None: + """Function that will trigger the player to play the song""" try: if song is None: return @@ -113,36 +123,46 @@ class PlayerProcess(Process): if song.source is None: return self.__playNext(None) + # If not connected, connect to bind channel + if self.__guild.voice_client is None: + self.__connectToVoiceChannel() + + # If the player is already playing return + if self.__guild.voice_client.is_playing(): + return + self.__playing = True player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) - voice = self.__guild.voice_client - voice.play(player, after=lambda e: self.__playNext(e)) + self.__guild.voice_client.play(player, after=lambda e: self.__playNext(e)) self.__timer.cancel() self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) - # await self.__context.invoke(self.__bot.get_command('np')) + await self.__showNowPlaying() except Exception as e: - print(f'[ERROR IN PLAY SONG] -> {e}') + print(f'[ERROR IN PLAY SONG] -> {e}, {type(e)}') self.__playNext(None) def __playNext(self, error) -> None: - if self.__forceStop: # If it's forced to stop player - self.__forceStop = False - return None + with self.__playerLock: + if self.__forceStop: # If it's forced to stop player + self.__forceStop = False + return None - with self.__lock: - song = self.__playlist.next_song() + with self.__playlistLock: + song = self.__playlist.next_song() - if song is not None: - coro = self.__playSong(song) - self.__bot.loop.create_task(coro) - else: - self.__playing = False + if song is not None: + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + else: + with self.__playlistLock: + self.__playlist.loop_off() + + self.__playing = False async def __playPrev(self, voiceChannelID: int) -> None: - with self.__lock: + with self.__playlistLock: song = self.__playlist.prev_song() if song is not None: @@ -158,7 +178,7 @@ class PlayerProcess(Process): self.__guild.voice_client.stop() self.__playing = False - await self.__playSong(song) + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') def __commandsReceiver(self) -> None: while True: @@ -166,23 +186,28 @@ class PlayerProcess(Process): type = command.getType() args = command.getArgs() - print(f'Process {self.name} receive Command: {type} with Args: {args}') - if type == VCommandsType.PAUSE: - self.__pause() - elif type == VCommandsType.PLAY: - self.__loop.create_task(self.__playPlaylistSongs()) - elif type == VCommandsType.PREV: - self.__loop.create_task(self.__playPrev(args)) - elif type == VCommandsType.RESUME: - 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: - print(f'[ERROR] -> Unknown Command Received: {command}') + try: + self.__playerLock.acquire() + if type == VCommandsType.PAUSE: + self.__pause() + elif type == VCommandsType.RESUME: + self.__resume() + elif type == VCommandsType.SKIP: + self.__skip() + elif type == VCommandsType.PLAY: + asyncio.run_coroutine_threadsafe(self.__playPlaylistSongs(), self.__loop) + elif type == VCommandsType.PREV: + asyncio.run_coroutine_threadsafe(self.__playPrev(args), self.__loop) + elif type == VCommandsType.RESET: + asyncio.run_coroutine_threadsafe(self.__reset(), self.__loop) + elif type == VCommandsType.STOP: + asyncio.run_coroutine_threadsafe(self.__stop(), self.__loop) + else: + print(f'[ERROR] -> Unknown Command Received: {command}') + except Exception as e: + print(f'[ERROR IN COMMAND RECEIVER] -> {type} - {e}') + finally: + self.__playerLock.release() def __pause(self) -> None: if self.__guild.voice_client is not None: @@ -204,7 +229,7 @@ class PlayerProcess(Process): async def __stop(self) -> None: if self.__guild.voice_client is not None: if self.__guild.voice_client.is_connected(): - with self.__lock: + with self.__playlistLock: self.__playlist.clear() self.__playlist.loop_off() self.__guild.voice_client.stop() @@ -220,17 +245,14 @@ class PlayerProcess(Process): self.__guild.voice_client.stop() async def __forceStop(self) -> None: - try: - if self.__guild.voice_client is None: - return + if self.__guild.voice_client is None: + return - self.__guild.voice_client.stop() - await self.__guild.voice_client.disconnect() - with self.__lock: - self.__playlist.clear() - self.__playlist.loop_off() - except Exception as e: - print(f'DEVELOPER NOTE -> Force Stop Error: {e}') + self.__guild.voice_client.stop() + await self.__guild.voice_client.disconnect() + with self.__playlistLock: + self.__playlist.clear() + self.__playlist.loop_off() async def __createBotInstance(self) -> Client: """Load a new bot instance that should not be directly called. @@ -268,12 +290,14 @@ class PlayerProcess(Process): self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) elif self.__guild.voice_client.is_connected(): - with self.__lock: + self.__playerLock.acquire() + with self.__playlistLock: self.__playlist.clear() self.__playlist.loop_off() self.__playing = False await self.__guild.voice_client.disconnect() # Release semaphore to finish process + self.__playerLock.release() self.__semStopPlaying.release() except Exception as e: print(f'[Error in Timeout] -> {e}') @@ -307,3 +331,20 @@ class PlayerProcess(Process): for member in guild_members: if member.id == self.__bot.user.id: return member + + async def __showNowPlaying(self) -> None: + # Get the current process of the guild + with self.__playlistLock: + if not self.__playing or self.__playlist.getCurrentSong() 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.__playlist.getCurrentSong().info + embed = self.__embeds.SONG_INFO(info, title) + await self.__textChannel.send(embed=embed) diff --git a/Parallelism/ProcessInfo.py b/Parallelism/ProcessInfo.py index fbec5b7..0f8bf83 100644 --- a/Parallelism/ProcessInfo.py +++ b/Parallelism/ProcessInfo.py @@ -13,6 +13,9 @@ class ProcessInfo: self.__playlist = playlist self.__lock = lock + def setProcess(self, newProcess: Process) -> None: + self.__process = newProcess + def getProcess(self) -> Process: return self.__process diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index f036535..53829f1 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -23,29 +23,30 @@ class ProcessManager(Singleton): self.__playersProcess: Dict[Guild, ProcessInfo] = {} def setPlayerContext(self, guild: Guild, context: ProcessInfo): - self.__playersProcess[guild] = context + self.__playersProcess[guild.id] = context - def getPlayerContext(self, guild: Guild, context: Context) -> ProcessInfo: + def getPlayerInfo(self, guild: Guild, context: Context) -> ProcessInfo: """Return the process info for the guild, if not, create one""" try: - if guild not in self.__playersProcess.keys(): - self.__playersProcess[guild] = self.__createProcess(context) + if guild.id not in self.__playersProcess.keys(): + self.__playersProcess[guild.id] = self.__createProcessInfo(context) else: - if not self.__playersProcess[guild].getProcess().is_alive(): - self.__playersProcess[guild] = self.__createProcess(context) + # 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) - return self.__playersProcess[guild] + return self.__playersProcess[guild.id] except Exception as e: print(f'[Error In GetPlayerContext] -> {e}') def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo: """Return the process info for the guild, if not, return None""" - if guild not in self.__playersProcess.keys(): + if guild.id not in self.__playersProcess.keys(): return None - return self.__playersProcess[guild] + return self.__playersProcess[guild.id] - def __createProcess(self, context: Context): + def __createProcessInfo(self, context: Context) -> ProcessInfo: guildID: int = context.guild.id textID: int = context.channel.id voiceID: int = context.author.voice.channel.id @@ -60,6 +61,23 @@ class ProcessManager(Singleton): return processInfo + def __recreateProcess(self, context: Context) -> 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 + + playlist: Playlist = self.__playersProcess[guildID].getPlaylist() + lock = Lock() + queue = Queue() + + process = PlayerProcess(context.guild.name, playlist, lock, queue, + guildID, textID, voiceID, authorID) + processInfo = ProcessInfo(process, queue, playlist, lock) + + return processInfo + class VManager(BaseManager): pass diff --git a/Utils/Utils.py b/Utils/Utils.py index 82f520b..95db315 100644 --- a/Utils/Utils.py +++ b/Utils/Utils.py @@ -32,19 +32,6 @@ class Utils: return False -class Timer: - def __init__(self, callback): - self.__callback = callback - self.__task = asyncio.create_task(self.__executor()) - - async def __executor(self): - await asyncio.sleep(config.VC_TIMEOUT) - await self.__callback() - - def cancel(self): - self.__task.cancel() - - def run_async(func): @wraps(func) async def run(*args, loop=None, executor=None, **kwargs): From f27dc1de93cd7d92439ae9eb55b17ca35674fada Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sun, 24 Jul 2022 23:49:35 -0300 Subject: [PATCH 09/10] Upgrading stability in errors occurrences --- Config/Configs.py | 1 + Config/Messages.py | 1 + DiscordCogs/MusicCog.py | 225 ++++++++++++++++++++-------------- Handlers/ClearHandler.py | 11 +- Handlers/HistoryHandler.py | 10 +- Handlers/LoopHandler.py | 16 ++- Handlers/MoveHandler.py | 13 +- Handlers/PlayHandler.py | 29 +++-- Handlers/QueueHandler.py | 14 ++- Handlers/ShuffleHandler.py | 10 +- Parallelism/PlayerProcess.py | 112 ++++++++++------- Parallelism/ProcessManager.py | 14 +++ Views/Embeds.py | 7 ++ 13 files changed, 309 insertions(+), 154 deletions(-) diff --git a/Config/Configs.py b/Config/Configs.py index 29139cb..09395bb 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -16,6 +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.ACQUIRE_LOCK_TIMEOUT = 10 self.COMMANDS_PATH = 'DiscordCogs' self.VC_TIMEOUT = 600 diff --git a/Config/Messages.py b/Config/Messages.py index 7e24672..c1c5693 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -64,6 +64,7 @@ class Messages(Singleton): self.DOWNLOADING_ERROR = "❌ It's impossible to download and play this video" self.EXTRACTING_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.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' diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 2158f84..7a6e7dd 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -35,155 +35,200 @@ class MusicCog(commands.Cog): @commands.command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) async def play(self, ctx: Context, *args) -> None: - controller = PlayHandler(ctx, self.__bot) + try: + controller = PlayHandler(ctx, self.__bot) - response = await controller.run(args) - if response is not None: - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() + response = await controller.run(args) + if response is not None: + view1 = EmbedView(response) + view2 = EmoteView(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']) async def queue(self, ctx: Context) -> None: - controller = QueueHandler(ctx, self.__bot) + try: + controller = QueueHandler(ctx, self.__bot) - response = await controller.run() - view2 = EmbedView(response) - await view2.run() + response = await controller.run() + view2 = EmbedView(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']) async def skip(self, ctx: Context) -> None: - controller = SkipHandler(ctx, self.__bot) + try: + controller = SkipHandler(ctx, self.__bot) - response = await controller.run() - if response.success: - view = EmoteView(response) - else: - view = EmbedView(response) + response = await controller.run() + if response.success: + view = EmoteView(response) + else: + view = EmbedView(response) - await view.run() + 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']) async def stop(self, ctx: Context) -> None: - controller = StopHandler(ctx, self.__bot) + try: + controller = StopHandler(ctx, self.__bot) - response = await controller.run() - if response.success: - view = EmoteView(response) - else: - view = EmbedView(response) + response = await controller.run() + if response.success: + view = EmoteView(response) + else: + view = EmbedView(response) - await view.run() + 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']) async def pause(self, ctx: Context) -> None: - controller = PauseHandler(ctx, self.__bot) + try: + controller = PauseHandler(ctx, self.__bot) - response = await controller.run() - view1 = EmoteView(response) - view2 = EmbedView(response) - await view1.run() - await view2.run() + response = await controller.run() + view1 = EmoteView(response) + view2 = EmbedView(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']) async def resume(self, ctx: Context) -> None: - controller = ResumeHandler(ctx, self.__bot) + try: + controller = ResumeHandler(ctx, self.__bot) - response = await controller.run() - view1 = EmoteView(response) - view2 = EmbedView(response) - await view1.run() - await view2.run() + response = await controller.run() + view1 = EmoteView(response) + view2 = EmbedView(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']) async def prev(self, ctx: Context) -> None: - controller = PrevHandler(ctx, self.__bot) + try: + controller = PrevHandler(ctx, self.__bot) - response = await controller.run() - if response is not None: + response = await controller.run() + if response is not None: + view1 = EmbedView(response) + view2 = EmoteView(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']) + async def history(self, ctx: Context) -> None: + try: + controller = HistoryHandler(ctx, self.__bot) + + response = await controller.run() view1 = EmbedView(response) view2 = EmoteView(response) await view1.run() await view2.run() - - @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) - - response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(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']) async def loop(self, ctx: Context, args='') -> None: - controller = LoopHandler(ctx, self.__bot) + try: + controller = LoopHandler(ctx, self.__bot) - response = await controller.run(args) - view1 = EmoteView(response) - view2 = EmbedView(response) - await view1.run() - await view2.run() + response = await controller.run(args) + view1 = EmoteView(response) + view2 = EmbedView(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']) async def clear(self, ctx: Context) -> None: - controller = ClearHandler(ctx, self.__bot) + try: + controller = ClearHandler(ctx, self.__bot) - response = await controller.run() - view = EmoteView(response) - await view.run() + response = await controller.run() + view = EmoteView(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']) async def now_playing(self, ctx: Context) -> None: - controller = NowPlayingHandler(ctx, self.__bot) + try: + controller = NowPlayingHandler(ctx, self.__bot) - response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() + response = await controller.run() + view1 = EmbedView(response) + view2 = EmoteView(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']) async def shuffle(self, ctx: Context) -> None: - controller = ShuffleHandler(ctx, self.__bot) + try: + controller = ShuffleHandler(ctx, self.__bot) - response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() + response = await controller.run() + view1 = EmbedView(response) + view2 = EmoteView(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']) async def move(self, ctx: Context, pos1, pos2='1') -> None: - controller = MoveHandler(ctx, self.__bot) + try: + controller = MoveHandler(ctx, self.__bot) - response = await controller.run(pos1, pos2) - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() + response = await controller.run(pos1, pos2) + view1 = EmbedView(response) + view2 = EmoteView(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']) async def remove(self, ctx: Context, position) -> None: - controller = RemoveHandler(ctx, self.__bot) + try: + controller = RemoveHandler(ctx, self.__bot) - response = await controller.run(position) - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() + response = await controller.run(position) + view1 = EmbedView(response) + view2 = EmoteView(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']) async def reset(self, ctx: Context) -> None: - controller = ResetHandler(ctx, self.__bot) + try: + controller = ResetHandler(ctx, self.__bot) - response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() + response = await controller.run() + view1 = EmbedView(response) + view2 = EmoteView(response) + await view1.run() + await view2.run() + except Exception as e: + print(f'[ERROR IN COG] -> {e}') def setup(bot): diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index 89572d3..765d0c3 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -16,7 +16,14 @@ class ClearHandler(AbstractHandler): if processInfo: # Clear the playlist playlist = processInfo.getPlaylist() - with processInfo.getLock(): + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: playlist.clear() - + processLock.release() + processLock.release() + else: + processManager.resetProcess(self.guild, self.ctx) + embed = self.embeds.PLAYER_RESTARTED() + return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx) diff --git a/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py index c1989a9..b33c263 100644 --- a/Handlers/HistoryHandler.py +++ b/Handlers/HistoryHandler.py @@ -15,9 +15,17 @@ class HistoryHandler(AbstractHandler): processManager = ProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: - with processInfo.getLock(): + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: playlist = processInfo.getPlaylist() history = playlist.getSongsHistory() + processLock.release() + else: + # If the player doesn't respond in time we restart it + processManager.resetProcess(self.guild, self.ctx) + embed = self.embeds.PLAYER_RESTARTED() + return HandlerResponse(self.ctx, embed) else: history = [] diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index 6654841..976b1b3 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -21,13 +21,16 @@ class LoopHandler(AbstractHandler): playlist = processInfo.getPlaylist() - with processInfo.getLock(): + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: if args == '' or args is None: playlist.loop_all() embed = self.embeds.LOOP_ALL_ACTIVATED() return HandlerResponse(self.ctx, embed) args = args.lower() + error = None if playlist.getCurrentSong() is None: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() @@ -36,16 +39,19 @@ class LoopHandler(AbstractHandler): 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) + + processLock.release() + return HandlerResponse(self.ctx, embed) + else: + processManager.resetProcess(self.guild, self.ctx) + embed = self.embeds.PLAYER_RESTARTED() + return HandlerResponse(self.ctx, embed) diff --git a/Handlers/MoveHandler.py b/Handlers/MoveHandler.py index fc04b50..8ce3ce9 100644 --- a/Handlers/MoveHandler.py +++ b/Handlers/MoveHandler.py @@ -20,10 +20,13 @@ class MoveHandler(AbstractHandler): error = BadCommandUsage() return HandlerResponse(self.ctx, embed, error) - with processInfo.getLock(): + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: error = self.__validateInput(pos1, pos2) if error: embed = self.embeds.ERROR_EMBED(error.message) + processLock.release() return HandlerResponse(self.ctx, embed, error) playlist = processInfo.getPlaylist() @@ -32,17 +35,25 @@ class MoveHandler(AbstractHandler): if not playlist.validate_position(pos1) or not playlist.validate_position(pos2): error = InvalidInput() embed = self.embeds.PLAYLIST_RANGE_ERROR() + processLock.release() return HandlerResponse(self.ctx, embed, error) try: song = playlist.move_songs(pos1, pos2) song_name = song.title if song.title else song.identifier embed = self.embeds.SONG_MOVED(song_name, pos1, pos2) + processLock.release() return HandlerResponse(self.ctx, embed) except: + # Release the acquired Lock + processLock.release() embed = self.embeds.ERROR_MOVING() error = UnknownError() return HandlerResponse(self.ctx, embed, error) + else: + processManager.resetProcess(self.guild, self.ctx) + embed = self.embeds.PLAYER_RESTARTED() + return HandlerResponse(self.ctx, embed) def __validateInput(self, pos1: str, pos2: str) -> Union[VulkanError, None]: try: diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index 26e652e..032321c 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -36,8 +36,8 @@ class PlayHandler(AbstractHandler): raise InvalidInput(self.messages.INVALID_INPUT, self.messages.ERROR_TITLE) # Get the process context for the current guild - manager = ProcessManager() - processInfo = manager.getPlayerInfo(self.guild, self.ctx) + processManager = ProcessManager() + processInfo = processManager.getPlayerInfo(self.guild, self.ctx) playlist = processInfo.getPlaylist() process = processInfo.getProcess() if not process.is_alive(): # If process has not yet started, start @@ -56,22 +56,31 @@ class PlayHandler(AbstractHandler): error = DownloadingError() return HandlerResponse(self.ctx, embed, error) - # Add the unique song to the playlist and send a command to player process - with processInfo.getLock(): - playlist.add_song(song) - queue = processInfo.getQueue() - playCommand = VCommands(VCommandsType.PLAY, None) - queue.put(playCommand) - # If not playing if not playlist.getCurrentSong(): embed = self.embeds.SONG_ADDED(song.title) - return HandlerResponse(self.ctx, embed) + response = HandlerResponse(self.ctx, embed) else: # If already playing pos = len(playlist.getSongs()) embed = self.embeds.SONG_ADDED_TWO(song.info, pos) + response = HandlerResponse(self.ctx, embed) + + # Add the unique song to the playlist and send a command to player process + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: + playlist.add_song(song) + # Release the acquired Lock + processLock.release() + queue = processInfo.getQueue() + playCommand = VCommands(VCommandsType.PLAY, None) + queue.put(playCommand) + else: + processManager.resetProcess(self.guild, self.ctx) + embed = self.embeds.PLAYER_RESTARTED() return HandlerResponse(self.ctx, embed) + return response else: # If multiple songs added # Trigger a task to download all songs and then store them in the process playlist asyncio.create_task(self.__downloadSongsAndStore(songs, processInfo)) diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index d25a1e3..f780d9b 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -15,14 +15,16 @@ class QueueHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Retrieve the process of the guild - process = ProcessManager() - processInfo = process.getRunningPlayerInfo(self.guild) + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: # If no process return empty list embed = self.embeds.EMPTY_QUEUE() return HandlerResponse(self.ctx, embed) # Acquire the Lock to manipulate the playlist - with processInfo.getLock(): + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: playlist = processInfo.getPlaylist() if playlist.isLoopingOne(): @@ -54,4 +56,10 @@ class QueueHandler(AbstractHandler): text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n" embed = self.embeds.QUEUE(title, text) + # Release the acquired Lock + processLock.release() + return HandlerResponse(self.ctx, embed) + else: + processManager.resetProcess(self.guild, self.ctx) + embed = self.embeds.PLAYER_RESTARTED() return HandlerResponse(self.ctx, embed) diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py index 99bc730..94999c8 100644 --- a/Handlers/ShuffleHandler.py +++ b/Handlers/ShuffleHandler.py @@ -15,9 +15,17 @@ class ShuffleHandler(AbstractHandler): processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: try: - with processInfo.getLock(): + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: playlist = processInfo.getPlaylist() playlist.shuffle() + # Release the acquired Lock + processLock.release() + else: + processManager.resetProcess(self.guild, self.ctx) + embed = self.embeds.PLAYER_RESTARTED() + return HandlerResponse(self.ctx, embed) embed = self.embeds.SONGS_SHUFFLED() return HandlerResponse(self.ctx, embed) diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index a62a8f9..5205a1f 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -1,6 +1,6 @@ import asyncio from os import listdir -from discord import Intents, User, Member +from discord import Intents, User, Member, Message, Embed from asyncio import AbstractEventLoop, Semaphore from multiprocessing import Process, Queue, RLock from threading import Lock, Thread @@ -40,7 +40,6 @@ class PlayerProcess(Process): # Synchronization objects self.__playlist: Playlist = playlist self.__playlistLock: Lock = lock - self.__playerLock: RLock = None self.__queue: Queue = queue self.__semStopPlaying: Semaphore = None self.__loop: AbstractEventLoop = None @@ -60,6 +59,7 @@ class PlayerProcess(Process): self.__configs: Configs = None self.__embeds: Embeds = None self.__messages: Messages = None + self.__messagesToDelete: List[Message] = [] self.__playing = False self.__forceStop = False self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', @@ -117,6 +117,7 @@ class PlayerProcess(Process): async def __playSong(self, song: Song) -> None: """Function that will trigger the player to play the song""" try: + self.__playerLock.acquire() if song is None: return @@ -125,13 +126,14 @@ class PlayerProcess(Process): # If not connected, connect to bind channel if self.__guild.voice_client is None: - self.__connectToVoiceChannel() + await self.__connectToVoiceChannel() # If the player is already playing return if self.__guild.voice_client.is_playing(): return self.__playing = True + self.__playingSong = song player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) self.__guild.voice_client.play(player, after=lambda e: self.__playNext(e)) @@ -139,10 +141,13 @@ class PlayerProcess(Process): self.__timer.cancel() self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) + await self.__deletePrevNowPlaying() await self.__showNowPlaying() except Exception as e: print(f'[ERROR IN PLAY SONG] -> {e}, {type(e)}') self.__playNext(None) + finally: + self.__playerLock.release() def __playNext(self, error) -> None: with self.__playerLock: @@ -158,7 +163,7 @@ class PlayerProcess(Process): else: with self.__playlistLock: self.__playlist.loop_off() - + self.__playingSong = None self.__playing = False async def __playPrev(self, voiceChannelID: int) -> None: @@ -169,7 +174,7 @@ class PlayerProcess(Process): 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) - self.__connectToVoiceChannel() + await self.__connectToVoiceChannel() # If already playing, stop the current play if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): @@ -232,27 +237,37 @@ class PlayerProcess(Process): with self.__playlistLock: self.__playlist.clear() self.__playlist.loop_off() - self.__guild.voice_client.stop() - await self.__guild.voice_client.disconnect() + + self.__guild.voice_client.stop() + self.__playingSong = None + await self.__guild.voice_client.disconnect() + self.__semStopPlaying.release() def __resume(self) -> None: - if self.__guild.voice_client is not None: - if self.__guild.voice_client.is_paused(): - self.__guild.voice_client.resume() + # Lock to work with Player + with self.__playerLock: + if self.__guild.voice_client is not None: + if self.__guild.voice_client.is_paused(): + self.__guild.voice_client.resume() def __skip(self) -> None: - if self.__guild.voice_client is not None: - self.__guild.voice_client.stop() + # Lock to work with Player + with self.__playerLock: + if self.__guild.voice_client is not None and self.__playing: + self.__playing = None + self.__guild.voice_client.stop() async def __forceStop(self) -> None: - if self.__guild.voice_client is None: - return + # Lock to work with Player + with self.__playerLock: + if self.__guild.voice_client is None: + return - self.__guild.voice_client.stop() - await self.__guild.voice_client.disconnect() - with self.__playlistLock: - self.__playlist.clear() - self.__playlist.loop_off() + self.__guild.voice_client.stop() + await self.__guild.voice_client.disconnect() + with self.__playlistLock: + self.__playlist.clear() + self.__playlist.loop_off() async def __createBotInstance(self) -> Client: """Load a new bot instance that should not be directly called. @@ -282,7 +297,6 @@ class PlayerProcess(Process): async def __timeoutHandler(self) -> None: try: - print('TimeoutHandler') if self.__guild.voice_client is None: return @@ -290,27 +304,17 @@ class PlayerProcess(Process): self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) elif self.__guild.voice_client.is_connected(): - self.__playerLock.acquire() - with self.__playlistLock: - self.__playlist.clear() - self.__playlist.loop_off() - self.__playing = False - await self.__guild.voice_client.disconnect() - # Release semaphore to finish process - self.__playerLock.release() - self.__semStopPlaying.release() + with self.__playerLock: + with self.__playlistLock: + self.__playlist.clear() + self.__playlist.loop_off() + self.__playing = False + await self.__guild.voice_client.disconnect() + # Release semaphore to finish process + self.__semStopPlaying.release() except Exception as e: print(f'[Error in Timeout] -> {e}') - def __is_connected(self) -> bool: - try: - if not self.__voiceChannel.is_connected(): - return False - else: - return True - except: - return False - async def __ensureDiscordConnection(self, bot: Client) -> None: """Await in this point until connection to discord is established""" guild = None @@ -333,9 +337,9 @@ class PlayerProcess(Process): return member async def __showNowPlaying(self) -> None: - # Get the current process of the guild + # Get the lock of the playlist with self.__playlistLock: - if not self.__playing or self.__playlist.getCurrentSong() is None: + if not self.__playing or self.__playingSong is None: embed = self.__embeds.NOT_PLAYING() await self.__textChannel.send(embed=embed) return @@ -345,6 +349,32 @@ class PlayerProcess(Process): else: title = self.__messages.SONG_PLAYING - info = self.__playlist.getCurrentSong().info + 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/ProcessManager.py b/Parallelism/ProcessManager.py index 53829f1..6df1b73 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -7,6 +7,7 @@ from discord.ext.commands import Context from Parallelism.PlayerProcess import PlayerProcess from Music.Playlist import Playlist from Parallelism.ProcessInfo import ProcessInfo +from Parallelism.Commands import VCommands, VCommandsType class ProcessManager(Singleton): @@ -39,6 +40,19 @@ class ProcessManager(Singleton): except Exception as e: print(f'[Error In GetPlayerContext] -> {e}') + def resetProcess(self, guild: Guild, context: Context) -> None: + """Restart a running process, already start it to return to play""" + if guild.id not in self.__playersProcess.keys(): + return None + + # Recreate the process keeping the playlist + newProcessInfo = self.__recreateProcess(context) + newProcessInfo.getProcess().start() # Start the process + # Send a command to start the play again + playCommand = VCommands(VCommandsType.PLAY) + newProcessInfo.getQueue().put(playCommand) + self.__playersProcess[guild.id] = newProcessInfo + def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo: """Return the process info for the guild, if not, return None""" if guild.id not in self.__playersProcess.keys(): diff --git a/Views/Embeds.py b/Views/Embeds.py index 447866a..e9c9329 100644 --- a/Views/Embeds.py +++ b/Views/Embeds.py @@ -231,6 +231,13 @@ class Embeds: colour=self.__colors.BLACK) return embed + def PLAYER_RESTARTED(self) -> Embed: + embed = Embed( + title=self.__messages.ERROR_TITLE, + description=self.__messages.ERROR_IN_PROCESS, + colour=self.__colors.BLACK) + return embed + def NO_CHANNEL(self) -> Embed: embed = Embed( title=self.__messages.IMPOSSIBLE_MOVE, From 140c1640d94a6240f522cc2abf03e291986bb471 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Mon, 25 Jul 2022 09:49:04 -0300 Subject: [PATCH 10/10] Stability update --- Config/Messages.py | 1 + DiscordCogs/ControlCog.py | 6 +++--- DiscordCogs/MusicCog.py | 2 +- Handlers/ClearHandler.py | 2 +- Handlers/LoopHandler.py | 3 ++- Handlers/PauseHandler.py | 5 ++++- Handlers/PlayHandler.py | 9 ++++++++- Handlers/PrevHandler.py | 1 + Handlers/QueueHandler.py | 5 ++--- Handlers/ResetHandler.py | 5 ++++- Handlers/ResumeHandler.py | 5 ++++- Handlers/SkipHandler.py | 6 +++++- Handlers/StopHandler.py | 3 +++ Music/Downloader.py | 4 ---- Parallelism/PlayerProcess.py | 2 +- Views/Embeds.py | 2 +- 16 files changed, 41 insertions(+), 20 deletions(-) diff --git a/Config/Messages.py b/Config/Messages.py index c1c5693..47ab1b5 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -58,6 +58,7 @@ class Messages(Singleton): self.PLAYER_NOT_PLAYING = f'❌ 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' diff --git a/DiscordCogs/ControlCog.py b/DiscordCogs/ControlCog.py index 6a9e022..e0e62da 100644 --- a/DiscordCogs/ControlCog.py +++ b/DiscordCogs/ControlCog.py @@ -66,9 +66,9 @@ class ControlCog(commands.Cog): return embedhelp = Embed( - title='Command Help', - description=f'Command {command_help} Not Found', - colour=self.__colors.RED + title='Help', + description=f'Command {command_help} do not exists, type {self.__config.BOT_PREFIX}help to see all commands', + colour=self.__colors.BLACK ) await ctx.send(embed=embedhelp) diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 7a6e7dd..4f3b137 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -114,7 +114,7 @@ class MusicCog(commands.Cog): 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']) + @commands.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) diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index 765d0c3..df2d822 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -22,8 +22,8 @@ class ClearHandler(AbstractHandler): playlist.clear() processLock.release() processLock.release() + return HandlerResponse(self.ctx) else: processManager.resetProcess(self.guild, self.ctx) embed = self.embeds.PLAYER_RESTARTED() return HandlerResponse(self.ctx, embed) - return HandlerResponse(self.ctx) diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index 976b1b3..b9b0bf4 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -27,6 +27,7 @@ class LoopHandler(AbstractHandler): if args == '' or args is None: playlist.loop_all() embed = self.embeds.LOOP_ALL_ACTIVATED() + processLock.release() return HandlerResponse(self.ctx, embed) args = args.lower() @@ -50,7 +51,7 @@ class LoopHandler(AbstractHandler): embed = self.embeds.BAD_LOOP_USE() processLock.release() - return HandlerResponse(self.ctx, embed) + return HandlerResponse(self.ctx, embed, error) else: processManager.resetProcess(self.guild, self.ctx) embed = self.embeds.PLAYER_RESTARTED() diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py index 793e2fe..20afe7b 100644 --- a/Handlers/PauseHandler.py +++ b/Handlers/PauseHandler.py @@ -19,4 +19,7 @@ class PauseHandler(AbstractHandler): queue = processInfo.getQueue() queue.put(command) - return HandlerResponse(self.ctx) + return HandlerResponse(self.ctx) + else: + embed = self.embeds.NOT_PLAYING() + return HandlerResponse(self.ctx, embed) diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index 032321c..b9636c1 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -113,13 +113,20 @@ 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() 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 - with processInfo.getLock(): + processInfo = processManager.getPlayerInfo(self.guild, self.ctx) + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: playlist.add_song(song) queue.put(playCommand) + processLock.release() + else: + processManager.resetProcess(self.guild, self.ctx) def __isUserConnected(self) -> bool: if self.ctx.author.voice: diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index 503e910..a1336ae 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -44,6 +44,7 @@ class PrevHandler(AbstractHandler): prevCommand = VCommands(VCommandsType.PREV, self.ctx.author.voice.channel.id) queue = processInfo.getQueue() queue.put(prevCommand) + return HandlerResponse(self.ctx) def __user_connected(self) -> bool: if self.ctx.author.voice: diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index f780d9b..953287a 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -1,4 +1,3 @@ -import asyncio from discord.ext.commands import Context from discord import Client from Handlers.AbstractHandler import AbstractHandler @@ -30,16 +29,16 @@ class QueueHandler(AbstractHandler): if playlist.isLoopingOne(): song = playlist.getCurrentSong() embed = self.embeds.ONE_SONG_LOOPING(song.info) + processLock.release() # Release the Lock return HandlerResponse(self.ctx, embed) songs_preload = playlist.getSongsToPreload() allSongs = playlist.getSongs() if len(songs_preload) == 0: embed = self.embeds.EMPTY_QUEUE() + processLock.release() # Release the Lock return HandlerResponse(self.ctx, embed) - asyncio.create_task(self.__down.preload(songs_preload)) - if playlist.isLoopingAll(): title = self.messages.ALL_SONGS_LOOPING else: diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py index abe1c16..c938d21 100644 --- a/Handlers/ResetHandler.py +++ b/Handlers/ResetHandler.py @@ -19,4 +19,7 @@ class ResetHandler(AbstractHandler): queue = processInfo.getQueue() queue.put(command) - return HandlerResponse(self.ctx) + return HandlerResponse(self.ctx) + else: + embed = self.embeds.NOT_PLAYING() + return HandlerResponse(self.ctx, embed) diff --git a/Handlers/ResumeHandler.py b/Handlers/ResumeHandler.py index 83b83f1..bd5c254 100644 --- a/Handlers/ResumeHandler.py +++ b/Handlers/ResumeHandler.py @@ -19,4 +19,7 @@ class ResumeHandler(AbstractHandler): queue = processInfo.getQueue() queue.put(command) - return HandlerResponse(self.ctx) + return HandlerResponse(self.ctx) + else: + embed = self.embeds.NOT_PLAYING() + return HandlerResponse(self.ctx, embed) diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index f03276b..ab0355d 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -25,4 +25,8 @@ class SkipHandler(AbstractHandler): command = VCommands(VCommandsType.SKIP, None) queue = processInfo.getQueue() queue.put(command) - return HandlerResponse(self.ctx) + + return HandlerResponse(self.ctx) + else: + embed = self.embeds.NOT_PLAYING() + return HandlerResponse(self.ctx, embed) diff --git a/Handlers/StopHandler.py b/Handlers/StopHandler.py index 434d842..669a953 100644 --- a/Handlers/StopHandler.py +++ b/Handlers/StopHandler.py @@ -20,3 +20,6 @@ class StopHandler(AbstractHandler): queue.put(command) return HandlerResponse(self.ctx) + else: + embed = self.embeds.NOT_PLAYING() + return HandlerResponse(self.ctx, embed) diff --git a/Music/Downloader.py b/Music/Downloader.py index ad8f417..3b97c8d 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -56,10 +56,6 @@ class Downloader: except DownloadError: raise DownloadingError() - async def preload(self, songs: List[Song]) -> None: - for song in songs: - asyncio.ensure_future(self.download_song(song)) - @run_async def extract_info(self, url: str) -> List[dict]: if url == '': diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 5205a1f..3fc49ee 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -254,7 +254,7 @@ class PlayerProcess(Process): # Lock to work with Player with self.__playerLock: if self.__guild.voice_client is not None and self.__playing: - self.__playing = None + self.__playing = False self.__guild.voice_client.stop() async def __forceStop(self) -> None: diff --git a/Views/Embeds.py b/Views/Embeds.py index e9c9329..ef60595 100644 --- a/Views/Embeds.py +++ b/Views/Embeds.py @@ -164,7 +164,7 @@ class Embeds: def COMMAND_NOT_FOUND(self) -> Embed: embed = Embed( - title=self.__messages.ERROR_TITLE, + title=self.__messages.COMMAND_NOT_FOUND_TITLE, description=self.__messages.COMMAND_NOT_FOUND, colour=self.__colors.BLACK )