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/Music.py b/Commands/Music.py deleted file mode 100644 index 759b26a..0000000 --- a/Commands/Music.py +++ /dev/null @@ -1,197 +0,0 @@ -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.PlayerController import PlayersController -from Controllers.PrevController import PrevController -from Controllers.RemoveController import RemoveController -from Controllers.ResetController import ResetController -from Controllers.ShuffleController import ShuffleController -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 Views.EmoteView import EmoteView -from Views.EmbedView import EmbedView - - -helper = Helper() - - -class Music(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: - controller = PlayController(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() - - @commands.command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila']) - async def queue(self, ctx: Context) -> None: - controller = QueueController(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']) - async def skip(self, ctx: Context) -> None: - controller = SkipController(ctx, self.__bot) - - response = await controller.run() - if response.success: - view = EmoteView(response) - else: - view = EmbedView(response) - - await view.run() - - @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) - - response = await controller.run() - if response.success: - view = EmoteView(response) - else: - view = EmbedView(response) - - await view.run() - - @commands.command(name='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar']) - async def pause(self, ctx: Context) -> None: - controller = PauseController(ctx, self.__bot) - - response = await controller.run() - view1 = EmoteView(response) - view2 = EmbedView(response) - await view1.run() - await view2.run() - - @commands.command(name='resume', help=helper.HELP_RESUME, description=helper.HELP_RESUME_LONG, aliases=['soltar']) - async def resume(self, ctx: Context) -> None: - controller = ResumeController(ctx, self.__bot) - - response = await controller.run() - view1 = EmoteView(response) - view2 = EmbedView(response) - await view1.run() - await view2.run() - - @commands.command(name='prev', help=helper.HELP_PREV, description=helper.HELP_PREV_LONG, aliases=['anterior']) - async def prev(self, ctx: Context) -> None: - controller = PrevController(ctx, self.__bot) - - response = await controller.run() - if response is not None: - 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']) - async def history(self, ctx: Context) -> None: - controller = HistoryController(ctx, self.__bot) - - response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() - - @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) - - response = await controller.run(args) - view1 = EmoteView(response) - view2 = EmbedView(response) - await view1.run() - await view2.run() - - @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) - - 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']) - async def now_playing(self, ctx: Context) -> None: - controller = NowPlayingController(ctx, self.__bot) - - response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() - - @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) - - response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() - - @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) - - response = await controller.run(pos1, pos2) - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() - - @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) - - response = await controller.run(position) - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() - - @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) - - response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) - await view1.run() - await view2.run() - - -def setup(bot): - bot.add_cog(Music(bot)) diff --git a/Config/Configs.py b/Config/Configs.py index ef3eecc..09395bb 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -16,12 +16,13 @@ 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.ACQUIRE_LOCK_TIMEOUT = 10 + self.COMMANDS_PATH = 'DiscordCogs' self.VC_TIMEOUT = 600 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/Exceptions/Exceptions.py b/Config/Exceptions.py similarity index 100% rename from Exceptions/Exceptions.py rename to Config/Exceptions.py diff --git a/Config/Messages.py b/Config/Messages.py index e89bc56..47ab1b5 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' @@ -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' @@ -56,12 +58,14 @@ 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' - 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.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/Controllers/ClearController.py b/Controllers/ClearController.py deleted file mode 100644 index ab54f38..0000000 --- a/Controllers/ClearController.py +++ /dev/null @@ -1,13 +0,0 @@ -from discord.ext.commands import Context -from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse - - -class ClearController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - - async def run(self) -> ControllerResponse: - self.player.playlist.clear() - return ControllerResponse(self.ctx) diff --git a/Controllers/HistoryController.py b/Controllers/HistoryController.py deleted file mode 100644 index 4a40b47..0000000 --- a/Controllers/HistoryController.py +++ /dev/null @@ -1,24 +0,0 @@ -from discord.ext.commands import Context -from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse -from Utils.Utils import Utils - - -class HistoryController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - - async def run(self) -> ControllerResponse: - history = self.player.playlist.songs_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): - text += f"**`{pos}` - ** {song.title} - `{Utils.format_time(song.duration)}`\n" - - embed = self.embeds.HISTORY(text) - return ControllerResponse(self.ctx, embed) diff --git a/Controllers/LoopController.py b/Controllers/LoopController.py deleted file mode 100644 index 2a2853c..0000000 --- a/Controllers/LoopController.py +++ /dev/null @@ -1,39 +0,0 @@ -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 - - -class LoopController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - - async def run(self, args: str) -> ControllerResponse: - if args == '' or args is None: - self.player.playlist.loop_all() - embed = self.embeds.LOOP_ALL_ACTIVATED() - return ControllerResponse(self.ctx, embed) - - args = args.lower() - if self.player.playlist.current is None: - embed = self.embeds.NOT_PLAYING() - error = BadCommandUsage() - return ControllerResponse(self.ctx, embed, error) - - if args == 'one': - self.player.playlist.loop_one() - embed = self.embeds.LOOP_ONE_ACTIVATED() - return ControllerResponse(self.ctx, embed) - elif args == 'all': - self.player.playlist.loop_all() - embed = self.embeds.LOOP_ALL_ACTIVATED() - return ControllerResponse(self.ctx, embed) - elif args == 'off': - self.player.playlist.loop_off() - embed = self.embeds.LOOP_DISABLE() - return ControllerResponse(self.ctx, embed) - else: - error = BadCommandUsage() - embed = self.embeds.BAD_LOOP_USE() - return ControllerResponse(self.ctx, embed, error) diff --git a/Controllers/MoveController.py b/Controllers/MoveController.py deleted file mode 100644 index e5292f0..0000000 --- a/Controllers/MoveController.py +++ /dev/null @@ -1,63 +0,0 @@ -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 Music.Downloader import Downloader - - -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: - 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) - - pos1, pos2 = self.__sanitize_input(pos1, pos2) - playlist = self.player.playlist - - 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) - - songs = self.player.playlist.songs_to_preload - 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) - - def __validate_input(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: - pos1 = int(pos1) - pos2 = int(pos2) - - if pos1 == -1: - pos1 = len(self.player.playlist) - if pos2 == -1: - pos2 = len(self.player.playlist) - - return pos1, pos2 diff --git a/Controllers/NowPlayingController.py b/Controllers/NowPlayingController.py deleted file mode 100644 index 35b684e..0000000 --- a/Controllers/NowPlayingController.py +++ /dev/null @@ -1,26 +0,0 @@ -from discord.ext.commands import Context -from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse -from Utils.Cleaner import Cleaner - - -class NowPlayingController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - self.__cleaner = Cleaner() - - async def run(self) -> ControllerResponse: - if not self.player.playing: - embed = self.embeds.NOT_PLAYING() - return ControllerResponse(self.ctx, embed) - - if self.player.playlist.looping_one: - 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 - embed = self.embeds.SONG_INFO(info, title) - return ControllerResponse(self.ctx, embed) diff --git a/Controllers/PauseController.py b/Controllers/PauseController.py deleted file mode 100644 index 58a43bc..0000000 --- a/Controllers/PauseController.py +++ /dev/null @@ -1,16 +0,0 @@ -from discord.ext.commands import Context -from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse - - -class PauseController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - 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() - - return ControllerResponse(self.ctx) diff --git a/Controllers/PlayController.py b/Controllers/PlayController.py deleted file mode 100644 index c0c73db..0000000 --- a/Controllers/PlayController.py +++ /dev/null @@ -1,103 +0,0 @@ -import asyncio -from Exceptions.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 Music.Downloader import Downloader -from Music.Searcher import Searcher -from Music.Song import Song - - -class PlayController(AbstractController): - 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: - track = " ".join(args) - requester = self.ctx.author.name - - if not self.__user_connected(): - error = ImpossibleMove() - 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: - 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.songs_to_preload - 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 = ControllerResponse(self.ctx, embed, error) - - elif not self.player.playing: - embed = self.embeds.SONG_ADDED(song.title) - response = ControllerResponse(self.ctx, embed) - else: - embed = self.embeds.SONG_ADDED_TWO(song.info, pos) - response = ControllerResponse(self.ctx, embed) - else: - embed = self.embeds.SONGS_ADDED(quant) - response = ControllerResponse(self.ctx, embed) - - asyncio.create_task(self.player.play(self.ctx)) - return response - - except Exception as err: - if isinstance(err, VulkanError): # If error was already processed - print(f'DEVELOPER NOTE -> PlayController Error: {err.message}') - error = err - embed = self.embeds.CUSTOM_ERROR(error) - else: - error = UnknownError() - embed = self.embeds.UNKNOWN_ERROR() - - return ControllerResponse(self.ctx, embed, error) - - 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/Controllers/PlayerController.py b/Controllers/PlayerController.py deleted file mode 100644 index 28293cc..0000000 --- a/Controllers/PlayerController.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/Controllers/PrevController.py b/Controllers/PrevController.py deleted file mode 100644 index 8be250a..0000000 --- a/Controllers/PrevController.py +++ /dev/null @@ -1,60 +0,0 @@ -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 - - -class PrevController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - - async def run(self) -> ControllerResponse: - if len(self.player.playlist.history()) == 0: - error = ImpossibleMove() - embed = self.embeds.NOT_PREVIOUS_SONG() - return ControllerResponse(self.ctx, embed, error) - - if not self.__user_connected(): - error = ImpossibleMove() - 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) - - if self.player.playlist.looping_all or self.player.playlist.looping_one: - error = BadCommandUsage() - embed = self.embeds.FAIL_DUE_TO_LOOP_ON() - return ControllerResponse(self.ctx, embed, error) - - await self.player.play_prev(self.ctx) - - 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/Controllers/QueueController.py b/Controllers/QueueController.py deleted file mode 100644 index 8a5014f..0000000 --- a/Controllers/QueueController.py +++ /dev/null @@ -1,44 +0,0 @@ -import asyncio -from discord.ext.commands import Context -from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse -from Music.Downloader import Downloader -from Utils.Utils import Utils - - -class QueueController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - 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: - embed = self.embeds.EMPTY_QUEUE() - return ControllerResponse(self.ctx, embed) - - asyncio.create_task(self.__down.preload(songs_preload)) - - if self.player.playlist.looping_all: - title = self.messages.ALL_SONGS_LOOPING - else: - title = self.messages.QUEUE_TITLE - - 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) - - 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/RemoveController.py b/Controllers/RemoveController.py deleted file mode 100644 index 97bf73d..0000000 --- a/Controllers/RemoveController.py +++ /dev/null @@ -1,52 +0,0 @@ -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 - - -class RemoveController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - - async def run(self, position: str) -> ControllerResponse: - if not self.player.playlist: - embed = self.embeds.NOT_PLAYING() - error = BadCommandUsage() - return ControllerResponse(self.ctx, embed, error) - - error = self.__validate_input(position) - if error: - embed = self.embeds.ERROR_EMBED(error.message) - return ControllerResponse(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) - - 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) - except: - error = ErrorRemoving() - embed = self.embeds.ERROR_REMOVING() - return ControllerResponse(self.ctx, embed, error) - - def __validate_input(self, position: str) -> Union[VulkanError, None]: - try: - position = int(position) - except: - return NumberRequired(self.messages.ERROR_NUMBER) - - def __sanitize_input(self, position: str) -> int: - position = int(position) - - if position == -1: - position = len(self.player.playlist) - return position diff --git a/Controllers/ResetController.py b/Controllers/ResetController.py deleted file mode 100644 index 43e0aa6..0000000 --- a/Controllers/ResetController.py +++ /dev/null @@ -1,20 +0,0 @@ -from discord.ext.commands import Context -from discord import Client, Member -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse -from Controllers.PlayerController import PlayersController - - -class ResetController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - self.__controller = PlayersController(self.bot) - - async def run(self) -> ControllerResponse: - try: - await self.player.force_stop() - await self.bot_member.move_to(None) - self.__controller.reset_player(self.guild) - return ControllerResponse(self.ctx) - except Exception as e: - print(f'DEVELOPER NOTE -> Reset Error: {e}') diff --git a/Controllers/ResumeController.py b/Controllers/ResumeController.py deleted file mode 100644 index f319adc..0000000 --- a/Controllers/ResumeController.py +++ /dev/null @@ -1,16 +0,0 @@ -from discord.ext.commands import Context -from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse - - -class ResumeController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - 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() - - return ControllerResponse(self.ctx) diff --git a/Controllers/ShuffleController.py b/Controllers/ShuffleController.py deleted file mode 100644 index 5ad3ac3..0000000 --- a/Controllers/ShuffleController.py +++ /dev/null @@ -1,27 +0,0 @@ -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 Music.Downloader import Downloader - - -class ShuffleController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - self.__down = Downloader() - - async def run(self) -> ControllerResponse: - try: - self.player.playlist.shuffle() - songs = self.player.playlist.songs_to_preload - - asyncio.create_task(self.__down.preload(songs)) - embed = self.embeds.SONGS_SHUFFLED() - return ControllerResponse(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) diff --git a/Controllers/SkipController.py b/Controllers/SkipController.py deleted file mode 100644 index 8c6298b..0000000 --- a/Controllers/SkipController.py +++ /dev/null @@ -1,23 +0,0 @@ -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 - - -class SkipController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - - async def run(self) -> ControllerResponse: - if self.player.playlist.looping_one: - 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) diff --git a/Controllers/StopController.py b/Controllers/StopController.py deleted file mode 100644 index f3ed2c8..0000000 --- a/Controllers/StopController.py +++ /dev/null @@ -1,23 +0,0 @@ -from discord.ext.commands import Context -from discord import Client -from Controllers.AbstractController import AbstractController -from Controllers.ControllerResponse import ControllerResponse - - -class StopController(AbstractController): - def __init__(self, ctx: Context, bot: Client) -> None: - super().__init__(ctx, bot) - - async def run(self) -> ControllerResponse: - if self.guild.voice_client is None: - 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/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/Commands/Control.py b/DiscordCogs/ControlCog.py similarity index 85% rename from Commands/Control.py rename to DiscordCogs/ControlCog.py index 58d5167..e0e62da 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 @@ -18,7 +19,7 @@ class Control(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', @@ -44,7 +45,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) @@ -65,9 +66,9 @@ class Control(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) @@ -79,10 +80,10 @@ class Control(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: @@ -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/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py new file mode 100644 index 0000000..4f3b137 --- /dev/null +++ b/DiscordCogs/MusicCog.py @@ -0,0 +1,235 @@ +from discord import Guild, Client +from discord.ext import commands +from discord.ext.commands import Context +from Config.Helper import Helper +from Handlers.ClearHandler import ClearHandler +from Handlers.MoveHandler import MoveHandler +from Handlers.NowPlayingHandler import NowPlayingHandler +from Handlers.PlayHandler import PlayHandler +from Handlers.PrevHandler import PrevHandler +from Handlers.RemoveHandler import RemoveHandler +from Handlers.ResetHandler import ResetHandler +from Handlers.ShuffleHandler import ShuffleHandler +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 + +helper = Helper() + + +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 + + @commands.command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) + async def play(self, ctx: Context, *args) -> None: + try: + controller = PlayHandler(ctx, self.__bot) + + response = await controller.run(args) + if response is not None: + view1 = EmbedView(response) + view2 = EmoteView(response) + 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: + try: + controller = QueueHandler(ctx, self.__bot) + + 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: + try: + controller = SkipHandler(ctx, self.__bot) + + response = await controller.run() + if response.success: + view = EmoteView(response) + else: + view = EmbedView(response) + + await view.run() + except Exception as e: + print(f'[ERROR IN COG] -> {e}') + + @commands.command(name='stop', help=helper.HELP_STOP, description=helper.HELP_STOP_LONG, aliases=['parar']) + async def stop(self, ctx: Context) -> None: + try: + controller = StopHandler(ctx, self.__bot) + + response = await controller.run() + if response.success: + view = EmoteView(response) + else: + view = EmbedView(response) + + await view.run() + except Exception as e: + print(f'[ERROR IN COG] -> {e}') + + @commands.command(name='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar', 'pare']) + async def pause(self, ctx: Context) -> None: + try: + controller = PauseHandler(ctx, self.__bot) + + 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: + try: + controller = ResumeHandler(ctx, self.__bot) + + 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', 'back']) + async def prev(self, ctx: Context) -> None: + try: + controller = PrevHandler(ctx, self.__bot) + + response = await controller.run() + if response is not None: + view1 = EmbedView(response) + view2 = EmoteView(response) + 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() + 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: + try: + controller = LoopHandler(ctx, self.__bot) + + 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: + try: + controller = ClearHandler(ctx, self.__bot) + + 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: + try: + controller = NowPlayingHandler(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='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio', 'misturar']) + async def shuffle(self, ctx: Context) -> None: + try: + controller = ShuffleHandler(ctx, self.__bot) + + response = await controller.run() + view1 = EmbedView(response) + view2 = EmoteView(response) + 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: + try: + controller = MoveHandler(ctx, self.__bot) + + 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: + try: + controller = RemoveHandler(ctx, self.__bot) + + 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: + try: + controller = ResetHandler(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}') + + +def setup(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..b2ec3fc 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']) 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 76% rename from Controllers/AbstractController.py rename to Handlers/AbstractHandler.py index 81c69b5..93e7e58 100644 --- a/Controllers/AbstractController.py +++ b/Handlers/AbstractHandler.py @@ -3,19 +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.PlayerController 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) - 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 @@ -27,7 +23,7 @@ class AbstractController(ABC): self.__bot_member: Member = self.__get_member() @abstractmethod - async def run(self) -> ControllerResponse: + async def run(self) -> HandlerResponse: pass @property @@ -46,14 +42,6 @@ class AbstractController(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/ClearHandler.py b/Handlers/ClearHandler.py new file mode 100644 index 0000000..df2d822 --- /dev/null +++ b/Handlers/ClearHandler.py @@ -0,0 +1,29 @@ +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Parallelism.ProcessManager import ProcessManager + + +class ClearHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self) -> HandlerResponse: + # Get the current process of the guild + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: + # Clear the playlist + playlist = processInfo.getPlaylist() + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: + 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) 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/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py new file mode 100644 index 0000000..b33c263 --- /dev/null +++ b/Handlers/HistoryHandler.py @@ -0,0 +1,40 @@ +from discord.ext.commands import Context +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): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self) -> HandlerResponse: + # Get the current process of the guild + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: + 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 = [] + + 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): + text += f"**`{pos}` - ** {song.title} - `{Utils.format_time(song.duration)}`\n" + + embed = self.embeds.HISTORY(text) + return HandlerResponse(self.ctx, embed) diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py new file mode 100644 index 0000000..b9b0bf4 --- /dev/null +++ b/Handlers/LoopHandler.py @@ -0,0 +1,58 @@ +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Config.Exceptions import BadCommandUsage +from Parallelism.ProcessManager import ProcessManager + + +class LoopHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self, args: str) -> HandlerResponse: + # Get the current process of the guild + 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() + + 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() + processLock.release() + return HandlerResponse(self.ctx, embed) + + args = args.lower() + error = None + 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() + elif args == 'all': + playlist.loop_all() + embed = self.embeds.LOOP_ALL_ACTIVATED() + elif args == 'off': + playlist.loop_off() + embed = self.embeds.LOOP_DISABLE() + else: + error = BadCommandUsage() + embed = self.embeds.BAD_LOOP_USE() + + processLock.release() + return HandlerResponse(self.ctx, embed, error) + 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 new file mode 100644 index 0000000..8ce3ce9 --- /dev/null +++ b/Handlers/MoveHandler.py @@ -0,0 +1,74 @@ +from typing import Union +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError +from Music.Playlist import Playlist +from Parallelism.ProcessManager import ProcessManager + + +class MoveHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self, pos1: str, pos2: str) -> HandlerResponse: + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if not processInfo: + embed = self.embeds.NOT_PLAYING() + error = BadCommandUsage() + return HandlerResponse(self.ctx, embed, error) + + 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() + 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() + 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: + pos1 = int(pos1) + pos2 = int(pos2) + except: + return NumberRequired(self.messages.ERROR_NUMBER) + + def __sanitizeInput(self, playlist: Playlist, pos1: int, pos2: int) -> tuple: + pos1 = int(pos1) + pos2 = int(pos2) + + if pos1 == -1: + pos1 = len(playlist.getSongs()) + if pos2 == -1: + pos2 = len(playlist.getSongs()) + + return pos1, pos2 diff --git a/Handlers/NowPlayingHandler.py b/Handlers/NowPlayingHandler.py new file mode 100644 index 0000000..93095c9 --- /dev/null +++ b/Handlers/NowPlayingHandler.py @@ -0,0 +1,35 @@ +from discord.ext.commands import Context +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): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + self.__cleaner = Cleaner() + + async def run(self) -> HandlerResponse: + # Get the current process of the guild + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if not processInfo: + embed = self.embeds.NOT_PLAYING() + return HandlerResponse(self.ctx, embed) + + playlist = processInfo.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 = playlist.getCurrentSong().info + embed = self.embeds.SONG_INFO(info, title) + return HandlerResponse(self.ctx, embed) diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py new file mode 100644 index 0000000..20afe7b --- /dev/null +++ b/Handlers/PauseHandler.py @@ -0,0 +1,25 @@ +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType + + +class PauseHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self) -> HandlerResponse: + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: + # Send Pause command to be execute by player process + command = VCommands(VCommandsType.PAUSE, None) + queue = processInfo.getQueue() + queue.put(command) + + 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 new file mode 100644 index 0000000..b9636c1 --- /dev/null +++ b/Handlers/PlayHandler.py @@ -0,0 +1,135 @@ +import asyncio +from typing import List +from Config.Exceptions import DownloadingError, InvalidInput, VulkanError +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Config.Exceptions import ImpossibleMove, UnknownError +from Handlers.HandlerResponse import HandlerResponse +from Music.Downloader import Downloader +from Music.Searcher import Searcher +from Music.Song import Song +from Parallelism.ProcessManager import ProcessManager +from Parallelism.ProcessInfo import ProcessInfo +from Parallelism.Commands import VCommands, VCommandsType + + +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) -> HandlerResponse: + track = " ".join(args) + requester = self.ctx.author.name + + if not self.__isUserConnected(): + error = ImpossibleMove() + embed = self.embeds.NO_CHANNEL() + return HandlerResponse(self.ctx, embed, error) + + try: + # 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) + + # Get the process context for the current guild + 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 + process.start() + + # 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 + 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)) + + embed = self.embeds.SONGS_ADDED(len(songs)) + return HandlerResponse(self.ctx, embed) + + 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: {error}, {type(error)}') + error = UnknownError() + embed = self.embeds.UNKNOWN_ERROR() + + 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 + 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 + 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: + return True + else: + return False diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py new file mode 100644 index 0000000..a1336ae --- /dev/null +++ b/Handlers/PrevHandler.py @@ -0,0 +1,53 @@ +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Config.Exceptions import BadCommandUsage, ImpossibleMove +from Handlers.HandlerResponse import HandlerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType + + +class PrevHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self) -> HandlerResponse: + processManager = ProcessManager() + processInfo = processManager.getPlayerInfo(self.guild, self.ctx) + 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) + + if not self.__user_connected(): + error = ImpossibleMove() + embed = self.embeds.NO_CHANNEL() + return HandlerResponse(self.ctx, embed, error) + + if playlist.isLoopingAll() or playlist.isLoopingOne(): + error = BadCommandUsage() + embed = self.embeds.FAIL_DUE_TO_LOOP_ON() + 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() + queue.put(prevCommand) + return HandlerResponse(self.ctx) + + def __user_connected(self) -> bool: + if self.ctx.author.voice: + return True + else: + return False diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py new file mode 100644 index 0000000..953287a --- /dev/null +++ b/Handlers/QueueHandler.py @@ -0,0 +1,64 @@ +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Music.Downloader import Downloader +from Utils.Utils import Utils +from Parallelism.ProcessManager import ProcessManager + + +class QueueHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + self.__down = Downloader() + + async def run(self) -> HandlerResponse: + # Retrieve the process of the 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 + processLock = processInfo.getLock() + acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: + playlist = processInfo.getPlaylist() + + 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) + + if playlist.isLoopingAll(): + title = self.messages.ALL_SONGS_LOOPING + else: + title = self.messages.QUEUE_TITLE + + total_time = Utils.format_time(sum([int(song.duration if song.duration else 0) + for song in allSongs])) + 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) + # 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/RemoveHandler.py b/Handlers/RemoveHandler.py new file mode 100644 index 0000000..c611f90 --- /dev/null +++ b/Handlers/RemoveHandler.py @@ -0,0 +1,64 @@ +from typing import Union +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired +from Music.Playlist import Playlist +from Parallelism.ProcessManager import ProcessManager + + +class RemoveHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self, position: str) -> HandlerResponse: + # Get the current process of the guild + processManager = ProcessManager() + 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 = processInfo.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.__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 = playlist.remove_song(position) + name = song.title if song.title else song.identifier + + embed = self.embeds.SONG_REMOVED(name) + return HandlerResponse(self.ctx, embed) + except: + error = ErrorRemoving() + embed = self.embeds.ERROR_REMOVING() + return HandlerResponse(self.ctx, embed, error) + + def __validateInput(self, position: str) -> Union[VulkanError, None]: + try: + position = int(position) + except: + return NumberRequired(self.messages.ERROR_NUMBER) + + def __sanitizeInput(self, playlist: Playlist, position: str) -> int: + position = int(position) + + if position == -1: + position = len(playlist.getSongs()) + return position diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py new file mode 100644 index 0000000..c938d21 --- /dev/null +++ b/Handlers/ResetHandler.py @@ -0,0 +1,25 @@ +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType + + +class ResetHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self) -> HandlerResponse: + # Get the current process of the guild + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: + command = VCommands(VCommandsType.RESET, None) + queue = processInfo.getQueue() + queue.put(command) + + 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 new file mode 100644 index 0000000..bd5c254 --- /dev/null +++ b/Handlers/ResumeHandler.py @@ -0,0 +1,25 @@ +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType + + +class ResumeHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self) -> HandlerResponse: + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: + # Send Resume command to be execute by player process + command = VCommands(VCommandsType.RESUME, None) + queue = processInfo.getQueue() + queue.put(command) + + return HandlerResponse(self.ctx) + else: + embed = self.embeds.NOT_PLAYING() + return HandlerResponse(self.ctx, embed) diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py new file mode 100644 index 0000000..94999c8 --- /dev/null +++ b/Handlers/ShuffleHandler.py @@ -0,0 +1,41 @@ +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Config.Exceptions import UnknownError +from Parallelism.ProcessManager import ProcessManager + + +class ShuffleHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self) -> HandlerResponse: + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: + try: + 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) + + 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) diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py new file mode 100644 index 0000000..ab0355d --- /dev/null +++ b/Handlers/SkipHandler.py @@ -0,0 +1,32 @@ +from discord.ext.commands import Context +from discord import Client +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 SkipHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self) -> HandlerResponse: + processManager = ProcessManager() + 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() + return HandlerResponse(self.ctx, embed, error) + + # Send a command to the player process to skip the music + command = VCommands(VCommandsType.SKIP, None) + queue = processInfo.getQueue() + queue.put(command) + + 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 new file mode 100644 index 0000000..669a953 --- /dev/null +++ b/Handlers/StopHandler.py @@ -0,0 +1,25 @@ +from discord.ext.commands import Context +from discord import Client +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from Parallelism.ProcessManager import ProcessManager +from Parallelism.Commands import VCommands, VCommandsType + + +class StopHandler(AbstractHandler): + def __init__(self, ctx: Context, bot: Client) -> None: + super().__init__(ctx, bot) + + async def run(self) -> HandlerResponse: + processManager = ProcessManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) + if processInfo: + # Send command to player process stop + command = VCommands(VCommandsType.STOP, None) + queue = processInfo.getQueue() + queue.put(command) + + return HandlerResponse(self.ctx) + else: + embed = self.embeds.NOT_PLAYING() + return HandlerResponse(self.ctx, embed) 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..3b97c8d 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -1,13 +1,14 @@ 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(): +class Downloader: config = Configs() __YDL_OPTIONS = {'format': 'bestaudio/best', 'default_search': 'auto', @@ -40,20 +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 - - async def preload(self, songs: List[Song]) -> None: - for song in songs: - asyncio.ensure_future(self.download_song(song)) + song.finish_down(song_info) + return song + # Convert yt_dlp error to my own error + except DownloadError: + raise DownloadingError() @run_async def extract_info(self, url: str) -> List[dict]: @@ -81,8 +82,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 a9c9722..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.songs_to_preload - 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 53fa14a..33bcbce 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 @@ -29,25 +32,23 @@ 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]: - return list(self.__queue)[:self.__config.MAX_PRELOAD_SONGS] + def setCurrentSong(self, song: Song) -> Song: + self.__current = song + + def getSongsToPreload(self) -> List[Song]: + return list(self.__queue)[:self.__configs.MAX_PRELOAD_SONGS] def __len__(self) -> int: return len(self.__queue) @@ -64,7 +65,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 @@ -75,6 +76,7 @@ class Playlist: # Get the new song if len(self.__queue) == 0: + self.__current = None return None self.__current = self.__queue.popleft() @@ -135,7 +137,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/Music/Searcher.py b/Music/Searcher.py index d92343e..c9a6155 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, VulkanError, 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 @@ -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/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/Commands.py b/Parallelism/Commands.py new file mode 100644 index 0000000..d6c8baa --- /dev/null +++ b/Parallelism/Commands.py @@ -0,0 +1,25 @@ +from enum import Enum +from typing import Tuple + + +class VCommandsType(Enum): + PREV = 'Prev' + SKIP = 'Skip' + PAUSE = 'Pause' + RESUME = 'Resume' + CONTEXT = 'Context' + PLAY = 'Play' + STOP = 'Stop' + RESET = 'Reset' + + +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..3fc49ee --- /dev/null +++ b/Parallelism/PlayerProcess.py @@ -0,0 +1,380 @@ +import asyncio +from os import listdir +from discord import Intents, User, Member, Message, Embed +from asyncio import AbstractEventLoop, Semaphore +from multiprocessing import Process, Queue, RLock +from threading import Lock, Thread +from 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 + + +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 from the main process by a Queue""" + + def __init__(self, name: str, playlist: Playlist, lock: Lock, queue: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None: + """ + 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, name=name, group=None, target=None, args=(), kwargs={}) + # Synchronization objects + self.__playlist: Playlist = playlist + self.__playlistLock: 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.__author: User = None + self.__botMember: Member = None + + 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', + 'options': '-vn'} + + 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.__playerLock = RLock() + self.__loop = asyncio.get_event_loop() + + self.__configs = Configs() + self.__messages = Messages() + self.__embeds = Embeds() + + self.__semStopPlaying = Semaphore(0) + 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 + 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) + self.__botMember = self.__getBotMember() + # Connect to voice Channel + await self.__connectToVoiceChannel() + + # Start the timeout function + self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) + # Thread that will receive commands to be executed 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() + # In this point the process should finalize + self.__timer.cancel() + + async def __playPlaylistSongs(self) -> None: + if not self.__playing: + with self.__playlistLock: + song = self.__playlist.next_song() + + if song is not None: + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + + 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 + + if song.source is None: + return self.__playNext(None) + + # If not connected, connect to bind channel + if self.__guild.voice_client is None: + 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)) + + 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: + if self.__forceStop: # If it's forced to stop player + self.__forceStop = False + return None + + with self.__playlistLock: + song = self.__playlist.next_song() + + if song is not None: + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + else: + with self.__playlistLock: + self.__playlist.loop_off() + self.__playingSong = None + self.__playing = False + + async def __playPrev(self, voiceChannelID: int) -> None: + with self.__playlistLock: + song = self.__playlist.prev_song() + + if song is not None: + if self.__guild.voice_client is None: # If not connect, connect to the user voice channel + self.__voiceChannelID = voiceChannelID + self.__voiceChannel = self.__guild.get_channel(self.__voiceChannelID) + await self.__connectToVoiceChannel() + + # If already playing, stop the current play + if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): + # Will forbidden next_song to execute after stopping current player + self.__forceStop = True + self.__guild.voice_client.stop() + self.__playing = False + + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + + def __commandsReceiver(self) -> None: + while True: + command: VCommands = self.__queue.get() + type = command.getType() + args = command.getArgs() + + 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: + 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(): + with self.__playlistLock: + self.__playlist.clear() + self.__playlist.loop_off() + + self.__guild.voice_client.stop() + self.__playingSong = None + await self.__guild.voice_client.disconnect() + self.__semStopPlaying.release() + + def __resume(self) -> None: + # 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: + # Lock to work with Player + with self.__playerLock: + if self.__guild.voice_client is not None and self.__playing: + self.__playing = False + self.__guild.voice_client.stop() + + async def __forceStop(self) -> None: + # 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() + + async def __createBotInstance(self) -> Client: + """Load a new bot instance that should not be directly called. + Get the guild, voice and text Channel in discord API using IDs passed in constructor + """ + intents = Intents.default() + intents.members = True + bot = Bot(command_prefix='Rafael', + pm_help=True, + case_insensitive=True, + intents=intents) + bot.remove_command('help') + + # Add the Cogs for this bot too + for filename in listdir(f'./{self.__configs.COMMANDS_PATH}'): + if filename.endswith('.py'): + bot.load_extension(f'{self.__configs.COMMANDS_PATH}.{filename[:-3]}') + + # Login and connect the bot instance to discord API + task = self.__loop.create_task(bot.login(token=self.__configs.BOT_TOKEN, bot=True)) + await task + self.__loop.create_task(bot.connect(reconnect=True)) + # Sleep to wait connection to be established + await self.__ensureDiscordConnection(bot) + + return bot + + async def __timeoutHandler(self) -> None: + try: + 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, self.__loop) + + elif self.__guild.voice_client.is_connected(): + with self.__playerLock: + with self.__playlistLock: + self.__playlist.clear() + self.__playlist.loop_off() + self.__playing = False + await self.__guild.voice_client.disconnect() + # Release semaphore to finish process + self.__semStopPlaying.release() + except Exception as e: + print(f'[Error in Timeout] -> {e}') + + async def __ensureDiscordConnection(self, bot: Client) -> None: + """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 Exception as e: + print(f'[ERROR CONNECTING TO VC] -> {e}') + 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 + + async def __showNowPlaying(self) -> None: + # Get the lock of the playlist + with self.__playlistLock: + if not self.__playing or self.__playingSong is None: + embed = self.__embeds.NOT_PLAYING() + await self.__textChannel.send(embed=embed) + return + + if self.__playlist.isLoopingOne(): + title = self.__messages.ONE_SONG_LOOPING + else: + title = self.__messages.SONG_PLAYING + + info = self.__playingSong.info + embed = self.__embeds.SONG_INFO(info, title) + await self.__textChannel.send(embed=embed) + self.__messagesToDelete.append(await self.__getSendedMessage()) + + async def __deletePrevNowPlaying(self) -> None: + for message in self.__messagesToDelete: + try: + await message.delete() + except: + pass + self.__messagesToDelete.clear() + + async def __getSendedMessage(self) -> Message: + stringToIdentify = 'Uploader:' + last_messages: List[Message] = await self.__textChannel.history(limit=5).flatten() + + for message in last_messages: + try: + if message.author == self.__bot.user: + if len(message.embeds) > 0: + embed: Embed = message.embeds[0] + if len(embed.fields) > 0: + if embed.fields[0].name == stringToIdentify: + return message + + except Exception as e: + print(f'DEVELOPER NOTE -> Error cleaning messages {e}') + continue diff --git a/Parallelism/ProcessInfo.py b/Parallelism/ProcessInfo.py new file mode 100644 index 0000000..0f8bf83 --- /dev/null +++ b/Parallelism/ProcessInfo.py @@ -0,0 +1,29 @@ +from multiprocessing import Process, Queue, Lock +from Music.Playlist import Playlist + + +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 + self.__playlist = playlist + self.__lock = lock + + def setProcess(self, newProcess: Process) -> None: + self.__process = newProcess + + 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..6df1b73 --- /dev/null +++ b/Parallelism/ProcessManager.py @@ -0,0 +1,101 @@ +from multiprocessing import Queue, Lock +from multiprocessing.managers import BaseManager, NamespaceProxy +from typing import Dict +from Config.Singleton import Singleton +from discord import Guild +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): + """ + Manage all running player process, creating and storing them for future calls + Deal with the creation of shared memory + """ + + 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.id] = context + + def getPlayerInfo(self, guild: Guild, context: Context) -> ProcessInfo: + """Return the process info for the guild, if not, create one""" + try: + if guild.id not in self.__playersProcess.keys(): + self.__playersProcess[guild.id] = self.__createProcessInfo(context) + else: + # If the process has ended create a new one + if not self.__playersProcess[guild.id].getProcess().is_alive(): + self.__playersProcess[guild.id] = self.__recreateProcess(context) + + return self.__playersProcess[guild.id] + 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(): + return None + + return self.__playersProcess[guild.id] + + def __createProcessInfo(self, context: Context) -> ProcessInfo: + 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(context.guild.name, playlist, lock, queue, + guildID, textID, voiceID, authorID) + processInfo = ProcessInfo(process, queue, playlist, lock) + + 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 + + +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/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): 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..ef60595 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 @@ -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 ) @@ -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, 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: 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()