From b904c75caa2972264cf42634cec3dd0180fda1df Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sun, 24 Jul 2022 14:25:44 -0300 Subject: [PATCH] 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):