From 4a22b43ce992d5c79f72458bac5a56ff26b2b591 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Mon, 25 Jul 2022 21:59:09 -0300 Subject: [PATCH 1/9] Setting new event loop --- Parallelism/PlayerProcess.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 3fc49ee..9e42284 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -70,7 +70,8 @@ class PlayerProcess(Process): try: print(f'Starting Process {self.name}') self.__playerLock = RLock() - self.__loop = asyncio.get_event_loop() + self.__loop = asyncio.get_event_loop_policy().new_event_loop() + asyncio.set_event_loop(self.__loop) self.__configs = Configs() self.__messages = Messages() From fededdbb8c5ad82e5f4d1b77b09f3103b68a5709 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Wed, 27 Jul 2022 01:36:55 -0300 Subject: [PATCH 2/9] Changing to pycord --- DiscordCogs/ControlCog.py | 37 +++++------------ DiscordCogs/MusicCog.py | 2 + DiscordCogs/RandomCog.py | 7 +++- Handlers/AbstractHandler.py | 5 ++- Handlers/ClearHandler.py | 4 +- Handlers/HistoryHandler.py | 4 +- Handlers/LoopHandler.py | 4 +- Handlers/MoveHandler.py | 4 +- Handlers/NowPlayingHandler.py | 4 +- Handlers/PauseHandler.py | 4 +- Handlers/PlayHandler.py | 4 +- Handlers/PrevHandler.py | 4 +- Handlers/QueueHandler.py | 4 +- Handlers/RemoveHandler.py | 4 +- Handlers/ResetHandler.py | 4 +- Handlers/ResumeHandler.py | 4 +- Handlers/ShuffleHandler.py | 4 +- Handlers/SkipHandler.py | 4 +- Handlers/StopHandler.py | 4 +- Music/MusicBot.py | 73 ++++++++++++++++++++++++++++++++++ Music/VulkanInitializer.py | 43 ++++++++++++++++++++ Parallelism/PlayerProcess.py | 37 +++++------------ Utils/Cleaner.py | 7 ++-- Views/AbstractView.py | 7 ++-- Views/Embeds.py | 2 +- main.py | 55 +++++++++++++++---------- requirements.txt | Bin 288 -> 244 bytes 27 files changed, 217 insertions(+), 118 deletions(-) create mode 100644 Music/MusicBot.py create mode 100644 Music/VulkanInitializer.py diff --git a/DiscordCogs/ControlCog.py b/DiscordCogs/ControlCog.py index e0e62da..ff6ef56 100644 --- a/DiscordCogs/ControlCog.py +++ b/DiscordCogs/ControlCog.py @@ -1,22 +1,24 @@ -from discord import Client, Game, Status, Embed -from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument +from discord import Embed from discord.ext import commands +from discord.ext.commands import Cog from Config.Configs import Configs from Config.Helper import Helper -from Config.Messages import Messages from Config.Colors import Colors +from Music.MusicBot import VulkanBot from Views.Embeds import Embeds helper = Helper() -class ControlCog(commands.Cog): +class ControlCog(Cog): """Class to handle discord events""" - def __init__(self, bot: Client): + def __init__(self, bot: VulkanBot): + print('Eae3') self.__bot = bot + print(self.__bot) + print(bot.extensions) self.__config = Configs() - self.__messages = Messages() self.__colors = Colors() self.__embeds = Embeds() self.__commands = { @@ -28,27 +30,6 @@ class ControlCog(commands.Cog): } - @commands.Cog.listener() - async def on_ready(self): - print(self.__messages.STARTUP_MESSAGE) - await self.__bot.change_presence(status=Status.online, activity=Game(name=f"Vulkan | {self.__config.BOT_PREFIX}help")) - print(self.__messages.STARTUP_COMPLETE_MESSAGE) - - @commands.Cog.listener() - async def on_command_error(self, ctx, error): - if isinstance(error, MissingRequiredArgument): - embed = self.__embeds.MISSING_ARGUMENTS() - await ctx.send(embed=embed) - - elif isinstance(error, CommandNotFound): - embed = self.__embeds.COMMAND_NOT_FOUND() - await ctx.send(embed=embed) - - else: - print(f'DEVELOPER NOTE -> Command Error: {error}') - embed = self.__embeds.UNKNOWN_ERROR() - await ctx.send(embed=embed) - @commands.command(name="help", help=helper.HELP_HELP, description=helper.HELP_HELP_LONG, aliases=['h', 'ajuda']) async def help_msg(self, ctx, command_help=''): if command_help != '': @@ -97,7 +78,7 @@ class ControlCog(commands.Cog): colour=self.__colors.BLUE ) - embedhelp.set_thumbnail(url=self.__bot.user.avatar_url) + embedhelp.set_thumbnail(url=self.__bot.user.avatar) await ctx.send(embed=embedhelp) @commands.command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG, aliases=['convite', 'inv', 'convidar']) diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 4f3b137..8c50f33 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -232,4 +232,6 @@ class MusicCog(commands.Cog): def setup(bot): + print('Loading Music') bot.add_cog(MusicCog(bot)) + print('Voltou') diff --git a/DiscordCogs/RandomCog.py b/DiscordCogs/RandomCog.py index b2ec3fc..02c00f3 100644 --- a/DiscordCogs/RandomCog.py +++ b/DiscordCogs/RandomCog.py @@ -1,5 +1,5 @@ from random import randint, random -from discord import Client +from Music.MusicBot import VulkanBot from discord.ext.commands import Context, command, Cog from Config.Helper import Helper from Views.Embeds import Embeds @@ -10,7 +10,10 @@ helper = Helper() class RandomCog(Cog): """Class to listen to commands of type Random""" - def __init__(self, bot: Client): + def __init__(self, bot: VulkanBot): + print('Eae2') + print(bot) + print(bot.extensions) self.__embeds = Embeds() @command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG, aliases=['rand']) diff --git a/Handlers/AbstractHandler.py b/Handlers/AbstractHandler.py index 93e7e58..19f81a6 100644 --- a/Handlers/AbstractHandler.py +++ b/Handlers/AbstractHandler.py @@ -3,6 +3,7 @@ from typing import List from discord.ext.commands import Context from discord import Client, Guild, ClientUser, Member from Config.Messages import Messages +from Music.MusicBot import VulkanBot from Handlers.HandlerResponse import HandlerResponse from Config.Configs import Configs from Config.Helper import Helper @@ -10,8 +11,8 @@ from Views.Embeds import Embeds class AbstractHandler(ABC): - def __init__(self, ctx: Context, bot: Client) -> None: - self.__bot: Client = bot + def __init__(self, ctx: Context, bot: VulkanBot) -> None: + self.__bot: VulkanBot = bot self.__guild: Guild = ctx.guild self.__ctx: Context = ctx self.__bot_user: ClientUser = self.__bot.user diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index df2d822..eb53a8a 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -1,12 +1,12 @@ from discord.ext.commands import Context -from discord import Client +from discord import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager class ClearHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py index b33c263..7869b6f 100644 --- a/Handlers/HistoryHandler.py +++ b/Handlers/HistoryHandler.py @@ -1,5 +1,5 @@ from discord.ext.commands import Context -from discord import Client +from Music.MusicBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Utils.Utils import Utils @@ -7,7 +7,7 @@ from Parallelism.ProcessManager import ProcessManager class HistoryHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index b9b0bf4..c9e429f 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -1,5 +1,5 @@ from discord.ext.commands import Context -from discord import Client +from Music.MusicBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage @@ -7,7 +7,7 @@ from Parallelism.ProcessManager import ProcessManager class LoopHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self, args: str) -> HandlerResponse: diff --git a/Handlers/MoveHandler.py b/Handlers/MoveHandler.py index 8ce3ce9..be0849c 100644 --- a/Handlers/MoveHandler.py +++ b/Handlers/MoveHandler.py @@ -1,6 +1,6 @@ from typing import Union from discord.ext.commands import Context -from discord import Client +from Music.MusicBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError @@ -9,7 +9,7 @@ from Parallelism.ProcessManager import ProcessManager class MoveHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self, pos1: str, pos2: str) -> HandlerResponse: diff --git a/Handlers/NowPlayingHandler.py b/Handlers/NowPlayingHandler.py index 93095c9..500ba4d 100644 --- a/Handlers/NowPlayingHandler.py +++ b/Handlers/NowPlayingHandler.py @@ -1,13 +1,13 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse +from Music.MusicBot import VulkanBot from Utils.Cleaner import Cleaner from Parallelism.ProcessManager import ProcessManager class NowPlayingHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) self.__cleaner = Cleaner() diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py index 20afe7b..61a0c9d 100644 --- a/Handlers/PauseHandler.py +++ b/Handlers/PauseHandler.py @@ -1,13 +1,13 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from Music.MusicBot import VulkanBot class PauseHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index b9636c1..d1c4945 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -2,7 +2,6 @@ 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 @@ -12,10 +11,11 @@ from Music.Song import Song from Parallelism.ProcessManager import ProcessManager from Parallelism.ProcessInfo import ProcessInfo from Parallelism.Commands import VCommands, VCommandsType +from Music.MusicBot import VulkanBot class PlayHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) self.__searcher = Searcher() self.__down = Downloader() diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index a1336ae..4ef209e 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -1,14 +1,14 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Config.Exceptions import BadCommandUsage, ImpossibleMove from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from Music.MusicBot import VulkanBot class PrevHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index 953287a..3f691bb 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -1,14 +1,14 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Music.Downloader import Downloader from Utils.Utils import Utils from Parallelism.ProcessManager import ProcessManager +from Music.MusicBot import VulkanBot class QueueHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) self.__down = Downloader() diff --git a/Handlers/RemoveHandler.py b/Handlers/RemoveHandler.py index c611f90..c3a8511 100644 --- a/Handlers/RemoveHandler.py +++ b/Handlers/RemoveHandler.py @@ -1,15 +1,15 @@ from typing import Union from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired from Music.Playlist import Playlist from Parallelism.ProcessManager import ProcessManager +from Music.MusicBot import VulkanBot class RemoveHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self, position: str) -> HandlerResponse: diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py index c938d21..762b849 100644 --- a/Handlers/ResetHandler.py +++ b/Handlers/ResetHandler.py @@ -1,13 +1,13 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from Music.MusicBot import VulkanBot class ResetHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/ResumeHandler.py b/Handlers/ResumeHandler.py index bd5c254..abda613 100644 --- a/Handlers/ResumeHandler.py +++ b/Handlers/ResumeHandler.py @@ -1,13 +1,13 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from Music.MusicBot import VulkanBot class ResumeHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py index 94999c8..392923a 100644 --- a/Handlers/ShuffleHandler.py +++ b/Handlers/ShuffleHandler.py @@ -1,13 +1,13 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import UnknownError from Parallelism.ProcessManager import ProcessManager +from Music.MusicBot import VulkanBot class ShuffleHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index ab0355d..7603ee3 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -1,14 +1,14 @@ 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 Music.MusicBot import VulkanBot from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType class SkipHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/StopHandler.py b/Handlers/StopHandler.py index 669a953..0a59cae 100644 --- a/Handlers/StopHandler.py +++ b/Handlers/StopHandler.py @@ -1,13 +1,13 @@ from discord.ext.commands import Context -from discord import Client from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse +from Music.MusicBot import VulkanBot from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType class StopHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: Client) -> None: + def __init__(self, ctx: Context, bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Music/MusicBot.py b/Music/MusicBot.py new file mode 100644 index 0000000..9093883 --- /dev/null +++ b/Music/MusicBot.py @@ -0,0 +1,73 @@ +from asyncio import AbstractEventLoop +from discord import Guild, Status, Game, Message +from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument +from Config.Configs import Configs +from discord.ext import commands +from Config.Messages import Messages +from Views.Embeds import Embeds + + +class VulkanBot(commands.Bot): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__configs = Configs() + self.__messages = Messages() + self.__embeds = Embeds() + self.remove_command("help") + + def startBot(self) -> None: + """Blocking function that will start the bot""" + if self.__configs.BOT_TOKEN == '': + print('DEVELOPER NOTE -> Token not found') + exit() + + super().run(self.__configs.BOT_TOKEN, reconnect=True) + + async def startBotCoro(self, loop: AbstractEventLoop) -> None: + """Start a bot coroutine, does not wait for connection to be established""" + task = loop.create_task(self.__login()) + await task + loop.create_task(self.__connect()) + + async def __login(self): + """Coroutine to login the Bot in discord""" + await self.login(token=self.__configs.BOT_TOKEN) + + async def __connect(self): + """Coroutine to connect the Bot in discord""" + await self.connect(reconnect=True) + + async def on_ready(self): + print(self.__messages.STARTUP_MESSAGE) + await self.change_presence(status=Status.online, activity=Game(name=f"Vulkan | {self.__configs.BOT_PREFIX}help")) + print(self.__messages.STARTUP_COMPLETE_MESSAGE) + + async def on_command_error(self, ctx, error): + if isinstance(error, MissingRequiredArgument): + embed = self.__embeds.MISSING_ARGUMENTS() + await ctx.send(embed=embed) + + elif isinstance(error, CommandNotFound): + embed = self.__embeds.COMMAND_NOT_FOUND() + await ctx.send(embed=embed) + + else: + print(f'DEVELOPER NOTE -> Command Error: {error}') + embed = self.__embeds.UNKNOWN_ERROR() + await ctx.send(embed=embed) + + async def process_commands(self, message: Message): + if message.author.bot: + return + + ctx = await self.get_context(message, cls=Context) + + if ctx.valid and not message.guild: + return + + await self.invoke(ctx) + + +class Context(commands.Context): + bot: VulkanBot + guild: Guild diff --git a/Music/VulkanInitializer.py b/Music/VulkanInitializer.py new file mode 100644 index 0000000..dcf1776 --- /dev/null +++ b/Music/VulkanInitializer.py @@ -0,0 +1,43 @@ +from random import choices +import string +from discord.bot import Bot +from discord import Intents +from Music.MusicBot import VulkanBot +from os import listdir +from Config.Configs import Configs + + +class VulkanInitializer: + def __init__(self, willListen: bool) -> None: + self.__config = Configs() + self.__intents = Intents.default() + self.__intents.message_content = True + self.__intents.members = True + self.__bot = self.__create_bot(willListen) + self.__add_cogs(self.__bot) + + def getBot(self) -> VulkanBot: + return self.__bot + + def __create_bot(self, willListen: bool) -> VulkanBot: + if willListen: + prefix = self.__config.BOT_PREFIX + else: + prefix = ''.join(choices(string.ascii_uppercase + string.digits, k=4)) + + bot = VulkanBot(command_prefix=prefix, + pm_help=True, + case_insensitive=True, + intents=self.__intents) + return bot + + def __add_cogs(self, bot: Bot) -> None: + try: + for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): + if filename.endswith('.py'): + print(f'Loading {filename}') + bot.load_extension(f'{self.__config.COMMANDS_PATH}.{filename[:-3]}') + + bot.load_extension(f'DiscordCogs.MusicCog') + except Exception as e: + print(e) diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 9e42284..ab2bd8d 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -1,6 +1,6 @@ import asyncio -from os import listdir -from discord import Intents, User, Member, Message, Embed +from Music.VulkanInitializer import VulkanInitializer +from discord import User, Member, Message, Embed from asyncio import AbstractEventLoop, Semaphore from multiprocessing import Process, Queue, RLock from threading import Lock, Thread @@ -10,7 +10,7 @@ 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 Music.MusicBot import VulkanBot from Views.Embeds import Embeds from Parallelism.Commands import VCommands, VCommandsType @@ -50,7 +50,7 @@ class PlayerProcess(Process): self.__authorID = authorID # All information of discord context will be retrieved directly with discord API self.__guild: Guild = None - self.__bot: Client = None + self.__bot: VulkanBot = None self.__voiceChannel: VoiceChannel = None self.__textChannel: TextChannel = None self.__author: User = None @@ -270,30 +270,13 @@ class PlayerProcess(Process): self.__playlist.clear() self.__playlist.loop_off() - async def __createBotInstance(self) -> Client: - """Load a new bot instance that should not be directly called. - Get the guild, voice and text Channel in discord API using IDs passed in constructor - """ - intents = Intents.default() - intents.members = True - bot = Bot(command_prefix='Rafael', - pm_help=True, - case_insensitive=True, - intents=intents) - bot.remove_command('help') + async def __createBotInstance(self) -> VulkanBot: + """Load a new bot instance that should not be directly called.""" + initializer = VulkanInitializer(willListen=False) + bot = initializer.getBot() - # Add the Cogs for this bot too - for filename in listdir(f'./{self.__configs.COMMANDS_PATH}'): - if filename.endswith('.py'): - bot.load_extension(f'{self.__configs.COMMANDS_PATH}.{filename[:-3]}') - - # Login and connect the bot instance to discord API - task = self.__loop.create_task(bot.login(token=self.__configs.BOT_TOKEN, bot=True)) - await task - self.__loop.create_task(bot.connect(reconnect=True)) - # Sleep to wait connection to be established + await bot.startBotCoro(self.__loop) await self.__ensureDiscordConnection(bot) - return bot async def __timeoutHandler(self) -> None: @@ -316,7 +299,7 @@ class PlayerProcess(Process): except Exception as e: print(f'[Error in Timeout] -> {e}') - async def __ensureDiscordConnection(self, bot: Client) -> None: + async def __ensureDiscordConnection(self, bot: VulkanBot) -> None: """Await in this point until connection to discord is established""" guild = None while guild is None: diff --git a/Utils/Cleaner.py b/Utils/Cleaner.py index f944a84..d66a0f2 100644 --- a/Utils/Cleaner.py +++ b/Utils/Cleaner.py @@ -1,16 +1,17 @@ from typing import List from discord.ext.commands import Context -from discord import Client, Message, Embed +from discord import Message, Embed from Config.Singleton import Singleton +from Music.MusicBot import VulkanBot class Cleaner(Singleton): - def __init__(self, bot: Client = None) -> None: + def __init__(self, bot: VulkanBot = None) -> None: if not super().created: self.__bot = bot self.__clean_str = 'Uploader:' - def set_bot(self, bot: Client) -> None: + def set_bot(self, bot: VulkanBot) -> None: self.__bot = bot async def clean_messages(self, ctx: Context, quant: int) -> None: diff --git a/Views/AbstractView.py b/Views/AbstractView.py index 87b6d19..a8f848b 100644 --- a/Views/AbstractView.py +++ b/Views/AbstractView.py @@ -1,7 +1,8 @@ from abc import ABC, abstractmethod from Handlers.HandlerResponse import HandlerResponse from discord.ext.commands import Context -from discord import Client, Message +from discord import Message +from Music.MusicBot import VulkanBot class AbstractView(ABC): @@ -9,14 +10,14 @@ class AbstractView(ABC): self.__response: HandlerResponse = response self.__context: Context = response.ctx self.__message: Message = response.ctx.message - self.__bot: Client = response.ctx.bot + self.__bot: VulkanBot = response.ctx.bot @property def response(self) -> HandlerResponse: return self.__response @property - def bot(self) -> Client: + def bot(self) -> VulkanBot: return self.__bot @property diff --git a/Views/Embeds.py b/Views/Embeds.py index ef60595..894bf52 100644 --- a/Views/Embeds.py +++ b/Views/Embeds.py @@ -334,7 +334,7 @@ class Embeds: def CARA_COROA(self, result: str) -> Embed: embed = Embed( - title='Cara Cora', + title='Cara Coroa', description=f'Result: {result}', colour=self.__colors.GREEN ) diff --git a/main.py b/main.py index e4b6269..c800823 100644 --- a/main.py +++ b/main.py @@ -1,38 +1,49 @@ -from discord import Intents, Client +from random import choices +import string +from discord.bot import Bot +from discord import Intents +from Music.MusicBot import VulkanBot from os import listdir from Config.Configs import Configs -from discord.ext.commands import Bot class VulkanInitializer: - def __init__(self) -> None: + def __init__(self, willListen: bool) -> None: self.__config = Configs() self.__intents = Intents.default() + self.__intents.message_content = True self.__intents.members = True - self.__bot = self.__create_bot() + self.__bot = self.__create_bot(willListen) self.__add_cogs(self.__bot) - def __create_bot(self) -> Client: - bot = Bot(command_prefix=self.__config.BOT_PREFIX, - pm_help=True, - case_insensitive=True, - intents=self.__intents) - bot.remove_command('help') + def getBot(self) -> VulkanBot: + return self.__bot + + def __create_bot(self, willListen: bool) -> VulkanBot: + if willListen: + prefix = self.__config.BOT_PREFIX + else: + prefix = ''.join(choices(string.ascii_uppercase + string.digits, k=4)) + + bot = VulkanBot(command_prefix=prefix, + pm_help=True, + case_insensitive=True, + intents=self.__intents) return bot - def __add_cogs(self, bot: Client) -> None: - for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): - if filename.endswith('.py'): - bot.load_extension(f'{self.__config.COMMANDS_PATH}.{filename[:-3]}') + def __add_cogs(self, bot: Bot) -> None: + try: + for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): + if filename.endswith('.py'): + print(f'Loading {filename}') + bot.load_extension(f'{self.__config.COMMANDS_PATH}.{filename[:-3]}') - def run(self) -> None: - if self.__config.BOT_TOKEN == '': - print('DEVELOPER NOTE -> Token not found') - exit() - - self.__bot.run(self.__config.BOT_TOKEN, bot=True, reconnect=True) + bot.load_extension(f'DiscordCogs.MusicCog') + except Exception as e: + print(e) if __name__ == '__main__': - vulkan = VulkanInitializer() - vulkan.run() + initializer = VulkanInitializer(willListen=True) + vulkanBot = initializer.getBot() + vulkanBot.startBot() diff --git a/requirements.txt b/requirements.txt index 65cdaf22c17f9ce4d38bf9e470e8f215c8755c7d..b6d030b3e99f8a0229bc56ce6076336b2353ede2 100644 GIT binary patch delta 23 ecmZ3$^o5cC|Gxr;N(NnqWQKf(B8HTS)=vRs{s Date: Wed, 27 Jul 2022 11:27:38 -0300 Subject: [PATCH 3/9] Fixing errors due to new discor d library --- DiscordCogs/ControlCog.py | 12 +++----- DiscordCogs/MusicCog.py | 37 +++++++++++------------- DiscordCogs/RandomCog.py | 5 +--- Handlers/AbstractHandler.py | 2 +- Handlers/ClearHandler.py | 2 +- Handlers/HistoryHandler.py | 2 +- Handlers/LoopHandler.py | 2 +- Handlers/MoveHandler.py | 2 +- Handlers/NowPlayingHandler.py | 2 +- Handlers/PauseHandler.py | 2 +- Handlers/PlayHandler.py | 2 +- Handlers/PrevHandler.py | 2 +- Handlers/QueueHandler.py | 2 +- Handlers/RemoveHandler.py | 2 +- Handlers/ResetHandler.py | 2 +- Handlers/ResumeHandler.py | 2 +- Handlers/ShuffleHandler.py | 2 +- Handlers/SkipHandler.py | 2 +- Handlers/StopHandler.py | 2 +- Music/{MusicBot.py => VulkanBot.py} | 6 ++-- Music/VulkanInitializer.py | 22 +++++++++++---- Parallelism/PlayerProcess.py | 2 +- Utils/Cleaner.py | 2 +- Views/AbstractView.py | 2 +- main.py | 44 +---------------------------- 25 files changed, 61 insertions(+), 103 deletions(-) rename Music/{MusicBot.py => VulkanBot.py} (96%) diff --git a/DiscordCogs/ControlCog.py b/DiscordCogs/ControlCog.py index ff6ef56..5b3ae94 100644 --- a/DiscordCogs/ControlCog.py +++ b/DiscordCogs/ControlCog.py @@ -1,10 +1,9 @@ from discord import Embed -from discord.ext import commands -from discord.ext.commands import Cog +from discord.ext.commands import Cog, command from Config.Configs import Configs from Config.Helper import Helper from Config.Colors import Colors -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from Views.Embeds import Embeds helper = Helper() @@ -14,10 +13,7 @@ class ControlCog(Cog): """Class to handle discord events""" def __init__(self, bot: VulkanBot): - print('Eae3') self.__bot = bot - print(self.__bot) - print(bot.extensions) self.__config = Configs() self.__colors = Colors() self.__embeds = Embeds() @@ -30,7 +26,7 @@ class ControlCog(Cog): } - @commands.command(name="help", help=helper.HELP_HELP, description=helper.HELP_HELP_LONG, aliases=['h', 'ajuda']) + @command(name="help", help=helper.HELP_HELP, description=helper.HELP_HELP_LONG, aliases=['h', 'ajuda']) async def help_msg(self, ctx, command_help=''): if command_help != '': for command in self.__bot.commands: @@ -81,7 +77,7 @@ class ControlCog(Cog): embedhelp.set_thumbnail(url=self.__bot.user.avatar) await ctx.send(embed=embedhelp) - @commands.command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG, aliases=['convite', 'inv', 'convidar']) + @command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG, aliases=['convite', 'inv', 'convidar']) async def invite_bot(self, ctx): invite_url = self.__config.INVITE_URL.format(self.__bot.user.id) txt = self.__config.INVITE_MESSAGE.format(invite_url, invite_url) diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 8c50f33..b54fce8 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -1,6 +1,5 @@ from discord import Guild, Client -from discord.ext import commands -from discord.ext.commands import Context +from discord.ext.commands import Context, command, Cog from Config.Helper import Helper from Handlers.ClearHandler import ClearHandler from Handlers.MoveHandler import MoveHandler @@ -23,7 +22,7 @@ from Views.EmbedView import EmbedView helper = Helper() -class MusicCog(commands.Cog): +class MusicCog(Cog): """ Class to listen to Music commands It'll listen for commands from discord, when triggered will create a specific Handler for the command @@ -33,7 +32,7 @@ class MusicCog(commands.Cog): 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']) + @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) @@ -47,7 +46,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas']) + @command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas']) async def queue(self, ctx: Context) -> None: try: controller = QueueHandler(ctx, self.__bot) @@ -58,7 +57,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name="skip", help=helper.HELP_SKIP, description=helper.HELP_SKIP_LONG, aliases=['s', 'pular', 'next']) + @command(name="skip", help=helper.HELP_SKIP, description=helper.HELP_SKIP_LONG, aliases=['s', 'pular', 'next']) async def skip(self, ctx: Context) -> None: try: controller = SkipHandler(ctx, self.__bot) @@ -73,7 +72,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='stop', help=helper.HELP_STOP, description=helper.HELP_STOP_LONG, aliases=['parar']) + @command(name='stop', help=helper.HELP_STOP, description=helper.HELP_STOP_LONG, aliases=['parar']) async def stop(self, ctx: Context) -> None: try: controller = StopHandler(ctx, self.__bot) @@ -88,7 +87,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar', 'pare']) + @command(name='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar', 'pare']) async def pause(self, ctx: Context) -> None: try: controller = PauseHandler(ctx, self.__bot) @@ -101,7 +100,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='resume', help=helper.HELP_RESUME, description=helper.HELP_RESUME_LONG, aliases=['soltar', 'despausar']) + @command(name='resume', help=helper.HELP_RESUME, description=helper.HELP_RESUME_LONG, aliases=['soltar', 'despausar']) async def resume(self, ctx: Context) -> None: try: controller = ResumeHandler(ctx, self.__bot) @@ -114,7 +113,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='prev', help=helper.HELP_PREV, description=helper.HELP_PREV_LONG, aliases=['anterior', 'return', 'previous', 'back']) + @command(name='prev', help=helper.HELP_PREV, description=helper.HELP_PREV_LONG, aliases=['anterior', 'return', 'previous', 'back']) async def prev(self, ctx: Context) -> None: try: controller = PrevHandler(ctx, self.__bot) @@ -128,7 +127,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico', 'anteriores', 'hist']) + @command(name='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico', 'anteriores', 'hist']) async def history(self, ctx: Context) -> None: try: controller = HistoryHandler(ctx, self.__bot) @@ -141,7 +140,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='loop', help=helper.HELP_LOOP, description=helper.HELP_LOOP_LONG, aliases=['l', 'repeat']) + @command(name='loop', help=helper.HELP_LOOP, description=helper.HELP_LOOP_LONG, aliases=['l', 'repeat']) async def loop(self, ctx: Context, args='') -> None: try: controller = LoopHandler(ctx, self.__bot) @@ -154,7 +153,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='clear', help=helper.HELP_CLEAR, description=helper.HELP_CLEAR_LONG, aliases=['c', 'limpar']) + @command(name='clear', help=helper.HELP_CLEAR, description=helper.HELP_CLEAR_LONG, aliases=['c', 'limpar']) async def clear(self, ctx: Context) -> None: try: controller = ClearHandler(ctx, self.__bot) @@ -165,7 +164,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='np', help=helper.HELP_NP, description=helper.HELP_NP_LONG, aliases=['playing', 'now', 'this']) + @command(name='np', help=helper.HELP_NP, description=helper.HELP_NP_LONG, aliases=['playing', 'now', 'this']) async def now_playing(self, ctx: Context) -> None: try: controller = NowPlayingHandler(ctx, self.__bot) @@ -178,7 +177,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio', 'misturar']) + @command(name='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio', 'misturar']) async def shuffle(self, ctx: Context) -> None: try: controller = ShuffleHandler(ctx, self.__bot) @@ -191,7 +190,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='move', help=helper.HELP_MOVE, description=helper.HELP_MOVE_LONG, aliases=['m', 'mover']) + @command(name='move', help=helper.HELP_MOVE, description=helper.HELP_MOVE_LONG, aliases=['m', 'mover']) async def move(self, ctx: Context, pos1, pos2='1') -> None: try: controller = MoveHandler(ctx, self.__bot) @@ -204,7 +203,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='remove', help=helper.HELP_REMOVE, description=helper.HELP_REMOVE_LONG, aliases=['remover']) + @command(name='remove', help=helper.HELP_REMOVE, description=helper.HELP_REMOVE_LONG, aliases=['remover']) async def remove(self, ctx: Context, position) -> None: try: controller = RemoveHandler(ctx, self.__bot) @@ -217,7 +216,7 @@ class MusicCog(commands.Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') - @commands.command(name='reset', help=helper.HELP_RESET, description=helper.HELP_RESET_LONG, aliases=['resetar']) + @command(name='reset', help=helper.HELP_RESET, description=helper.HELP_RESET_LONG, aliases=['resetar']) async def reset(self, ctx: Context) -> None: try: controller = ResetHandler(ctx, self.__bot) @@ -232,6 +231,4 @@ class MusicCog(commands.Cog): def setup(bot): - print('Loading Music') bot.add_cog(MusicCog(bot)) - print('Voltou') diff --git a/DiscordCogs/RandomCog.py b/DiscordCogs/RandomCog.py index 02c00f3..6ec53c7 100644 --- a/DiscordCogs/RandomCog.py +++ b/DiscordCogs/RandomCog.py @@ -1,5 +1,5 @@ from random import randint, random -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from discord.ext.commands import Context, command, Cog from Config.Helper import Helper from Views.Embeds import Embeds @@ -11,9 +11,6 @@ class RandomCog(Cog): """Class to listen to commands of type Random""" def __init__(self, bot: VulkanBot): - print('Eae2') - print(bot) - print(bot.extensions) self.__embeds = Embeds() @command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG, aliases=['rand']) diff --git a/Handlers/AbstractHandler.py b/Handlers/AbstractHandler.py index 19f81a6..de9d592 100644 --- a/Handlers/AbstractHandler.py +++ b/Handlers/AbstractHandler.py @@ -3,7 +3,7 @@ from typing import List from discord.ext.commands import Context from discord import Client, Guild, ClientUser, Member from Config.Messages import Messages -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from Handlers.HandlerResponse import HandlerResponse from Config.Configs import Configs from Config.Helper import Helper diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index eb53a8a..85dd75b 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -1,5 +1,5 @@ from discord.ext.commands import Context -from discord import VulkanBot +from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager diff --git a/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py index 7869b6f..e4d0d07 100644 --- a/Handlers/HistoryHandler.py +++ b/Handlers/HistoryHandler.py @@ -1,5 +1,5 @@ from discord.ext.commands import Context -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Utils.Utils import Utils diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index c9e429f..a62e5cb 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -1,5 +1,5 @@ from discord.ext.commands import Context -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage diff --git a/Handlers/MoveHandler.py b/Handlers/MoveHandler.py index be0849c..8088f66 100644 --- a/Handlers/MoveHandler.py +++ b/Handlers/MoveHandler.py @@ -1,6 +1,6 @@ from typing import Union from discord.ext.commands import Context -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError diff --git a/Handlers/NowPlayingHandler.py b/Handlers/NowPlayingHandler.py index 500ba4d..cbaf2ad 100644 --- a/Handlers/NowPlayingHandler.py +++ b/Handlers/NowPlayingHandler.py @@ -1,7 +1,7 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from Utils.Cleaner import Cleaner from Parallelism.ProcessManager import ProcessManager diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py index 61a0c9d..95d698c 100644 --- a/Handlers/PauseHandler.py +++ b/Handlers/PauseHandler.py @@ -3,7 +3,7 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class PauseHandler(AbstractHandler): diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index d1c4945..4065635 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -11,7 +11,7 @@ from Music.Song import Song from Parallelism.ProcessManager import ProcessManager from Parallelism.ProcessInfo import ProcessInfo from Parallelism.Commands import VCommands, VCommandsType -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class PlayHandler(AbstractHandler): diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index 4ef209e..60dbf24 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -4,7 +4,7 @@ from Config.Exceptions import BadCommandUsage, ImpossibleMove from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class PrevHandler(AbstractHandler): diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index 3f691bb..79cab2a 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -4,7 +4,7 @@ from Handlers.HandlerResponse import HandlerResponse from Music.Downloader import Downloader from Utils.Utils import Utils from Parallelism.ProcessManager import ProcessManager -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class QueueHandler(AbstractHandler): diff --git a/Handlers/RemoveHandler.py b/Handlers/RemoveHandler.py index c3a8511..86d094e 100644 --- a/Handlers/RemoveHandler.py +++ b/Handlers/RemoveHandler.py @@ -5,7 +5,7 @@ from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired from Music.Playlist import Playlist from Parallelism.ProcessManager import ProcessManager -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class RemoveHandler(AbstractHandler): diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py index 762b849..f04d88e 100644 --- a/Handlers/ResetHandler.py +++ b/Handlers/ResetHandler.py @@ -3,7 +3,7 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class ResetHandler(AbstractHandler): diff --git a/Handlers/ResumeHandler.py b/Handlers/ResumeHandler.py index abda613..e957be5 100644 --- a/Handlers/ResumeHandler.py +++ b/Handlers/ResumeHandler.py @@ -3,7 +3,7 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class ResumeHandler(AbstractHandler): diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py index 392923a..053873a 100644 --- a/Handlers/ShuffleHandler.py +++ b/Handlers/ShuffleHandler.py @@ -3,7 +3,7 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import UnknownError from Parallelism.ProcessManager import ProcessManager -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class ShuffleHandler(AbstractHandler): diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index 7603ee3..89e2ee9 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -2,7 +2,7 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Config.Exceptions import BadCommandUsage from Handlers.HandlerResponse import HandlerResponse -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType diff --git a/Handlers/StopHandler.py b/Handlers/StopHandler.py index 0a59cae..3c3908d 100644 --- a/Handlers/StopHandler.py +++ b/Handlers/StopHandler.py @@ -1,7 +1,7 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType diff --git a/Music/MusicBot.py b/Music/VulkanBot.py similarity index 96% rename from Music/MusicBot.py rename to Music/VulkanBot.py index 9093883..5b2ce45 100644 --- a/Music/MusicBot.py +++ b/Music/VulkanBot.py @@ -2,12 +2,12 @@ from asyncio import AbstractEventLoop from discord import Guild, Status, Game, Message from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument from Config.Configs import Configs -from discord.ext import commands +from discord.ext.commands import Bot, Context from Config.Messages import Messages from Views.Embeds import Embeds -class VulkanBot(commands.Bot): +class VulkanBot(Bot): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__configs = Configs() @@ -68,6 +68,6 @@ class VulkanBot(commands.Bot): await self.invoke(ctx) -class Context(commands.Context): +class Context(Context): bot: VulkanBot guild: Guild diff --git a/Music/VulkanInitializer.py b/Music/VulkanInitializer.py index dcf1776..6121aeb 100644 --- a/Music/VulkanInitializer.py +++ b/Music/VulkanInitializer.py @@ -2,9 +2,10 @@ from random import choices import string from discord.bot import Bot from discord import Intents -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from os import listdir from Config.Configs import Configs +from Config.Exceptions import VulkanError class VulkanInitializer: @@ -35,9 +36,18 @@ class VulkanInitializer: try: for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): if filename.endswith('.py'): - print(f'Loading {filename}') - bot.load_extension(f'{self.__config.COMMANDS_PATH}.{filename[:-3]}') + cogPath = f'{self.__config.COMMANDS_PATH}.{filename[:-3]}' + bot.load_extension(cogPath, store=True) - bot.load_extension(f'DiscordCogs.MusicCog') - except Exception as e: - print(e) + if len(bot.cogs.keys()) != self.__getTotalCogs(): + raise VulkanError(message='Failed to load some Cog') + + except VulkanError as e: + print(f'[Error Loading Vulkan] -> {e.message}') + + def __getTotalCogs(self) -> int: + quant = 0 + for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): + if filename.endswith('.py'): + quant += 1 + return quant diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index ab2bd8d..5cbc522 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -10,7 +10,7 @@ from Music.Playlist import Playlist from Music.Song import Song from Config.Configs import Configs from Config.Messages import Messages -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot from Views.Embeds import Embeds from Parallelism.Commands import VCommands, VCommandsType diff --git a/Utils/Cleaner.py b/Utils/Cleaner.py index d66a0f2..57e3457 100644 --- a/Utils/Cleaner.py +++ b/Utils/Cleaner.py @@ -2,7 +2,7 @@ from typing import List from discord.ext.commands import Context from discord import Message, Embed from Config.Singleton import Singleton -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class Cleaner(Singleton): diff --git a/Views/AbstractView.py b/Views/AbstractView.py index a8f848b..41e71b1 100644 --- a/Views/AbstractView.py +++ b/Views/AbstractView.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from Handlers.HandlerResponse import HandlerResponse from discord.ext.commands import Context from discord import Message -from Music.MusicBot import VulkanBot +from Music.VulkanBot import VulkanBot class AbstractView(ABC): diff --git a/main.py b/main.py index c800823..c44bff4 100644 --- a/main.py +++ b/main.py @@ -1,46 +1,4 @@ -from random import choices -import string -from discord.bot import Bot -from discord import Intents -from Music.MusicBot import VulkanBot -from os import listdir -from Config.Configs import Configs - - -class VulkanInitializer: - def __init__(self, willListen: bool) -> None: - self.__config = Configs() - self.__intents = Intents.default() - self.__intents.message_content = True - self.__intents.members = True - self.__bot = self.__create_bot(willListen) - self.__add_cogs(self.__bot) - - def getBot(self) -> VulkanBot: - return self.__bot - - def __create_bot(self, willListen: bool) -> VulkanBot: - if willListen: - prefix = self.__config.BOT_PREFIX - else: - prefix = ''.join(choices(string.ascii_uppercase + string.digits, k=4)) - - bot = VulkanBot(command_prefix=prefix, - pm_help=True, - case_insensitive=True, - intents=self.__intents) - return bot - - def __add_cogs(self, bot: Bot) -> None: - try: - for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): - if filename.endswith('.py'): - print(f'Loading {filename}') - bot.load_extension(f'{self.__config.COMMANDS_PATH}.{filename[:-3]}') - - bot.load_extension(f'DiscordCogs.MusicCog') - except Exception as e: - print(e) +from Music.VulkanInitializer import VulkanInitializer if __name__ == '__main__': From 4f11506c2b85c5609e07868f2969318b9c3ae007 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Wed, 27 Jul 2022 16:14:13 -0300 Subject: [PATCH 4/9] Creating Player View --- Config/Colors.py | 2 +- Config/Configs.py | 2 +- {Views => Config}/Embeds.py | 10 +-- Config/Emojis.py | 20 +++++ Config/Helper.py | 4 +- Config/Messages.py | 88 ++++++++++--------- DiscordCogs/ControlCog.py | 12 +-- DiscordCogs/MusicCog.py | 72 ++++++++------- DiscordCogs/RandomCog.py | 4 +- Handlers/AbstractHandler.py | 12 +-- Music/Downloader.py | 6 +- Music/Playlist.py | 7 +- Music/SpotifySearcher.py | 4 +- Music/VulkanBot.py | 8 +- Music/VulkanInitializer.py | 4 +- Parallelism/PlayerProcess.py | 68 +++++++------- Utils/Utils.py | 4 +- ...AbstractView.py => AbstractCogResponse.py} | 2 +- Views/{EmbedView.py => EmbedCogResponse.py} | 4 +- Views/EmoteCogResponse.py | 16 ++++ Views/EmoteView.py | 14 --- Views/PlayerView.py | 47 ++++++++++ 22 files changed, 246 insertions(+), 164 deletions(-) rename {Views => Config}/Embeds.py (98%) create mode 100644 Config/Emojis.py rename Views/{AbstractView.py => AbstractCogResponse.py} (95%) rename Views/{EmbedView.py => EmbedCogResponse.py} (70%) create mode 100644 Views/EmoteCogResponse.py delete mode 100644 Views/EmoteView.py create mode 100644 Views/PlayerView.py diff --git a/Config/Colors.py b/Config/Colors.py index 1f55a3d..c990c2a 100644 --- a/Config/Colors.py +++ b/Config/Colors.py @@ -1,7 +1,7 @@ from Config.Singleton import Singleton -class Colors(Singleton): +class VColors(Singleton): def __init__(self) -> None: self.__red = 0xDC143C self.__green = 0x1F8B4C diff --git a/Config/Configs.py b/Config/Configs.py index 09395bb..523f7e1 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -2,7 +2,7 @@ from decouple import config from Config.Singleton import Singleton -class Configs(Singleton): +class VConfigs(Singleton): def __init__(self) -> None: if not super().created: self.BOT_PREFIX = '!' diff --git a/Views/Embeds.py b/Config/Embeds.py similarity index 98% rename from Views/Embeds.py rename to Config/Embeds.py index 894bf52..801a818 100644 --- a/Views/Embeds.py +++ b/Config/Embeds.py @@ -1,16 +1,16 @@ from Config.Messages import Messages from Config.Exceptions import VulkanError from discord import Embed -from Config.Configs import Configs -from Config.Colors import Colors +from Config.Configs import VConfigs +from Config.Colors import VColors from datetime import timedelta -class Embeds: +class VEmbeds: def __init__(self) -> None: - self.__config = Configs() + self.__config = VConfigs() self.__messages = Messages() - self.__colors = Colors() + self.__colors = VColors() def ONE_SONG_LOOPING(self, info: dict) -> Embed: title = self.__messages.ONE_SONG_LOOPING diff --git a/Config/Emojis.py b/Config/Emojis.py new file mode 100644 index 0000000..d34b4d2 --- /dev/null +++ b/Config/Emojis.py @@ -0,0 +1,20 @@ +from Config.Singleton import Singleton + + +class VEmojis(Singleton): + def __init__(self) -> None: + if not super().created: + self.SKIP = "⏩" + self.BACK = "⏪" + self.PAUSE = "⏸️" + self.PLAY = "▶️" + self.STOP = "⏹️" + self.LOOP_ONE = "🔂" + self.LOOP_OFF = "➡️" + self.LOOP_ALL = "🔁" + self.SHUFFLE = "🔀" + self.QUEUE = "📜" + self.MUSIC = "🎧" + self.ERROR = "❌" + self.DOWNLOADING = "📥" + self.SUCCESS = "✅" diff --git a/Config/Helper.py b/Config/Helper.py index 656bfa6..49ee40c 100644 --- a/Config/Helper.py +++ b/Config/Helper.py @@ -1,11 +1,11 @@ from Config.Singleton import Singleton -from Config.Configs import Configs +from Config.Configs import VConfigs class Helper(Singleton): def __init__(self) -> None: if not super().created: - config = Configs() + config = VConfigs() self.HELP_SKIP = 'Skip the current playing song.' self.HELP_SKIP_LONG = 'Skip the playing of the current song, does not work if loop one is activated. \n\nArguments: None.' self.HELP_RESUME = 'Resumes the song player.' diff --git a/Config/Messages.py b/Config/Messages.py index 47ab1b5..aaa24a7 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -1,11 +1,13 @@ from Config.Singleton import Singleton -from Config.Configs import Configs +from Config.Configs import VConfigs +from Config.Emojis import VEmojis class Messages(Singleton): def __init__(self) -> None: if not super().created: - configs = Configs() + self.__emojis = VEmojis() + configs = VConfigs() self.STARTUP_MESSAGE = 'Starting Vulkan...' self.STARTUP_COMPLETE_MESSAGE = 'Vulkan is now operating.' @@ -16,67 +18,67 @@ class Messages(Singleton): self.SONGS_ADDED = 'Downloading `{}` songs to add to the queue' self.SONG_ADDED = 'Downloading the song `{}` to add to the queue' - self.SONG_ADDED_TWO = '🎧 Song added to the queue' - self.SONG_PLAYING = '🎧 Song playing now' - self.SONG_PLAYER = '🎧 Song Player' - self.QUEUE_TITLE = '🎧 Songs in Queue' - self.ONE_SONG_LOOPING = '🎧 Looping One Song' - self.ALL_SONGS_LOOPING = '🎧 Looping All Songs' - self.SONG_PAUSED = '⏸️ Song paused' - self.SONG_RESUMED = '▶️ Song playing' - self.EMPTY_QUEUE = f'📜 Song queue is empty, use {configs.BOT_PREFIX}play to add new songs' - self.SONG_DOWNLOADING = '📥 Downloading...' + self.SONG_ADDED_TWO = f'{self.__emojis.MUSIC} Song added to the queue' + self.SONG_PLAYING = f'{self.__emojis.MUSIC} Song playing now' + self.SONG_PLAYER = f'{self.__emojis.MUSIC} Song Player' + self.QUEUE_TITLE = f'{self.__emojis.MUSIC} Songs in Queue' + self.ONE_SONG_LOOPING = f'{self.__emojis.MUSIC} Looping One Song' + self.ALL_SONGS_LOOPING = f'{self.__emojis.MUSIC} Looping All Songs' + self.SONG_PAUSED = f'{self.__emojis.PAUSE} Song paused' + self.SONG_RESUMED = f'{self.__emojis.PLAY} Song playing' + self.EMPTY_QUEUE = f'{self.__emojis.QUEUE} Song queue is empty, use {configs.BOT_PREFIX}play to add new songs' + self.SONG_DOWNLOADING = f'{self.__emojis.DOWNLOADING} Downloading...' - self.HISTORY_TITLE = '🎧 Played Songs' - self.HISTORY_EMPTY = '📜 There is no musics in history' + self.HISTORY_TITLE = f'{self.__emojis.MUSIC} Played Songs' + self.HISTORY_EMPTY = f'{self.__emojis.QUEUE} There is no musics in history' self.SONG_MOVED_SUCCESSFULLY = 'Song `{}` in position `{}` moved to the position `{}` successfully' self.SONG_REMOVED_SUCCESSFULLY = 'Song `{}` removed successfully' - self.LOOP_ALL_ON = f'❌ Vulkan is looping all songs, use {configs.BOT_PREFIX}loop off to disable this loop first' - self.LOOP_ONE_ON = f'❌ Vulkan is looping one song, use {configs.BOT_PREFIX}loop off to disable this loop first' - self.LOOP_ALL_ALREADY_ON = '🔁 Vulkan is already looping all songs' - self.LOOP_ONE_ALREADY_ON = '🔂 Vulkan is already looping the current song' - self.LOOP_ALL_ACTIVATE = '🔁 Looping all songs' - self.LOOP_ONE_ACTIVATE = '🔂 Looping the current song' - self.LOOP_DISABLE = '➡️ Loop disabled' - self.LOOP_ALREADY_DISABLE = '❌ Loop is already disabled' - self.LOOP_ON = f'❌ This command cannot be invoked with any loop activated. Use {configs.BOT_PREFIX}loop off to disable loop' - self.BAD_USE_OF_LOOP = f"""❌ Invalid arguments of Loop command. Use {configs.BOT_PREFIX}help loop to more information. - -> Available Arguments: ["all", "off", "one", ""]""" + self.LOOP_ALL_ON = f'{self.__emojis.ERROR} Vulkan is looping all songs, use {configs.BOT_PREFIX}loop off to disable this loop first' + self.LOOP_ONE_ON = f'{self.__emojis.ERROR} Vulkan is looping one song, use {configs.BOT_PREFIX}loop off to disable this loop first' + self.LOOP_ALL_ALREADY_ON = f'{self.__emojis.LOOP_ALL} Vulkan is already looping all songs' + self.LOOP_ONE_ALREADY_ON = f'{self.__emojis.LOOP_ONE} Vulkan is already looping the current song' + self.LOOP_ALL_ACTIVATE = f'{self.__emojis.LOOP_ALL} Looping all songs' + self.LOOP_ONE_ACTIVATE = f'{self.__emojis.LOOP_ONE} Looping the current song' + self.LOOP_DISABLE = f'{self.__emojis.LOOP_OFF} Loop disabled' + self.LOOP_ALREADY_DISABLE = f'{self.__emojis.ERROR} Loop is already disabled' + self.LOOP_ON = f'{self.__emojis.ERROR} This command cannot be invoked with any loop activated. Use {configs.BOT_PREFIX}loop off to disable loop' + self.BAD_USE_OF_LOOP = f"""{self.__emojis.ERROR} Invalid arguments of Loop command. Use {configs.BOT_PREFIX}help loop to more information. + -> Available Arguments: ["all", "off", "one", ""]""" - self.SONGS_SHUFFLED = '🔀 Songs shuffled successfully' - self.ERROR_SHUFFLING = '❌ Error while shuffling the songs' - self.ERROR_MOVING = '❌ Error while moving the songs' - self.LENGTH_ERROR = '❌ Numbers must be between 1 and queue length, use -1 for the last song' - self.ERROR_NUMBER = '❌ This command require a number' - self.ERROR_PLAYING = '❌ Error while playing songs' - self.COMMAND_NOT_FOUND = f'❌ Command not found, type {configs.BOT_PREFIX}help to see all commands' - self.UNKNOWN_ERROR = f'❌ Unknown Error, if needed, use {configs.BOT_PREFIX}reset to reset the player of your server' - self.ERROR_MISSING_ARGUMENTS = f'❌ Missing arguments in this command. Type {configs.BOT_PREFIX}help "command" to see more info about this command' - self.NOT_PREVIOUS = '❌ There is none previous song to play' - self.PLAYER_NOT_PLAYING = f'❌ No song playing. Use {configs.BOT_PREFIX}play to start the player' + self.SONGS_SHUFFLED = f'{self.__emojis.SHUFFLE} Songs shuffled successfully' + self.ERROR_SHUFFLING = f'{self.__emojis.ERROR} Error while shuffling the songs' + self.ERROR_MOVING = f'{self.__emojis.ERROR} Error while moving the songs' + self.LENGTH_ERROR = f'{self.__emojis.ERROR} Numbers must be between 1 and queue length, use -1 for the last song' + self.ERROR_NUMBER = f'{self.__emojis.ERROR} This command require a number' + self.ERROR_PLAYING = f'{self.__emojis.ERROR} Error while playing songs' + self.COMMAND_NOT_FOUND = f'{self.__emojis.ERROR} Command not found, type {configs.BOT_PREFIX}help to see all commands' + self.UNKNOWN_ERROR = f'{self.__emojis.ERROR} Unknown Error, if needed, use {configs.BOT_PREFIX}reset to reset the player of your server' + self.ERROR_MISSING_ARGUMENTS = f'{self.__emojis.ERROR} Missing arguments in this command. Type {configs.BOT_PREFIX}help "command" to see more info about this command' + self.NOT_PREVIOUS = f'{self.__emojis.ERROR} There is none previous song to play' + self.PLAYER_NOT_PLAYING = f'{self.__emojis.ERROR} No song playing. Use {configs.BOT_PREFIX}play to start the player' self.IMPOSSIBLE_MOVE = 'That is impossible :(' self.ERROR_TITLE = 'Error :-(' self.COMMAND_NOT_FOUND_TITLE = 'This is strange :-(' self.NO_CHANNEL = 'To play some music, connect to any voice channel first.' self.NO_GUILD = f'This server does not has a Player, try {configs.BOT_PREFIX}reset' self.INVALID_INPUT = f'This URL was too strange, try something better or type {configs.BOT_PREFIX}help play' - self.DOWNLOADING_ERROR = "❌ It's impossible to download and play this video" - self.EXTRACTING_ERROR = '❌ An error ocurred while searching for the songs' + self.DOWNLOADING_ERROR = f"{self.__emojis.ERROR} It's impossible to download and play this video" + self.EXTRACTING_ERROR = f'{self.__emojis.ERROR} An error ocurred while searching for the songs' - self.ERROR_IN_PROCESS = "❌ Due to a internal error your player was restarted, skipping the song." + self.ERROR_IN_PROCESS = f"{self.__emojis.ERROR} Due to a internal error your player was restarted, skipping the song." self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose' self.BAD_COMMAND_TITLE = 'Misuse of command' - self.BAD_COMMAND = f'❌ Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better' - self.VIDEO_UNAVAILABLE = '❌ Sorry. This video is unavailable for download.' - self.ERROR_DUE_LOOP_ONE_ON = f'❌ This command cannot be executed with loop one activated. Use {configs.BOT_PREFIX}loop off to disable loop.' + self.BAD_COMMAND = f'{self.__emojis.ERROR} Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better' + self.VIDEO_UNAVAILABLE = f'{self.__emojis.ERROR} Sorry. This video is unavailable for download.' + self.ERROR_DUE_LOOP_ONE_ON = f'{self.__emojis.ERROR} This command cannot be executed with loop one activated. Use {configs.BOT_PREFIX}loop off to disable loop.' class SearchMessages(Singleton): def __init__(self) -> None: if not super().created: - config = Configs() + config = VConfigs() self.UNKNOWN_INPUT = f'This type of input was too strange, try something else or type {config.BOT_PREFIX}help play' self.UNKNOWN_INPUT_TITLE = 'Nothing Found' self.GENERIC_TITLE = 'URL could not be processed' diff --git a/DiscordCogs/ControlCog.py b/DiscordCogs/ControlCog.py index 5b3ae94..a3cce55 100644 --- a/DiscordCogs/ControlCog.py +++ b/DiscordCogs/ControlCog.py @@ -1,10 +1,10 @@ from discord import Embed from discord.ext.commands import Cog, command -from Config.Configs import Configs +from Config.Configs import VConfigs from Config.Helper import Helper -from Config.Colors import Colors +from Config.Colors import VColors from Music.VulkanBot import VulkanBot -from Views.Embeds import Embeds +from Config.Embeds import VEmbeds helper = Helper() @@ -14,9 +14,9 @@ class ControlCog(Cog): def __init__(self, bot: VulkanBot): self.__bot = bot - self.__config = Configs() - self.__colors = Colors() - self.__embeds = Embeds() + self.__config = VConfigs() + self.__colors = VColors() + self.__embeds = VEmbeds() self.__commands = { 'MUSIC': ['resume', 'pause', 'loop', 'stop', 'skip', 'play', 'queue', 'clear', diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index b54fce8..d97e71f 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -1,4 +1,3 @@ -from discord import Guild, Client from discord.ext.commands import Context, command, Cog from Config.Helper import Helper from Handlers.ClearHandler import ClearHandler @@ -16,8 +15,10 @@ from Handlers.ResumeHandler import ResumeHandler from Handlers.HistoryHandler import HistoryHandler from Handlers.QueueHandler import QueueHandler from Handlers.LoopHandler import LoopHandler -from Views.EmoteView import EmoteView -from Views.EmbedView import EmbedView +from Views.EmoteCogResponse import EmoteCommandResponse +from Views.EmbedCogResponse import EmbedCommandResponse +from Views.PlayerView import PlayerView +from Music.VulkanBot import VulkanBot helper = Helper() @@ -29,8 +30,8 @@ class MusicCog(Cog): Execute the handler and then create a specific View to be showed in Discord """ - def __init__(self, bot) -> None: - self.__bot: Client = bot + def __init__(self, bot: VulkanBot) -> None: + self.__bot: VulkanBot = bot @command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) async def play(self, ctx: Context, *args) -> None: @@ -39,8 +40,8 @@ class MusicCog(Cog): response = await controller.run(args) if response is not None: - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -52,7 +53,7 @@ class MusicCog(Cog): controller = QueueHandler(ctx, self.__bot) response = await controller.run() - view2 = EmbedView(response) + view2 = EmbedCommandResponse(response) await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -64,9 +65,9 @@ class MusicCog(Cog): response = await controller.run() if response.success: - view = EmoteView(response) + view = EmoteCommandResponse(response) else: - view = EmbedView(response) + view = EmbedCommandResponse(response) await view.run() except Exception as e: @@ -79,9 +80,9 @@ class MusicCog(Cog): response = await controller.run() if response.success: - view = EmoteView(response) + view = EmoteCommandResponse(response) else: - view = EmbedView(response) + view = EmbedCommandResponse(response) await view.run() except Exception as e: @@ -93,8 +94,8 @@ class MusicCog(Cog): controller = PauseHandler(ctx, self.__bot) response = await controller.run() - view1 = EmoteView(response) - view2 = EmbedView(response) + view1 = EmoteCommandResponse(response) + view2 = EmbedCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -106,8 +107,8 @@ class MusicCog(Cog): controller = ResumeHandler(ctx, self.__bot) response = await controller.run() - view1 = EmoteView(response) - view2 = EmbedView(response) + view1 = EmoteCommandResponse(response) + view2 = EmbedCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -120,8 +121,8 @@ class MusicCog(Cog): response = await controller.run() if response is not None: - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -133,8 +134,8 @@ class MusicCog(Cog): controller = HistoryHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -146,8 +147,8 @@ class MusicCog(Cog): controller = LoopHandler(ctx, self.__bot) response = await controller.run(args) - view1 = EmoteView(response) - view2 = EmbedView(response) + view1 = EmoteCommandResponse(response) + view2 = EmbedCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -159,7 +160,7 @@ class MusicCog(Cog): controller = ClearHandler(ctx, self.__bot) response = await controller.run() - view = EmoteView(response) + view = EmoteCommandResponse(response) await view.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') @@ -170,8 +171,8 @@ class MusicCog(Cog): controller = NowPlayingHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -183,8 +184,8 @@ class MusicCog(Cog): controller = ShuffleHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -196,8 +197,8 @@ class MusicCog(Cog): controller = MoveHandler(ctx, self.__bot) response = await controller.run(pos1, pos2) - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -209,8 +210,8 @@ class MusicCog(Cog): controller = RemoveHandler(ctx, self.__bot) response = await controller.run(position) - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: @@ -222,13 +223,18 @@ class MusicCog(Cog): controller = ResetHandler(ctx, self.__bot) response = await controller.run() - view1 = EmbedView(response) - view2 = EmoteView(response) + view1 = EmbedCommandResponse(response) + view2 = EmoteCommandResponse(response) await view1.run() await view2.run() except Exception as e: print(f'[ERROR IN COG] -> {e}') + @command(name='rafael') + async def rafael(self, ctx: Context) -> None: + view = PlayerView() + await ctx.send(view=view) + def setup(bot): bot.add_cog(MusicCog(bot)) diff --git a/DiscordCogs/RandomCog.py b/DiscordCogs/RandomCog.py index 6ec53c7..7de8a2f 100644 --- a/DiscordCogs/RandomCog.py +++ b/DiscordCogs/RandomCog.py @@ -2,7 +2,7 @@ from random import randint, random from Music.VulkanBot import VulkanBot from discord.ext.commands import Context, command, Cog from Config.Helper import Helper -from Views.Embeds import Embeds +from Config.Embeds import VEmbeds helper = Helper() @@ -11,7 +11,7 @@ class RandomCog(Cog): """Class to listen to commands of type Random""" def __init__(self, bot: VulkanBot): - self.__embeds = Embeds() + self.__embeds = VEmbeds() @command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG, aliases=['rand']) async def random(self, ctx: Context, arg: str) -> None: diff --git a/Handlers/AbstractHandler.py b/Handlers/AbstractHandler.py index de9d592..a1f188d 100644 --- a/Handlers/AbstractHandler.py +++ b/Handlers/AbstractHandler.py @@ -5,9 +5,9 @@ from discord import Client, Guild, ClientUser, Member from Config.Messages import Messages from Music.VulkanBot import VulkanBot from Handlers.HandlerResponse import HandlerResponse -from Config.Configs import Configs +from Config.Configs import VConfigs from Config.Helper import Helper -from Views.Embeds import Embeds +from Config.Embeds import VEmbeds class AbstractHandler(ABC): @@ -18,9 +18,9 @@ class AbstractHandler(ABC): self.__bot_user: ClientUser = self.__bot.user self.__id = self.__bot_user.id self.__messages = Messages() - self.__config = Configs() + self.__config = VConfigs() self.__helper = Helper() - self.__embeds = Embeds() + self.__embeds = VEmbeds() self.__bot_member: Member = self.__get_member() @abstractmethod @@ -48,7 +48,7 @@ class AbstractHandler(ABC): return self.__bot @property - def config(self) -> Configs: + def config(self) -> VConfigs: return self.__config @property @@ -64,7 +64,7 @@ class AbstractHandler(ABC): return self.__ctx @property - def embeds(self) -> Embeds: + def embeds(self) -> VEmbeds: return self.__embeds def __get_member(self) -> Member: diff --git a/Music/Downloader.py b/Music/Downloader.py index 3b97c8d..97a22d0 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -1,6 +1,6 @@ import asyncio from typing import List -from Config.Configs import Configs +from Config.Configs import VConfigs from yt_dlp import YoutubeDL, DownloadError from concurrent.futures import ThreadPoolExecutor from Music.Song import Song @@ -9,7 +9,7 @@ from Config.Exceptions import DownloadingError class Downloader: - config = Configs() + config = VConfigs() __YDL_OPTIONS = {'format': 'bestaudio/best', 'default_search': 'auto', 'playliststart': 0, @@ -34,7 +34,7 @@ class Downloader: __BASE_URL = 'https://www.youtube.com/watch?v={}' def __init__(self) -> None: - self.__config = Configs() + self.__config = VConfigs() self.__music_keys_only = ['resolution', 'fps', 'quality'] self.__not_extracted_keys_only = ['ie_key'] self.__not_extracted_not_keys = ['entries'] diff --git a/Music/Playlist.py b/Music/Playlist.py index 33bcbce..82be37f 100644 --- a/Music/Playlist.py +++ b/Music/Playlist.py @@ -1,6 +1,6 @@ from collections import deque from typing import List -from Config.Configs import Configs +from Config.Configs import VConfigs from Music.Song import Song import random @@ -8,7 +8,7 @@ import random class Playlist: def __init__(self) -> None: - self.__configs = Configs() + self.__configs = VConfigs() self.__queue = deque() # Store the musics to play self.__songs_history = deque() # Store the musics played @@ -62,7 +62,7 @@ class Playlist: # Att played song info if played_song != None: if not self.__looping_one and not self.__looping_all: - if played_song.problematic == False: + if not played_song.problematic: self.__songs_history.appendleft(played_song) if len(self.__songs_history) > self.__configs.MAX_SONGS_HISTORY: @@ -90,6 +90,7 @@ class Playlist: self.__queue.appendleft(self.__current) last_song = self.__songs_history.popleft() # Get the last song + print(f'Setando como {last_song} 2') self.__current = last_song return self.__current # return the song diff --git a/Music/SpotifySearcher.py b/Music/SpotifySearcher.py index f663b3a..c423dd5 100644 --- a/Music/SpotifySearcher.py +++ b/Music/SpotifySearcher.py @@ -2,14 +2,14 @@ from spotipy import Spotify from spotipy.oauth2 import SpotifyClientCredentials from spotipy.exceptions import SpotifyException from Config.Exceptions import SpotifyError -from Config.Configs import Configs +from Config.Configs import VConfigs from Config.Messages import SpotifyMessages class SpotifySearch(): def __init__(self) -> None: self.__messages = SpotifyMessages() - self.__config = Configs() + self.__config = VConfigs() self.__connected = False self.__connect() diff --git a/Music/VulkanBot.py b/Music/VulkanBot.py index 5b2ce45..8a8ba2a 100644 --- a/Music/VulkanBot.py +++ b/Music/VulkanBot.py @@ -1,18 +1,18 @@ from asyncio import AbstractEventLoop from discord import Guild, Status, Game, Message from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument -from Config.Configs import Configs +from Config.Configs import VConfigs from discord.ext.commands import Bot, Context from Config.Messages import Messages -from Views.Embeds import Embeds +from Config.Embeds import VEmbeds class VulkanBot(Bot): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.__configs = Configs() + self.__configs = VConfigs() self.__messages = Messages() - self.__embeds = Embeds() + self.__embeds = VEmbeds() self.remove_command("help") def startBot(self) -> None: diff --git a/Music/VulkanInitializer.py b/Music/VulkanInitializer.py index 6121aeb..b366257 100644 --- a/Music/VulkanInitializer.py +++ b/Music/VulkanInitializer.py @@ -4,13 +4,13 @@ from discord.bot import Bot from discord import Intents from Music.VulkanBot import VulkanBot from os import listdir -from Config.Configs import Configs +from Config.Configs import VConfigs from Config.Exceptions import VulkanError class VulkanInitializer: def __init__(self, willListen: bool) -> None: - self.__config = Configs() + self.__config = VConfigs() self.__intents = Intents.default() self.__intents.message_content = True self.__intents.members = True diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 5cbc522..b3caec7 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -5,13 +5,13 @@ 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 discord import Guild, FFmpegPCMAudio, VoiceChannel, TextChannel from Music.Playlist import Playlist from Music.Song import Song -from Config.Configs import Configs +from Config.Configs import VConfigs from Config.Messages import Messages from Music.VulkanBot import VulkanBot -from Views.Embeds import Embeds +from Config.Embeds import VEmbeds from Parallelism.Commands import VCommands, VCommandsType @@ -21,7 +21,7 @@ class TimeoutClock: self.__task = loop.create_task(self.__executor()) async def __executor(self): - await asyncio.sleep(Configs().VC_TIMEOUT) + await asyncio.sleep(VConfigs().VC_TIMEOUT) await self.__callback() def cancel(self): @@ -56,8 +56,8 @@ class PlayerProcess(Process): self.__author: User = None self.__botMember: Member = None - self.__configs: Configs = None - self.__embeds: Embeds = None + self.__configs: VConfigs = None + self.__embeds: VEmbeds = None self.__messages: Messages = None self.__messagesToDelete: List[Message] = [] self.__playing = False @@ -73,9 +73,9 @@ class PlayerProcess(Process): self.__loop = asyncio.get_event_loop_policy().new_event_loop() asyncio.set_event_loop(self.__loop) - self.__configs = Configs() + self.__configs = VConfigs() self.__messages = Messages() - self.__embeds = Embeds() + self.__embeds = VEmbeds() self.__semStopPlaying = Semaphore(0) self.__loop.run_until_complete(self._run()) @@ -108,9 +108,13 @@ class PlayerProcess(Process): self.__timer.cancel() async def __playPlaylistSongs(self) -> None: + """If the player is not running trigger to play a new song""" if not self.__playing: + song = None with self.__playlistLock: - song = self.__playlist.next_song() + with self.__playerLock: + if not (self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused()): + song = self.__playlist.next_song() if song is not None: self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') @@ -151,40 +155,40 @@ class PlayerProcess(Process): self.__playerLock.release() def __playNext(self, error) -> None: - with self.__playerLock: - if self.__forceStop: # If it's forced to stop player - self.__forceStop = False - return None + with self.__playlistLock: + with self.__playerLock: + if self.__forceStop: # If it's forced to stop player + self.__forceStop = False + return None - with self.__playlistLock: song = self.__playlist.next_song() - if song is not None: - self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') - else: - with self.__playlistLock: + if song is not None: + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + else: self.__playlist.loop_off() - self.__playingSong = None - self.__playing = False + self.__playingSong = None + self.__playing = False async def __playPrev(self, voiceChannelID: int) -> None: with self.__playlistLock: song = self.__playlist.prev_song() - if song is not None: - if self.__guild.voice_client is None: # If not connect, connect to the user voice channel - self.__voiceChannelID = voiceChannelID - self.__voiceChannel = self.__guild.get_channel(self.__voiceChannelID) - await self.__connectToVoiceChannel() + with self.__playerLock: + if song is not None: + if self.__guild.voice_client is None: # If not connect, connect to the user voice channel + self.__voiceChannelID = voiceChannelID + self.__voiceChannel = self.__guild.get_channel(self.__voiceChannelID) + await self.__connectToVoiceChannel() - # If already playing, stop the current play - if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): - # Will forbidden next_song to execute after stopping current player - self.__forceStop = True - self.__guild.voice_client.stop() - self.__playing = False + # If already playing, stop the current play + if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): + # Will forbidden next_song to execute after stopping current player + self.__forceStop = True + self.__guild.voice_client.stop() + self.__playing = False - self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') def __commandsReceiver(self) -> None: while True: diff --git a/Utils/Utils.py b/Utils/Utils.py index 95db315..26b6dd3 100644 --- a/Utils/Utils.py +++ b/Utils/Utils.py @@ -1,8 +1,8 @@ import re import asyncio -from Config.Configs import Configs +from Config.Configs import VConfigs from functools import wraps, partial -config = Configs() +config = VConfigs() class Utils: diff --git a/Views/AbstractView.py b/Views/AbstractCogResponse.py similarity index 95% rename from Views/AbstractView.py rename to Views/AbstractCogResponse.py index 41e71b1..36d7b68 100644 --- a/Views/AbstractView.py +++ b/Views/AbstractCogResponse.py @@ -5,7 +5,7 @@ from discord import Message from Music.VulkanBot import VulkanBot -class AbstractView(ABC): +class AbstractCommandResponse(ABC): def __init__(self, response: HandlerResponse) -> None: self.__response: HandlerResponse = response self.__context: Context = response.ctx diff --git a/Views/EmbedView.py b/Views/EmbedCogResponse.py similarity index 70% rename from Views/EmbedView.py rename to Views/EmbedCogResponse.py index 0e21d5d..dba8053 100644 --- a/Views/EmbedView.py +++ b/Views/EmbedCogResponse.py @@ -1,8 +1,8 @@ -from Views.AbstractView import AbstractView +from Views.AbstractCogResponse import AbstractCommandResponse from Handlers.HandlerResponse import HandlerResponse -class EmbedView(AbstractView): +class EmbedCommandResponse(AbstractCommandResponse): def __init__(self, response: HandlerResponse) -> None: super().__init__(response) diff --git a/Views/EmoteCogResponse.py b/Views/EmoteCogResponse.py new file mode 100644 index 0000000..79b4a3e --- /dev/null +++ b/Views/EmoteCogResponse.py @@ -0,0 +1,16 @@ +from Config.Emojis import VEmojis +from Views.AbstractCogResponse import AbstractCommandResponse +from Handlers.HandlerResponse import HandlerResponse + + +class EmoteCommandResponse(AbstractCommandResponse): + + def __init__(self, response: HandlerResponse) -> None: + super().__init__(response) + self.__emojis = VEmojis() + + async def run(self) -> None: + if self.response.success: + await self.message.add_reaction(self.__emojis.SUCCESS) + else: + await self.message.add_reaction(self.__emojis.ERROR) diff --git a/Views/EmoteView.py b/Views/EmoteView.py deleted file mode 100644 index a70202d..0000000 --- a/Views/EmoteView.py +++ /dev/null @@ -1,14 +0,0 @@ -from Views.AbstractView import AbstractView -from Handlers.HandlerResponse import HandlerResponse - - -class EmoteView(AbstractView): - - def __init__(self, response: HandlerResponse) -> None: - super().__init__(response) - - async def run(self) -> None: - if self.response.success: - await self.message.add_reaction('✅') - else: - await self.message.add_reaction('❌') diff --git a/Views/PlayerView.py b/Views/PlayerView.py new file mode 100644 index 0000000..79e6a5c --- /dev/null +++ b/Views/PlayerView.py @@ -0,0 +1,47 @@ +from typing import Optional +from discord.ui import View, Button, button +from Config.Emojis import VEmojis +from discord import Interaction, ButtonStyle + +emojis = VEmojis() + + +class PlayerView(View): + def __init__(self, timeout: Optional[float] = 180): + super().__init__(timeout=timeout) + + @button(label="Back", style=ButtonStyle.secondary, emoji=emojis.BACK) + async def prevCallback(self, button: Button, interaction: Interaction) -> None: + await interaction.response.send_message("Hello") + + @button(label="Pause", style=ButtonStyle.secondary, emoji=emojis.PAUSE) + async def pauseCallback(self, button: Button, interaction: Interaction) -> None: + await interaction.response.send_message("Hello") + + @button(label="Play", style=ButtonStyle.secondary, emoji=emojis.PLAY) + async def playCallback(self, button: Button, interaction: Interaction) -> None: + await interaction.response.send_message("Hello") + + @button(label="Stop", style=ButtonStyle.secondary, emoji=emojis.STOP) + async def stopCallback(self, button: Button, interaction: Interaction) -> None: + await interaction.response.send_message("Hello") + + @button(label="Skip", style=ButtonStyle.secondary, emoji=emojis.SKIP) + async def skipCallback(self, button: Button, interaction: Interaction) -> None: + await interaction.response.send_message("Hello") + + @button(label="Songs", style=ButtonStyle.secondary, emoji=emojis.QUEUE) + async def songsCallback(self, button: Button, interaction: Interaction) -> None: + await interaction.response.send_message("Hello") + + @button(label="Loop Off", style=ButtonStyle.grey, emoji=emojis.LOOP_OFF) + async def loopOffCallback(self, button: Button, interaction: Interaction) -> None: + await interaction.response.send_message("Hello") + + @button(label="Loop All", style=ButtonStyle.secondary, emoji=emojis.LOOP_ALL) + async def loopAllCallback(self, button: Button, interaction: Interaction) -> None: + await interaction.response.send_message("Hello") + + @button(label="Loop One", style=ButtonStyle.secondary, emoji=emojis.LOOP_ONE) + async def loopOneCallback(self, button: Button, interaction: Interaction) -> None: + await interaction.response.send_message("Hello") From ca754c6f62dcff88c2fde4bb614b22ced29cb0ba Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Wed, 27 Jul 2022 17:20:57 -0300 Subject: [PATCH 5/9] Subclassing Button class for each button, changing context interface for handlers --- DiscordCogs/MusicCog.py | 8 ++-- Handlers/AbstractHandler.py | 16 +++++-- Handlers/ClearHandler.py | 4 +- Handlers/HandlerResponse.py | 6 +-- Handlers/HistoryHandler.py | 4 +- Handlers/LoopHandler.py | 4 +- Handlers/MoveHandler.py | 4 +- Handlers/NowPlayingHandler.py | 4 +- Handlers/PauseHandler.py | 4 +- Handlers/PlayHandler.py | 4 +- Handlers/PrevHandler.py | 8 ++-- Handlers/QueueHandler.py | 4 +- Handlers/RemoveHandler.py | 4 +- Handlers/ResetHandler.py | 4 +- Handlers/ResumeHandler.py | 4 +- Handlers/ShuffleHandler.py | 4 +- Handlers/SkipHandler.py | 4 +- Handlers/StopHandler.py | 4 +- Music/Playlist.py | 1 - Music/VulkanInitializer.py | 4 +- Parallelism/PlayerProcess.py | 1 + Parallelism/ProcessManager.py | 19 +++++--- UI/Buttons/BackButton.py | 24 ++++++++++ UI/Buttons/LoopAllButton.py | 11 +++++ UI/Buttons/LoopOffButton.py | 11 +++++ UI/Buttons/LoopOneButton.py | 11 +++++ UI/Buttons/PauseButton.py | 11 +++++ UI/Buttons/PlayButton.py | 11 +++++ UI/Buttons/SkipButton.py | 11 +++++ UI/Buttons/SongsButton.py | 11 +++++ UI/Buttons/StopButton.py | 11 +++++ .../Responses}/AbstractCogResponse.py | 0 {Views => UI/Responses}/EmbedCogResponse.py | 2 +- {Views => UI/Responses}/EmoteCogResponse.py | 2 +- UI/Views/PlayerView.py | 30 ++++++++++++ Views/PlayerView.py | 47 ------------------- 36 files changed, 227 insertions(+), 85 deletions(-) create mode 100644 UI/Buttons/BackButton.py create mode 100644 UI/Buttons/LoopAllButton.py create mode 100644 UI/Buttons/LoopOffButton.py create mode 100644 UI/Buttons/LoopOneButton.py create mode 100644 UI/Buttons/PauseButton.py create mode 100644 UI/Buttons/PlayButton.py create mode 100644 UI/Buttons/SkipButton.py create mode 100644 UI/Buttons/SongsButton.py create mode 100644 UI/Buttons/StopButton.py rename {Views => UI/Responses}/AbstractCogResponse.py (100%) rename {Views => UI/Responses}/EmbedCogResponse.py (82%) rename {Views => UI/Responses}/EmoteCogResponse.py (87%) create mode 100644 UI/Views/PlayerView.py delete mode 100644 Views/PlayerView.py diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index d97e71f..4cb361b 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -15,9 +15,9 @@ from Handlers.ResumeHandler import ResumeHandler from Handlers.HistoryHandler import HistoryHandler from Handlers.QueueHandler import QueueHandler from Handlers.LoopHandler import LoopHandler -from Views.EmoteCogResponse import EmoteCommandResponse -from Views.EmbedCogResponse import EmbedCommandResponse -from Views.PlayerView import PlayerView +from UI.Responses.EmoteCogResponse import EmoteCommandResponse +from UI.Responses.EmbedCogResponse import EmbedCommandResponse +from UI.Views.PlayerView import PlayerView from Music.VulkanBot import VulkanBot helper = Helper() @@ -232,7 +232,7 @@ class MusicCog(Cog): @command(name='rafael') async def rafael(self, ctx: Context) -> None: - view = PlayerView() + view = PlayerView(self.__bot) await ctx.send(view=view) diff --git a/Handlers/AbstractHandler.py b/Handlers/AbstractHandler.py index a1f188d..db6729e 100644 --- a/Handlers/AbstractHandler.py +++ b/Handlers/AbstractHandler.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod -from typing import List +from typing import List, Union from discord.ext.commands import Context -from discord import Client, Guild, ClientUser, Member +from discord import Client, Guild, ClientUser, Interaction, Member, User from Config.Messages import Messages from Music.VulkanBot import VulkanBot from Handlers.HandlerResponse import HandlerResponse @@ -11,7 +11,7 @@ from Config.Embeds import VEmbeds class AbstractHandler(ABC): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: self.__bot: VulkanBot = bot self.__guild: Guild = ctx.guild self.__ctx: Context = ctx @@ -22,6 +22,10 @@ class AbstractHandler(ABC): self.__helper = Helper() self.__embeds = VEmbeds() self.__bot_member: Member = self.__get_member() + if isinstance(ctx, Context): + self.__author = ctx.author + else: + self.__author = ctx.user @abstractmethod async def run(self) -> HandlerResponse: @@ -39,6 +43,10 @@ class AbstractHandler(ABC): def bot_user(self) -> ClientUser: return self.__bot_user + @property + def author(self) -> User: + return self.__author + @property def guild(self) -> Guild: return self.__guild @@ -60,7 +68,7 @@ class AbstractHandler(ABC): return self.__helper @property - def ctx(self) -> Context: + def ctx(self) -> Union[Context, Interaction]: return self.__ctx @property diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index 85dd75b..a7735ef 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -1,3 +1,5 @@ +from typing import Union +from discord import Interaction from discord.ext.commands import Context from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler @@ -6,7 +8,7 @@ from Parallelism.ProcessManager import ProcessManager class ClearHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/HandlerResponse.py b/Handlers/HandlerResponse.py index 42c5fdf..6840762 100644 --- a/Handlers/HandlerResponse.py +++ b/Handlers/HandlerResponse.py @@ -1,18 +1,18 @@ from typing import Union from discord.ext.commands import Context from Config.Exceptions import VulkanError -from discord import Embed +from discord import Embed, Interaction class HandlerResponse: - def __init__(self, ctx: Context, embed: Embed = None, error: VulkanError = None) -> None: + def __init__(self, ctx: Union[Context, Interaction], embed: Embed = None, error: VulkanError = None) -> None: self.__ctx: Context = ctx self.__error: VulkanError = error self.__embed: Embed = embed self.__success = False if error else True @property - def ctx(self) -> Context: + def ctx(self) -> Union[Context, Interaction]: return self.__ctx @property diff --git a/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py index e4d0d07..c041335 100644 --- a/Handlers/HistoryHandler.py +++ b/Handlers/HistoryHandler.py @@ -3,11 +3,13 @@ from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Utils.Utils import Utils +from typing import Union from Parallelism.ProcessManager import ProcessManager +from discord import Interaction class HistoryHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index a62e5cb..929333e 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -4,10 +4,12 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage from Parallelism.ProcessManager import ProcessManager +from typing import Union +from discord import Interaction class LoopHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self, args: str) -> HandlerResponse: diff --git a/Handlers/MoveHandler.py b/Handlers/MoveHandler.py index 8088f66..ca9e9f8 100644 --- a/Handlers/MoveHandler.py +++ b/Handlers/MoveHandler.py @@ -6,10 +6,12 @@ from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError from Music.Playlist import Playlist from Parallelism.ProcessManager import ProcessManager +from typing import Union +from discord import Interaction class MoveHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self, pos1: str, pos2: str) -> HandlerResponse: diff --git a/Handlers/NowPlayingHandler.py b/Handlers/NowPlayingHandler.py index cbaf2ad..727c022 100644 --- a/Handlers/NowPlayingHandler.py +++ b/Handlers/NowPlayingHandler.py @@ -4,10 +4,12 @@ from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot from Utils.Cleaner import Cleaner from Parallelism.ProcessManager import ProcessManager +from typing import Union +from discord import Interaction class NowPlayingHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) self.__cleaner = Cleaner() diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py index 95d698c..cbe41b9 100644 --- a/Handlers/PauseHandler.py +++ b/Handlers/PauseHandler.py @@ -4,10 +4,12 @@ from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class PauseHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index 4065635..fbcd085 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -12,10 +12,12 @@ from Parallelism.ProcessManager import ProcessManager from Parallelism.ProcessInfo import ProcessInfo from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class PlayHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) self.__searcher = Searcher() self.__down = Downloader() diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index 60dbf24..0fb3996 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -5,10 +5,12 @@ from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class PrevHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: @@ -41,13 +43,13 @@ class PrevHandler(AbstractHandler): process.start() # Send a prev command, together with the user voice channel - prevCommand = VCommands(VCommandsType.PREV, self.ctx.author.voice.channel.id) + prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id) queue = processInfo.getQueue() queue.put(prevCommand) return HandlerResponse(self.ctx) def __user_connected(self) -> bool: - if self.ctx.author.voice: + if self.author.voice: return True else: return False diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index 79cab2a..e1c0736 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -5,10 +5,12 @@ from Music.Downloader import Downloader from Utils.Utils import Utils from Parallelism.ProcessManager import ProcessManager from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class QueueHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) self.__down = Downloader() diff --git a/Handlers/RemoveHandler.py b/Handlers/RemoveHandler.py index 86d094e..d0a6e23 100644 --- a/Handlers/RemoveHandler.py +++ b/Handlers/RemoveHandler.py @@ -6,10 +6,12 @@ from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, Inval from Music.Playlist import Playlist from Parallelism.ProcessManager import ProcessManager from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class RemoveHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self, position: str) -> HandlerResponse: diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py index f04d88e..de800f7 100644 --- a/Handlers/ResetHandler.py +++ b/Handlers/ResetHandler.py @@ -4,10 +4,12 @@ from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class ResetHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/ResumeHandler.py b/Handlers/ResumeHandler.py index e957be5..924a3b8 100644 --- a/Handlers/ResumeHandler.py +++ b/Handlers/ResumeHandler.py @@ -4,10 +4,12 @@ from Handlers.HandlerResponse import HandlerResponse from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class ResumeHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py index 053873a..dda23e2 100644 --- a/Handlers/ShuffleHandler.py +++ b/Handlers/ShuffleHandler.py @@ -4,10 +4,12 @@ from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import UnknownError from Parallelism.ProcessManager import ProcessManager from Music.VulkanBot import VulkanBot +from typing import Union +from discord import Interaction class ShuffleHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index 89e2ee9..42b0143 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -5,10 +5,12 @@ from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from typing import Union +from discord import Interaction class SkipHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Handlers/StopHandler.py b/Handlers/StopHandler.py index 3c3908d..68ae620 100644 --- a/Handlers/StopHandler.py +++ b/Handlers/StopHandler.py @@ -4,10 +4,12 @@ from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType +from typing import Union +from discord import Interaction class StopHandler(AbstractHandler): - def __init__(self, ctx: Context, bot: VulkanBot) -> None: + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: diff --git a/Music/Playlist.py b/Music/Playlist.py index 82be37f..c86b3cd 100644 --- a/Music/Playlist.py +++ b/Music/Playlist.py @@ -90,7 +90,6 @@ class Playlist: self.__queue.appendleft(self.__current) last_song = self.__songs_history.popleft() # Get the last song - print(f'Setando como {last_song} 2') self.__current = last_song return self.__current # return the song diff --git a/Music/VulkanInitializer.py b/Music/VulkanInitializer.py index b366257..9f847cc 100644 --- a/Music/VulkanInitializer.py +++ b/Music/VulkanInitializer.py @@ -34,12 +34,14 @@ class VulkanInitializer: def __add_cogs(self, bot: Bot) -> None: try: + cogsStatus = [] for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): if filename.endswith('.py'): cogPath = f'{self.__config.COMMANDS_PATH}.{filename[:-3]}' - bot.load_extension(cogPath, store=True) + cogsStatus.append(bot.load_extension(cogPath, store=True)) if len(bot.cogs.keys()) != self.__getTotalCogs(): + print(cogsStatus) raise VulkanError(message='Failed to load some Cog') except VulkanError as e: diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index b3caec7..4dcbd90 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -195,6 +195,7 @@ class PlayerProcess(Process): command: VCommands = self.__queue.get() type = command.getType() args = command.getArgs() + print(f'{self.name} received command {type}') try: self.__playerLock.acquire() diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index 6df1b73..a0e1e4b 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -1,8 +1,8 @@ from multiprocessing import Queue, Lock from multiprocessing.managers import BaseManager, NamespaceProxy -from typing import Dict +from typing import Dict, Union from Config.Singleton import Singleton -from discord import Guild +from discord import Guild, Interaction from discord.ext.commands import Context from Parallelism.PlayerProcess import PlayerProcess from Music.Playlist import Playlist @@ -23,12 +23,19 @@ class ProcessManager(Singleton): self.__manager.start() self.__playersProcess: Dict[Guild, ProcessInfo] = {} - def setPlayerContext(self, guild: Guild, context: ProcessInfo): - self.__playersProcess[guild.id] = context + def setPlayerInfo(self, guild: Guild, info: ProcessInfo): + self.__playersProcess[guild.id] = info - def getPlayerInfo(self, guild: Guild, context: Context) -> ProcessInfo: - """Return the process info for the guild, if not, create one""" + def getPlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: + """Return the process info for the guild, if not and context is a instance + of discord.Context then create one, else return None""" try: + if isinstance(context, Interaction): + if guild.id not in self.__playersProcess.keys(): + return None + else: + return self.__playersProcess[guild.id] + if guild.id not in self.__playersProcess.keys(): self.__playersProcess[guild.id] = self.__createProcessInfo(context) else: diff --git a/UI/Buttons/BackButton.py b/UI/Buttons/BackButton.py new file mode 100644 index 0000000..95fcd95 --- /dev/null +++ b/UI/Buttons/BackButton.py @@ -0,0 +1,24 @@ +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Config.Emojis import VEmojis +from Handlers.PrevHandler import PrevHandler +from Music.VulkanBot import VulkanBot + + +class BackButton(Button): + def __init__(self, bot: VulkanBot): + super().__init__(label="Back", style=ButtonStyle.secondary, emoji=VEmojis().BACK) + self.__bot = bot + + async def callback(self, interaction: Interaction) -> None: + await interaction.response.defer() + + handler = PrevHandler(interaction, self.__bot) + response = await handler.run() + print(response) + print(response.success) + print(response.error) + print(response.error) + print(response.embed) + if response.embed: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/LoopAllButton.py b/UI/Buttons/LoopAllButton.py new file mode 100644 index 0000000..aa37e0a --- /dev/null +++ b/UI/Buttons/LoopAllButton.py @@ -0,0 +1,11 @@ +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Config.Emojis import VEmojis + + +class LoopAllButton(Button): + def __init__(self): + super().__init__(label="Loop All", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ALL) + + async def callback(self, interaction: Interaction) -> None: + pass diff --git a/UI/Buttons/LoopOffButton.py b/UI/Buttons/LoopOffButton.py new file mode 100644 index 0000000..e3e2b64 --- /dev/null +++ b/UI/Buttons/LoopOffButton.py @@ -0,0 +1,11 @@ +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Config.Emojis import VEmojis + + +class LoopOffButton(Button): + def __init__(self): + super().__init__(label="Loop Off", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_OFF) + + async def callback(self, interaction: Interaction) -> None: + pass diff --git a/UI/Buttons/LoopOneButton.py b/UI/Buttons/LoopOneButton.py new file mode 100644 index 0000000..c96029f --- /dev/null +++ b/UI/Buttons/LoopOneButton.py @@ -0,0 +1,11 @@ +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Config.Emojis import VEmojis + + +class LoopOneButton(Button): + def __init__(self): + super().__init__(label="Loop One", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ONE) + + async def callback(self, interaction: Interaction) -> None: + pass diff --git a/UI/Buttons/PauseButton.py b/UI/Buttons/PauseButton.py new file mode 100644 index 0000000..2ca454f --- /dev/null +++ b/UI/Buttons/PauseButton.py @@ -0,0 +1,11 @@ +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Config.Emojis import VEmojis + + +class PauseButton(Button): + def __init__(self): + super().__init__(label="Pause", style=ButtonStyle.secondary, emoji=VEmojis().PAUSE) + + async def callback(self, interaction: Interaction) -> None: + pass diff --git a/UI/Buttons/PlayButton.py b/UI/Buttons/PlayButton.py new file mode 100644 index 0000000..93d23f2 --- /dev/null +++ b/UI/Buttons/PlayButton.py @@ -0,0 +1,11 @@ +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Config.Emojis import VEmojis + + +class PlayButton(Button): + def __init__(self): + super().__init__(label="Play", style=ButtonStyle.secondary, emoji=VEmojis().PLAY) + + async def callback(self, interaction: Interaction) -> None: + pass diff --git a/UI/Buttons/SkipButton.py b/UI/Buttons/SkipButton.py new file mode 100644 index 0000000..5aba74f --- /dev/null +++ b/UI/Buttons/SkipButton.py @@ -0,0 +1,11 @@ +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Config.Emojis import VEmojis + + +class SkipButton(Button): + def __init__(self): + super().__init__(label="Skip", style=ButtonStyle.secondary, emoji=VEmojis().SKIP) + + async def callback(self, interaction: Interaction) -> None: + pass diff --git a/UI/Buttons/SongsButton.py b/UI/Buttons/SongsButton.py new file mode 100644 index 0000000..7d9c11d --- /dev/null +++ b/UI/Buttons/SongsButton.py @@ -0,0 +1,11 @@ +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Config.Emojis import VEmojis + + +class SongsButton(Button): + def __init__(self): + super().__init__(label="Songs", style=ButtonStyle.secondary, emoji=VEmojis().QUEUE) + + async def callback(self, interaction: Interaction) -> None: + pass diff --git a/UI/Buttons/StopButton.py b/UI/Buttons/StopButton.py new file mode 100644 index 0000000..0cdeda8 --- /dev/null +++ b/UI/Buttons/StopButton.py @@ -0,0 +1,11 @@ +from discord import ButtonStyle, Interaction +from discord.ui import Button +from Config.Emojis import VEmojis + + +class StopButton(Button): + def __init__(self): + super().__init__(label="Stop", style=ButtonStyle.secondary, emoji=VEmojis().STOP) + + async def callback(self, interaction: Interaction) -> None: + pass diff --git a/Views/AbstractCogResponse.py b/UI/Responses/AbstractCogResponse.py similarity index 100% rename from Views/AbstractCogResponse.py rename to UI/Responses/AbstractCogResponse.py diff --git a/Views/EmbedCogResponse.py b/UI/Responses/EmbedCogResponse.py similarity index 82% rename from Views/EmbedCogResponse.py rename to UI/Responses/EmbedCogResponse.py index dba8053..9dac78c 100644 --- a/Views/EmbedCogResponse.py +++ b/UI/Responses/EmbedCogResponse.py @@ -1,4 +1,4 @@ -from Views.AbstractCogResponse import AbstractCommandResponse +from UI.Responses.AbstractCogResponse import AbstractCommandResponse from Handlers.HandlerResponse import HandlerResponse diff --git a/Views/EmoteCogResponse.py b/UI/Responses/EmoteCogResponse.py similarity index 87% rename from Views/EmoteCogResponse.py rename to UI/Responses/EmoteCogResponse.py index 79b4a3e..09294cf 100644 --- a/Views/EmoteCogResponse.py +++ b/UI/Responses/EmoteCogResponse.py @@ -1,5 +1,5 @@ from Config.Emojis import VEmojis -from Views.AbstractCogResponse import AbstractCommandResponse +from UI.Responses.AbstractCogResponse import AbstractCommandResponse from Handlers.HandlerResponse import HandlerResponse diff --git a/UI/Views/PlayerView.py b/UI/Views/PlayerView.py new file mode 100644 index 0000000..d37aba1 --- /dev/null +++ b/UI/Views/PlayerView.py @@ -0,0 +1,30 @@ +from typing import Optional +from discord.ui import View +from Config.Emojis import VEmojis +from UI.Buttons.PauseButton import PauseButton +from UI.Buttons.BackButton import BackButton +from UI.Buttons.SkipButton import SkipButton +from UI.Buttons.StopButton import StopButton +from UI.Buttons.SongsButton import SongsButton +from UI.Buttons.PlayButton import PlayButton +from UI.Buttons.LoopAllButton import LoopAllButton +from UI.Buttons.LoopOneButton import LoopOneButton +from UI.Buttons.LoopOffButton import LoopOffButton +from Music.VulkanBot import VulkanBot + +emojis = VEmojis() + + +class PlayerView(View): + def __init__(self, bot: VulkanBot, timeout: float = 180): + super().__init__(timeout=timeout) + self.__bot = bot + self.add_item(BackButton(self.__bot)) + self.add_item(PauseButton()) + self.add_item(PlayButton()) + self.add_item(StopButton()) + self.add_item(SkipButton()) + self.add_item(SongsButton()) + self.add_item(LoopOneButton()) + self.add_item(LoopOffButton()) + self.add_item(LoopAllButton()) diff --git a/Views/PlayerView.py b/Views/PlayerView.py deleted file mode 100644 index 79e6a5c..0000000 --- a/Views/PlayerView.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Optional -from discord.ui import View, Button, button -from Config.Emojis import VEmojis -from discord import Interaction, ButtonStyle - -emojis = VEmojis() - - -class PlayerView(View): - def __init__(self, timeout: Optional[float] = 180): - super().__init__(timeout=timeout) - - @button(label="Back", style=ButtonStyle.secondary, emoji=emojis.BACK) - async def prevCallback(self, button: Button, interaction: Interaction) -> None: - await interaction.response.send_message("Hello") - - @button(label="Pause", style=ButtonStyle.secondary, emoji=emojis.PAUSE) - async def pauseCallback(self, button: Button, interaction: Interaction) -> None: - await interaction.response.send_message("Hello") - - @button(label="Play", style=ButtonStyle.secondary, emoji=emojis.PLAY) - async def playCallback(self, button: Button, interaction: Interaction) -> None: - await interaction.response.send_message("Hello") - - @button(label="Stop", style=ButtonStyle.secondary, emoji=emojis.STOP) - async def stopCallback(self, button: Button, interaction: Interaction) -> None: - await interaction.response.send_message("Hello") - - @button(label="Skip", style=ButtonStyle.secondary, emoji=emojis.SKIP) - async def skipCallback(self, button: Button, interaction: Interaction) -> None: - await interaction.response.send_message("Hello") - - @button(label="Songs", style=ButtonStyle.secondary, emoji=emojis.QUEUE) - async def songsCallback(self, button: Button, interaction: Interaction) -> None: - await interaction.response.send_message("Hello") - - @button(label="Loop Off", style=ButtonStyle.grey, emoji=emojis.LOOP_OFF) - async def loopOffCallback(self, button: Button, interaction: Interaction) -> None: - await interaction.response.send_message("Hello") - - @button(label="Loop All", style=ButtonStyle.secondary, emoji=emojis.LOOP_ALL) - async def loopAllCallback(self, button: Button, interaction: Interaction) -> None: - await interaction.response.send_message("Hello") - - @button(label="Loop One", style=ButtonStyle.secondary, emoji=emojis.LOOP_ONE) - async def loopOneCallback(self, button: Button, interaction: Interaction) -> None: - await interaction.response.send_message("Hello") From 5902a0dc72191dbe8778f167c0b016454c1bc9a5 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Wed, 27 Jul 2022 17:50:27 -0300 Subject: [PATCH 6/9] Others buttons are now working using handlers to do the work --- UI/Buttons/BackButton.py | 8 +++----- UI/Buttons/LoopAllButton.py | 13 +++++++++++-- UI/Buttons/LoopOffButton.py | 13 +++++++++++-- UI/Buttons/LoopOneButton.py | 13 +++++++++++-- UI/Buttons/PauseButton.py | 13 +++++++++++-- UI/Buttons/PlayButton.py | 13 +++++++++++-- UI/Buttons/SkipButton.py | 13 +++++++++++-- UI/Buttons/SongsButton.py | 13 +++++++++++-- UI/Buttons/StopButton.py | 13 +++++++++++-- UI/Views/PlayerView.py | 16 ++++++++-------- 10 files changed, 99 insertions(+), 29 deletions(-) diff --git a/UI/Buttons/BackButton.py b/UI/Buttons/BackButton.py index 95fcd95..e800f6f 100644 --- a/UI/Buttons/BackButton.py +++ b/UI/Buttons/BackButton.py @@ -11,14 +11,12 @@ class BackButton(Button): self.__bot = bot async def callback(self, interaction: Interaction) -> None: + """Callback to when Button is clicked""" + # Return to Discord that this command is being processed await interaction.response.defer() handler = PrevHandler(interaction, self.__bot) response = await handler.run() - print(response) - print(response.success) - print(response.error) - print(response.error) - print(response.embed) + if response.embed: await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/LoopAllButton.py b/UI/Buttons/LoopAllButton.py index aa37e0a..00d7571 100644 --- a/UI/Buttons/LoopAllButton.py +++ b/UI/Buttons/LoopAllButton.py @@ -1,11 +1,20 @@ from discord import ButtonStyle, Interaction from discord.ui import Button from Config.Emojis import VEmojis +from Handlers.LoopHandler import LoopHandler +from Music.VulkanBot import VulkanBot class LoopAllButton(Button): - def __init__(self): + def __init__(self, bot: VulkanBot): super().__init__(label="Loop All", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ALL) + self.__bot = bot async def callback(self, interaction: Interaction) -> None: - pass + await interaction.response.defer() + + handler = LoopHandler(interaction, self.__bot) + response = await handler.run('all') + + if response.embed: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/LoopOffButton.py b/UI/Buttons/LoopOffButton.py index e3e2b64..ce943b3 100644 --- a/UI/Buttons/LoopOffButton.py +++ b/UI/Buttons/LoopOffButton.py @@ -1,11 +1,20 @@ from discord import ButtonStyle, Interaction from discord.ui import Button from Config.Emojis import VEmojis +from Handlers.LoopHandler import LoopHandler +from Music.VulkanBot import VulkanBot class LoopOffButton(Button): - def __init__(self): + def __init__(self, bot: VulkanBot): super().__init__(label="Loop Off", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_OFF) + self.__bot = bot async def callback(self, interaction: Interaction) -> None: - pass + await interaction.response.defer() + + handler = LoopHandler(interaction, self.__bot) + response = await handler.run('off') + + if response.embed: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/LoopOneButton.py b/UI/Buttons/LoopOneButton.py index c96029f..45c3dc7 100644 --- a/UI/Buttons/LoopOneButton.py +++ b/UI/Buttons/LoopOneButton.py @@ -1,11 +1,20 @@ from discord import ButtonStyle, Interaction from discord.ui import Button from Config.Emojis import VEmojis +from Handlers.LoopHandler import LoopHandler +from Music.VulkanBot import VulkanBot class LoopOneButton(Button): - def __init__(self): + def __init__(self, bot: VulkanBot): super().__init__(label="Loop One", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ONE) + self.__bot = bot async def callback(self, interaction: Interaction) -> None: - pass + await interaction.response.defer() + + handler = LoopHandler(interaction, self.__bot) + response = await handler.run('one') + + if response.embed: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/PauseButton.py b/UI/Buttons/PauseButton.py index 2ca454f..996e829 100644 --- a/UI/Buttons/PauseButton.py +++ b/UI/Buttons/PauseButton.py @@ -1,11 +1,20 @@ from discord import ButtonStyle, Interaction from discord.ui import Button from Config.Emojis import VEmojis +from Handlers.PauseHandler import PauseHandler +from Music.VulkanBot import VulkanBot class PauseButton(Button): - def __init__(self): + def __init__(self, bot: VulkanBot): super().__init__(label="Pause", style=ButtonStyle.secondary, emoji=VEmojis().PAUSE) + self.__bot = bot async def callback(self, interaction: Interaction) -> None: - pass + await interaction.response.defer() + + handler = PauseHandler(interaction, self.__bot) + response = await handler.run() + + if response.embed: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/PlayButton.py b/UI/Buttons/PlayButton.py index 93d23f2..e0e939b 100644 --- a/UI/Buttons/PlayButton.py +++ b/UI/Buttons/PlayButton.py @@ -1,11 +1,20 @@ from discord import ButtonStyle, Interaction from discord.ui import Button from Config.Emojis import VEmojis +from Music.VulkanBot import VulkanBot +from Handlers.ResumeHandler import ResumeHandler class PlayButton(Button): - def __init__(self): + def __init__(self, bot: VulkanBot): super().__init__(label="Play", style=ButtonStyle.secondary, emoji=VEmojis().PLAY) + self.__bot = bot async def callback(self, interaction: Interaction) -> None: - pass + await interaction.response.defer() + + handler = ResumeHandler(interaction, self.__bot) + response = await handler.run() + + if response.embed: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/SkipButton.py b/UI/Buttons/SkipButton.py index 5aba74f..479a210 100644 --- a/UI/Buttons/SkipButton.py +++ b/UI/Buttons/SkipButton.py @@ -1,11 +1,20 @@ from discord import ButtonStyle, Interaction from discord.ui import Button from Config.Emojis import VEmojis +from Music.VulkanBot import VulkanBot +from Handlers.SkipHandler import SkipHandler class SkipButton(Button): - def __init__(self): + def __init__(self, bot: VulkanBot): super().__init__(label="Skip", style=ButtonStyle.secondary, emoji=VEmojis().SKIP) + self.__bot = bot async def callback(self, interaction: Interaction) -> None: - pass + await interaction.response.defer() + + handler = SkipHandler(interaction, self.__bot) + response = await handler.run() + + if response.embed: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/SongsButton.py b/UI/Buttons/SongsButton.py index 7d9c11d..fa57da1 100644 --- a/UI/Buttons/SongsButton.py +++ b/UI/Buttons/SongsButton.py @@ -1,11 +1,20 @@ +from Handlers.QueueHandler import QueueHandler from discord import ButtonStyle, Interaction from discord.ui import Button from Config.Emojis import VEmojis +from Music.VulkanBot import VulkanBot class SongsButton(Button): - def __init__(self): + def __init__(self, bot: VulkanBot): super().__init__(label="Songs", style=ButtonStyle.secondary, emoji=VEmojis().QUEUE) + self.__bot = bot async def callback(self, interaction: Interaction) -> None: - pass + await interaction.response.defer() + + handler = QueueHandler(interaction, self.__bot) + response = await handler.run() + + if response.embed: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Buttons/StopButton.py b/UI/Buttons/StopButton.py index 0cdeda8..18b0535 100644 --- a/UI/Buttons/StopButton.py +++ b/UI/Buttons/StopButton.py @@ -1,11 +1,20 @@ from discord import ButtonStyle, Interaction from discord.ui import Button from Config.Emojis import VEmojis +from Music.VulkanBot import VulkanBot +from Handlers.StopHandler import StopHandler class StopButton(Button): - def __init__(self): + def __init__(self, bot: VulkanBot): super().__init__(label="Stop", style=ButtonStyle.secondary, emoji=VEmojis().STOP) + self.__bot = bot async def callback(self, interaction: Interaction) -> None: - pass + await interaction.response.defer() + + handler = StopHandler(interaction, self.__bot) + response = await handler.run() + + if response.embed: + await interaction.followup.send(embed=response.embed) diff --git a/UI/Views/PlayerView.py b/UI/Views/PlayerView.py index d37aba1..0892462 100644 --- a/UI/Views/PlayerView.py +++ b/UI/Views/PlayerView.py @@ -20,11 +20,11 @@ class PlayerView(View): super().__init__(timeout=timeout) self.__bot = bot self.add_item(BackButton(self.__bot)) - self.add_item(PauseButton()) - self.add_item(PlayButton()) - self.add_item(StopButton()) - self.add_item(SkipButton()) - self.add_item(SongsButton()) - self.add_item(LoopOneButton()) - self.add_item(LoopOffButton()) - self.add_item(LoopAllButton()) + self.add_item(PauseButton(self.__bot)) + self.add_item(PlayButton(self.__bot)) + self.add_item(StopButton(self.__bot)) + self.add_item(SkipButton(self.__bot)) + self.add_item(SongsButton(self.__bot)) + self.add_item(LoopOneButton(self.__bot)) + self.add_item(LoopOffButton(self.__bot)) + self.add_item(LoopAllButton(self.__bot)) From 60a36425eedc77edc4bc5e318486161062f919ba Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Thu, 28 Jul 2022 00:38:30 -0300 Subject: [PATCH 7/9] Adding a queue for player send commands to Main process --- Config/Configs.py | 6 ++ DiscordCogs/MusicCog.py | 3 + Handlers/ClearHandler.py | 3 +- Handlers/HistoryHandler.py | 3 +- Handlers/LoopHandler.py | 3 +- Handlers/MoveHandler.py | 3 +- Handlers/NowPlayingHandler.py | 3 +- Handlers/PauseHandler.py | 5 +- Handlers/PlayHandler.py | 9 ++- Handlers/PrevHandler.py | 5 +- Handlers/QueueHandler.py | 3 +- Handlers/RemoveHandler.py | 3 +- Handlers/ResetHandler.py | 5 +- Handlers/ResumeHandler.py | 5 +- Handlers/ShuffleHandler.py | 3 +- Handlers/SkipHandler.py | 5 +- Handlers/StopHandler.py | 5 +- Music/MessagesController.py | 63 ++++++++++++++++++ Parallelism/Commands.py | 3 + Parallelism/PlayerProcess.py | 33 +++++----- Parallelism/ProcessInfo.py | 17 +++-- Parallelism/ProcessManager.py | 121 +++++++++++++++++++++++++++++----- UI/Views/PlayerView.py | 1 - 23 files changed, 233 insertions(+), 77 deletions(-) create mode 100644 Music/MessagesController.py diff --git a/Config/Configs.py b/Config/Configs.py index 523f7e1..b7120e4 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -30,3 +30,9 @@ class VConfigs(Singleton): self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose' self.INVITE_URL = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot' + + def getProcessManager(self): + return self.__manager + + def setProcessManager(self, newManager): + self.__manager = newManager diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 4cb361b..859e88b 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -19,6 +19,8 @@ from UI.Responses.EmoteCogResponse import EmoteCommandResponse from UI.Responses.EmbedCogResponse import EmbedCommandResponse from UI.Views.PlayerView import PlayerView from Music.VulkanBot import VulkanBot +from Config.Configs import VConfigs +from Parallelism.ProcessManager import ProcessManager helper = Helper() @@ -32,6 +34,7 @@ class MusicCog(Cog): def __init__(self, bot: VulkanBot) -> None: self.__bot: VulkanBot = bot + VConfigs().setProcessManager(ProcessManager(bot)) @command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) async def play(self, ctx: Context, *args) -> None: diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index a7735ef..a151d83 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -4,7 +4,6 @@ from discord.ext.commands import Context from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager class ClearHandler(AbstractHandler): @@ -13,7 +12,7 @@ class ClearHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Clear the playlist diff --git a/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py index c041335..864d90e 100644 --- a/Handlers/HistoryHandler.py +++ b/Handlers/HistoryHandler.py @@ -4,7 +4,6 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Utils.Utils import Utils from typing import Union -from Parallelism.ProcessManager import ProcessManager from discord import Interaction @@ -14,7 +13,7 @@ class HistoryHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: processLock = processInfo.getLock() diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index 929333e..065334a 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -3,7 +3,6 @@ from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage -from Parallelism.ProcessManager import ProcessManager from typing import Union from discord import Interaction @@ -14,7 +13,7 @@ class LoopHandler(AbstractHandler): async def run(self, args: str) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/MoveHandler.py b/Handlers/MoveHandler.py index ca9e9f8..0f36260 100644 --- a/Handlers/MoveHandler.py +++ b/Handlers/MoveHandler.py @@ -5,7 +5,6 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError from Music.Playlist import Playlist -from Parallelism.ProcessManager import ProcessManager from typing import Union from discord import Interaction @@ -15,7 +14,7 @@ class MoveHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self, pos1: str, pos2: str) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/NowPlayingHandler.py b/Handlers/NowPlayingHandler.py index 727c022..81dc414 100644 --- a/Handlers/NowPlayingHandler.py +++ b/Handlers/NowPlayingHandler.py @@ -3,7 +3,6 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot from Utils.Cleaner import Cleaner -from Parallelism.ProcessManager import ProcessManager from typing import Union from discord import Interaction @@ -15,7 +14,7 @@ class NowPlayingHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py index cbe41b9..dc80bce 100644 --- a/Handlers/PauseHandler.py +++ b/Handlers/PauseHandler.py @@ -1,7 +1,6 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot from typing import Union @@ -13,12 +12,12 @@ class PauseHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Send Pause command to be execute by player process command = VCommands(VCommandsType.PAUSE, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index fbcd085..4674336 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -8,7 +8,6 @@ from Handlers.HandlerResponse import HandlerResponse from Music.Downloader import Downloader from Music.Searcher import Searcher from Music.Song import Song -from Parallelism.ProcessManager import ProcessManager from Parallelism.ProcessInfo import ProcessInfo from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot @@ -38,7 +37,7 @@ class PlayHandler(AbstractHandler): raise InvalidInput(self.messages.INVALID_INPUT, self.messages.ERROR_TITLE) # Get the process context for the current guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getPlayerInfo(self.guild, self.ctx) playlist = processInfo.getPlaylist() process = processInfo.getProcess() @@ -74,7 +73,7 @@ class PlayHandler(AbstractHandler): playlist.add_song(song) # Release the acquired Lock processLock.release() - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() playCommand = VCommands(VCommandsType.PLAY, None) queue.put(playCommand) else: @@ -106,7 +105,7 @@ class PlayHandler(AbstractHandler): async def __downloadSongsAndStore(self, songs: List[Song], processInfo: ProcessInfo) -> None: playlist = processInfo.getPlaylist() - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() playCommand = VCommands(VCommandsType.PLAY, None) # Trigger a task for each song to be downloaded tasks: List[asyncio.Task] = [] @@ -115,7 +114,7 @@ class PlayHandler(AbstractHandler): tasks.append(task) # In the original order, await for the task and then if successfully downloaded add in the playlist - processManager = ProcessManager() + processManager = self.config.getProcessManager() for index, task in enumerate(tasks): await task song = songs[index] diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index 0fb3996..1e25b4c 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -2,7 +2,6 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Config.Exceptions import BadCommandUsage, ImpossibleMove from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot from typing import Union @@ -14,7 +13,7 @@ class PrevHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getPlayerInfo(self.guild, self.ctx) if not processInfo: embed = self.embeds.NOT_PLAYING() @@ -44,7 +43,7 @@ class PrevHandler(AbstractHandler): # Send a prev command, together with the user voice channel prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(prevCommand) return HandlerResponse(self.ctx) diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index e1c0736..db5ebb2 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -3,7 +3,6 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Music.Downloader import Downloader from Utils.Utils import Utils -from Parallelism.ProcessManager import ProcessManager from Music.VulkanBot import VulkanBot from typing import Union from discord import Interaction @@ -16,7 +15,7 @@ class QueueHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Retrieve the process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: # If no process return empty list embed = self.embeds.EMPTY_QUEUE() diff --git a/Handlers/RemoveHandler.py b/Handlers/RemoveHandler.py index d0a6e23..b55d61a 100644 --- a/Handlers/RemoveHandler.py +++ b/Handlers/RemoveHandler.py @@ -4,7 +4,6 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired from Music.Playlist import Playlist -from Parallelism.ProcessManager import ProcessManager from Music.VulkanBot import VulkanBot from typing import Union from discord import Interaction @@ -16,7 +15,7 @@ class RemoveHandler(AbstractHandler): async def run(self, position: str) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: # Clear the playlist diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py index de800f7..272f02c 100644 --- a/Handlers/ResetHandler.py +++ b/Handlers/ResetHandler.py @@ -1,7 +1,6 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot from typing import Union @@ -14,11 +13,11 @@ class ResetHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: command = VCommands(VCommandsType.RESET, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/ResumeHandler.py b/Handlers/ResumeHandler.py index 924a3b8..da40bc0 100644 --- a/Handlers/ResumeHandler.py +++ b/Handlers/ResumeHandler.py @@ -1,7 +1,6 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot from typing import Union @@ -13,12 +12,12 @@ class ResumeHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Send Resume command to be execute by player process command = VCommands(VCommandsType.RESUME, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py index dda23e2..3f38b8b 100644 --- a/Handlers/ShuffleHandler.py +++ b/Handlers/ShuffleHandler.py @@ -2,7 +2,6 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import UnknownError -from Parallelism.ProcessManager import ProcessManager from Music.VulkanBot import VulkanBot from typing import Union from discord import Interaction @@ -13,7 +12,7 @@ class ShuffleHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: try: diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index 42b0143..c5f1e90 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -3,7 +3,6 @@ from Handlers.AbstractHandler import AbstractHandler from Config.Exceptions import BadCommandUsage from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from typing import Union from discord import Interaction @@ -14,7 +13,7 @@ class SkipHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Verify if there is a running process playlist = processInfo.getPlaylist() @@ -25,7 +24,7 @@ class SkipHandler(AbstractHandler): # Send a command to the player process to skip the music command = VCommands(VCommandsType.SKIP, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Handlers/StopHandler.py b/Handlers/StopHandler.py index 68ae620..89c9e09 100644 --- a/Handlers/StopHandler.py +++ b/Handlers/StopHandler.py @@ -2,7 +2,6 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot -from Parallelism.ProcessManager import ProcessManager from Parallelism.Commands import VCommands, VCommandsType from typing import Union from discord import Interaction @@ -13,12 +12,12 @@ class StopHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = ProcessManager() + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Send command to player process stop command = VCommands(VCommandsType.STOP, None) - queue = processInfo.getQueue() + queue = processInfo.getQueueToPlayer() queue.put(command) return HandlerResponse(self.ctx) diff --git a/Music/MessagesController.py b/Music/MessagesController.py new file mode 100644 index 0000000..5082721 --- /dev/null +++ b/Music/MessagesController.py @@ -0,0 +1,63 @@ +from typing import List +from discord import Embed, Message +from Music.VulkanBot import VulkanBot +from Parallelism.ProcessInfo import ProcessInfo +from Config.Configs import VConfigs +from Config.Messages import Messages +from Music.Song import Song +from Config.Embeds import VEmbeds +from UI.Views.PlayerView import PlayerView + + +class MessagesController: + def __init__(self, bot: VulkanBot) -> None: + self.__bot = bot + self.__previousMessages = [] + self.__configs = VConfigs() + self.__messages = Messages() + self.__embeds = VEmbeds() + + async def sendNowPlaying(self, processInfo: ProcessInfo, song: Song) -> None: + # Get the lock of the playlist + print('Entrei') + playlistLock = processInfo.getLock() + playlist = processInfo.getPlaylist() + with playlistLock: + print('A') + if playlist.isLoopingOne(): + title = self.__messages.ONE_SONG_LOOPING + else: + title = self.__messages.SONG_PLAYING + + embed = self.__embeds.SONG_INFO(song.info, title) + view = PlayerView(self.__bot) + channel = processInfo.getTextChannel() + + await self.__deletePreviousNPMessages() + await channel.send(embed=embed, view=view) + self.__previousMessages.append(await self.__getSendedMessage()) + + async def __deletePreviousNPMessages(self) -> None: + for message in self.__previousMessages: + try: + await message.delete() + except: + pass + self.__previousMessages.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/Commands.py b/Parallelism/Commands.py index d6c8baa..9a18a09 100644 --- a/Parallelism/Commands.py +++ b/Parallelism/Commands.py @@ -11,6 +11,9 @@ class VCommandsType(Enum): PLAY = 'Play' STOP = 'Stop' RESET = 'Reset' + NOW_PLAYING = 'Now Playing' + TERMINATE = 'Terminate' + SLEEPING = 'Sleeping' class VCommands: diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index 4dcbd90..d356615 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -1,9 +1,9 @@ import asyncio from Music.VulkanInitializer import VulkanInitializer from discord import User, Member, Message, Embed -from asyncio import AbstractEventLoop, Semaphore -from multiprocessing import Process, Queue, RLock -from threading import Lock, Thread +from asyncio import AbstractEventLoop, Semaphore, Queue +from multiprocessing import Process, RLock, Lock +from threading import Thread from typing import Callable, List from discord import Guild, FFmpegPCMAudio, VoiceChannel, TextChannel from Music.Playlist import Playlist @@ -31,7 +31,7 @@ class TimeoutClock: class PlayerProcess(Process): """Process that will play songs, receive commands from the main process by a Queue""" - def __init__(self, name: str, playlist: Playlist, lock: Lock, queue: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None: + def __init__(self, name: str, playlist: Playlist, lock: Lock, queueToReceive: Queue, queueToSend: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None: """ Start a new process that will have his own bot instance Due to pickle serialization, no objects are stored, the values initialization are being made in the run method @@ -40,7 +40,8 @@ class PlayerProcess(Process): # Synchronization objects self.__playlist: Playlist = playlist self.__playlistLock: Lock = lock - self.__queue: Queue = queue + self.__queueReceive: Queue = queueToReceive + self.__queueSend: Queue = queueToSend self.__semStopPlaying: Semaphore = None self.__loop: AbstractEventLoop = None # Discord context ID @@ -96,8 +97,7 @@ class PlayerProcess(Process): # 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() + self.__loop.create_task(self.__commandsReceiver()) # Start a Task to play songs self.__loop.create_task(self.__playPlaylistSongs()) @@ -146,8 +146,10 @@ class PlayerProcess(Process): self.__timer.cancel() self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) - await self.__deletePrevNowPlaying() - await self.__showNowPlaying() + nowPlayingCommand = VCommands(VCommandsType.NOW_PLAYING, song) + await self.__queueSend.put(nowPlayingCommand) + # await self.__deletePrevNowPlaying() + # await self.__showNowPlaying() except Exception as e: print(f'[ERROR IN PLAY SONG] -> {e}, {type(e)}') self.__playNext(None) @@ -190,12 +192,11 @@ class PlayerProcess(Process): self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') - def __commandsReceiver(self) -> None: + async def __commandsReceiver(self) -> None: while True: - command: VCommands = self.__queue.get() + command: VCommands = await self.__queueReceive.get() type = command.getType() args = command.getArgs() - print(f'{self.name} received command {type}') try: self.__playerLock.acquire() @@ -206,13 +207,13 @@ class PlayerProcess(Process): elif type == VCommandsType.SKIP: self.__skip() elif type == VCommandsType.PLAY: - asyncio.run_coroutine_threadsafe(self.__playPlaylistSongs(), self.__loop) + await self.__playPlaylistSongs() elif type == VCommandsType.PREV: - asyncio.run_coroutine_threadsafe(self.__playPrev(args), self.__loop) + await self.__playPrev(args) elif type == VCommandsType.RESET: - asyncio.run_coroutine_threadsafe(self.__reset(), self.__loop) + await self.__reset() elif type == VCommandsType.STOP: - asyncio.run_coroutine_threadsafe(self.__stop(), self.__loop) + await self.__stop() else: print(f'[ERROR] -> Unknown Command Received: {command}') except Exception as e: diff --git a/Parallelism/ProcessInfo.py b/Parallelism/ProcessInfo.py index 0f8bf83..011f638 100644 --- a/Parallelism/ProcessInfo.py +++ b/Parallelism/ProcessInfo.py @@ -1,4 +1,5 @@ from multiprocessing import Process, Queue, Lock +from discord import TextChannel from Music.Playlist import Playlist @@ -7,11 +8,13 @@ class ProcessInfo: Class to store the reference to all structures to maintain a player process """ - def __init__(self, process: Process, queue: Queue, playlist: Playlist, lock: Lock) -> None: + def __init__(self, process: Process, queueToPlayer: Queue, queueToMain: Queue, playlist: Playlist, lock: Lock, textChannel: TextChannel) -> None: self.__process = process - self.__queue = queue + self.__queueToPlayer = queueToPlayer + self.__queueToMain = queueToMain self.__playlist = playlist self.__lock = lock + self.__textChannel = textChannel def setProcess(self, newProcess: Process) -> None: self.__process = newProcess @@ -19,11 +22,17 @@ class ProcessInfo: def getProcess(self) -> Process: return self.__process - def getQueue(self) -> Queue: - return self.__queue + def getQueueToPlayer(self) -> Queue: + return self.__queueToPlayer + + def getQueueToMain(self) -> Queue: + return self.__queueToMain def getPlaylist(self) -> Playlist: return self.__playlist def getLock(self) -> Lock: return self.__lock + + def getTextChannel(self) -> TextChannel: + return self.__textChannel diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index a0e1e4b..09cb929 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -1,13 +1,21 @@ -from multiprocessing import Queue, Lock +from asyncio import Queue, Task +import asyncio +from multiprocessing import Lock from multiprocessing.managers import BaseManager, NamespaceProxy -from typing import Dict, Union +from queue import Empty +from threading import Thread +from typing import Dict, Tuple, Union from Config.Singleton import Singleton from discord import Guild, Interaction from discord.ext.commands import Context +from Music.MessagesController import MessagesController +from Music.Song import Song from Parallelism.PlayerProcess import PlayerProcess from Music.Playlist import Playlist from Parallelism.ProcessInfo import ProcessInfo from Parallelism.Commands import VCommands, VCommandsType +from Music.VulkanBot import VulkanBot +from Tests.LoopRunner import LoopRunner class ProcessManager(Singleton): @@ -16,12 +24,16 @@ class ProcessManager(Singleton): Deal with the creation of shared memory """ - def __init__(self) -> None: + def __init__(self, bot: VulkanBot = None) -> None: if not super().created: + self.__bot = bot VManager.register('Playlist', Playlist) self.__manager = VManager() self.__manager.start() self.__playersProcess: Dict[Guild, ProcessInfo] = {} + # self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {} + self.__playersListeners: Dict[Guild, Task] = {} + self.__playersMessages: Dict[Guild, MessagesController] = {} def setPlayerInfo(self, guild: Guild, info: ProcessInfo): self.__playersProcess[guild.id] = info @@ -37,11 +49,11 @@ class ProcessManager(Singleton): return self.__playersProcess[guild.id] if guild.id not in self.__playersProcess.keys(): - self.__playersProcess[guild.id] = self.__createProcessInfo(context) + self.__playersProcess[guild.id] = self.__createProcessInfo(guild, context) else: # If the process has ended create a new one if not self.__playersProcess[guild.id].getProcess().is_alive(): - self.__playersProcess[guild.id] = self.__recreateProcess(context) + self.__playersProcess[guild.id] = self.__recreateProcess(guild, context) return self.__playersProcess[guild.id] except Exception as e: @@ -53,11 +65,11 @@ class ProcessManager(Singleton): return None # Recreate the process keeping the playlist - newProcessInfo = self.__recreateProcess(context) + newProcessInfo = self.__recreateProcess(guild, context) newProcessInfo.getProcess().start() # Start the process # Send a command to start the play again playCommand = VCommands(VCommandsType.PLAY) - newProcessInfo.getQueue().put(playCommand) + newProcessInfo.getQueueToPlayer().put(playCommand) self.__playersProcess[guild.id] = newProcessInfo def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo: @@ -67,7 +79,7 @@ class ProcessManager(Singleton): return self.__playersProcess[guild.id] - def __createProcessInfo(self, context: Context) -> ProcessInfo: + def __createProcessInfo(self, guild: Guild, context: Context) -> ProcessInfo: guildID: int = context.guild.id textID: int = context.channel.id voiceID: int = context.author.voice.channel.id @@ -75,14 +87,25 @@ class ProcessManager(Singleton): playlist: Playlist = self.__manager.Playlist() lock = Lock() - queue = Queue() - process = PlayerProcess(context.guild.name, playlist, lock, queue, - guildID, textID, voiceID, authorID) - processInfo = ProcessInfo(process, queue, playlist, lock) + queueToListen = Queue() + queueToSend = Queue() + process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, + queueToListen, guildID, textID, voiceID, authorID) + processInfo = ProcessInfo(process, queueToSend, queueToListen, + playlist, lock, context.channel) + + task = asyncio.create_task(self.__listenToCommands(queueToListen, guild)) + # Create a Thread to listen for the queue coming from the Player Process + # thread = Thread(target=self.__listenToCommands, args=(queueToListen, guild), daemon=True) + self.__playersListeners[guildID] = task + # thread.start() + + # Create a Message Controller for this player + self.__playersMessages[guildID] = MessagesController(self.__bot) return processInfo - def __recreateProcess(self, context: Context) -> ProcessInfo: + def __recreateProcess(self, guild: Guild, context: Context) -> ProcessInfo: """Create a new process info using previous playlist""" guildID: int = context.guild.id textID: int = context.channel.id @@ -91,14 +114,78 @@ class ProcessManager(Singleton): playlist: Playlist = self.__playersProcess[guildID].getPlaylist() lock = Lock() - queue = Queue() + queueToListen = Queue() + queueToSend = Queue() + process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, + queueToListen, guildID, textID, voiceID, authorID) + processInfo = ProcessInfo(process, queueToSend, queueToListen, playlist, lock) - process = PlayerProcess(context.guild.name, playlist, lock, queue, - guildID, textID, voiceID, authorID) - processInfo = ProcessInfo(process, queue, playlist, lock) + task = asyncio.create_task(self.__listenToCommands(queueToListen, guild)) + # Create a Thread to listen for the queue coming from the Player Process + # thread = Thread(target=self.__listenToCommands, args=(queueToListen, guild), daemon=True) + self.__playersListeners[guildID] = task + # thread.start() + + # Create a Message Controller for this player + self.__playersMessages[guildID] = MessagesController(self.__bot) return processInfo + async def __listenToCommands(self, queue: Queue, guild: Guild) -> None: + shouldEnd = False + guildID = guild.id + while not shouldEnd: + shouldEnd = self.__playersListeners[guildID][1] + try: + print('Esperando') + command: VCommands = await queue.get() + commandType = command.getType() + args = command.getArgs() + + print(f'Process {guild.name} sended command {commandType}') + if commandType == VCommandsType.NOW_PLAYING: + print('Aqui dentro') + await self.__showNowPlaying(args, guildID) + elif commandType == VCommandsType.TERMINATE: + # Delete the process elements and return, to finish task + self.__terminateProcess() + return + elif commandType == VCommandsType.SLEEPING: + # The process might be used again + self.__sleepingProcess() + return + else: + print(f'[ERROR] -> Unknown Command Received from Process: {commandType}') + except Empty: + continue + except Exception as e: + print(e) + print(f'[ERROR IN LISTENING PROCESS] -> {guild.name} - {e}') + + def __terminateProcess(self, guildID: int) -> None: + # Delete all structures associated with the Player + del self.__playersProcess[guildID] + del self.__playersMessages[guildID] + threadListening = self.__playersListeners[guildID] + threadListening._stop() + del self.__playersListeners[guildID] + + def __sleepingProcess(self, guildID: int) -> None: + # Disable all process structures, except Playlist + queue1 = self.__playersProcess[guildID].getQueueToMain() + queue2 = self.__playersProcess[guildID].getQueueToPlayer() + queue1.close() + queue1.join_thread() + queue2.close() + queue2.join_thread() + + async def __showNowPlaying(self, guildID: int, song: Song) -> None: + messagesController = self.__playersMessages[guildID] + processInfo = self.__playersProcess[guildID] + print('Aq1') + await messagesController.sendNowPlaying(processInfo, song) + print('Aq2') + class VManager(BaseManager): pass diff --git a/UI/Views/PlayerView.py b/UI/Views/PlayerView.py index 0892462..aca753c 100644 --- a/UI/Views/PlayerView.py +++ b/UI/Views/PlayerView.py @@ -1,4 +1,3 @@ -from typing import Optional from discord.ui import View from Config.Emojis import VEmojis from UI.Buttons.PauseButton import PauseButton From c5885f30938eb396e272aabf65a12f951c8ad464 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Thu, 28 Jul 2022 11:32:04 -0300 Subject: [PATCH 8/9] Finishing work with the second queue --- Config/Configs.py | 2 +- Handlers/ClearHandler.py | 1 - Music/MessagesController.py | 36 ++++++------- Parallelism/PlayerProcess.py | 96 ++++++++++++++--------------------- Parallelism/ProcessManager.py | 57 +++++++++------------ UI/Views/PlayerView.py | 13 ++++- 6 files changed, 94 insertions(+), 111 deletions(-) diff --git a/Config/Configs.py b/Config/Configs.py index b7120e4..06efea9 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -18,7 +18,7 @@ class VConfigs(Singleton): self.CLEANER_MESSAGES_QUANT = 5 self.ACQUIRE_LOCK_TIMEOUT = 10 self.COMMANDS_PATH = 'DiscordCogs' - self.VC_TIMEOUT = 600 + self.VC_TIMEOUT = 300 self.MAX_PLAYLIST_LENGTH = 50 self.MAX_PLAYLIST_FORCED_LENGTH = 5 diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index a151d83..935725f 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -22,7 +22,6 @@ class ClearHandler(AbstractHandler): if acquired: playlist.clear() processLock.release() - processLock.release() return HandlerResponse(self.ctx) else: processManager.resetProcess(self.guild, self.ctx) diff --git a/Music/MessagesController.py b/Music/MessagesController.py index 5082721..0e8567a 100644 --- a/Music/MessagesController.py +++ b/Music/MessagesController.py @@ -1,5 +1,5 @@ from typing import List -from discord import Embed, Message +from discord import Embed, Message, TextChannel from Music.VulkanBot import VulkanBot from Parallelism.ProcessInfo import ProcessInfo from Config.Configs import VConfigs @@ -19,23 +19,25 @@ class MessagesController: async def sendNowPlaying(self, processInfo: ProcessInfo, song: Song) -> None: # Get the lock of the playlist - print('Entrei') - playlistLock = processInfo.getLock() playlist = processInfo.getPlaylist() - with playlistLock: - print('A') - if playlist.isLoopingOne(): - title = self.__messages.ONE_SONG_LOOPING - else: - title = self.__messages.SONG_PLAYING + if playlist.isLoopingOne(): + title = self.__messages.ONE_SONG_LOOPING + else: + title = self.__messages.SONG_PLAYING - embed = self.__embeds.SONG_INFO(song.info, title) - view = PlayerView(self.__bot) - channel = processInfo.getTextChannel() + # Create View and Embed + embed = self.__embeds.SONG_INFO(song.info, title) + view = PlayerView(self.__bot) + channel = processInfo.getTextChannel() + # Delete the previous and send the message + await self.__deletePreviousNPMessages() + await channel.send(embed=embed, view=view) - await self.__deletePreviousNPMessages() - await channel.send(embed=embed, view=view) - self.__previousMessages.append(await self.__getSendedMessage()) + # Get the sended message + sendedMessage = await self.__getSendedMessage(channel) + # Set the message witch contains the view + view.set_message(message=sendedMessage) + self.__previousMessages.append(sendedMessage) async def __deletePreviousNPMessages(self) -> None: for message in self.__previousMessages: @@ -45,9 +47,9 @@ class MessagesController: pass self.__previousMessages.clear() - async def __getSendedMessage(self) -> Message: + async def __getSendedMessage(self, channel: TextChannel) -> Message: stringToIdentify = 'Uploader:' - last_messages: List[Message] = await self.__textChannel.history(limit=5).flatten() + last_messages: List[Message] = await channel.history(limit=5).flatten() for message in last_messages: try: diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index d356615..d47aff0 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -1,8 +1,8 @@ import asyncio from Music.VulkanInitializer import VulkanInitializer -from discord import User, Member, Message, Embed +from discord import User, Member, Message from asyncio import AbstractEventLoop, Semaphore, Queue -from multiprocessing import Process, RLock, Lock +from multiprocessing import Process, RLock, Lock, Queue from threading import Thread from typing import Callable, List from discord import Guild, FFmpegPCMAudio, VoiceChannel, TextChannel @@ -97,7 +97,8 @@ class PlayerProcess(Process): # Start the timeout function self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) # Thread that will receive commands to be executed in this Process - self.__loop.create_task(self.__commandsReceiver()) + self.__commandsReceiver = Thread(target=self.__commandsReceiver, daemon=True) + self.__commandsReceiver.start() # Start a Task to play songs self.__loop.create_task(self.__playPlaylistSongs()) @@ -147,9 +148,7 @@ class PlayerProcess(Process): self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) nowPlayingCommand = VCommands(VCommandsType.NOW_PLAYING, song) - await self.__queueSend.put(nowPlayingCommand) - # await self.__deletePrevNowPlaying() - # await self.__showNowPlaying() + self.__queueSend.put(nowPlayingCommand) except Exception as e: print(f'[ERROR IN PLAY SONG] -> {e}, {type(e)}') self.__playNext(None) @@ -171,6 +170,11 @@ class PlayerProcess(Process): self.__playlist.loop_off() self.__playingSong = None self.__playing = False + # Send a command to the main process put this one to sleep + sleepCommand = VCommands(VCommandsType.SLEEPING) + self.__queueSend.put(sleepCommand) + # Release the semaphore to finish the process + self.__semStopPlaying.release() async def __playPrev(self, voiceChannelID: int) -> None: with self.__playlistLock: @@ -192,9 +196,9 @@ class PlayerProcess(Process): self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') - async def __commandsReceiver(self) -> None: + def __commandsReceiver(self) -> None: while True: - command: VCommands = await self.__queueReceive.get() + command: VCommands = self.__queueReceive.get() type = command.getType() args = command.getArgs() @@ -207,13 +211,13 @@ class PlayerProcess(Process): elif type == VCommandsType.SKIP: self.__skip() elif type == VCommandsType.PLAY: - await self.__playPlaylistSongs() + asyncio.run_coroutine_threadsafe(self.__playPlaylistSongs(), self.__loop) elif type == VCommandsType.PREV: - await self.__playPrev(args) + asyncio.run_coroutine_threadsafe(self.__playPrev(args), self.__loop) elif type == VCommandsType.RESET: - await self.__reset() + asyncio.run_coroutine_threadsafe(self.__reset(), self.__loop) elif type == VCommandsType.STOP: - await self.__stop() + asyncio.run_coroutine_threadsafe(self.__stop(), self.__loop) else: print(f'[ERROR] -> Unknown Command Received: {command}') except Exception as e: @@ -242,9 +246,11 @@ class PlayerProcess(Process): if self.__guild.voice_client is not None: if self.__guild.voice_client.is_connected(): with self.__playlistLock: - self.__playlist.clear() self.__playlist.loop_off() + # Send a command to the main process put this to sleep + sleepCommand = VCommands(VCommandsType.SLEEPING) + self.__queueSend.put(sleepCommand) self.__guild.voice_client.stop() self.__playingSong = None await self.__guild.voice_client.disconnect() @@ -291,20 +297,35 @@ class PlayerProcess(Process): return if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused(): - self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) + if not self.__isBotAloneInChannel(): # If bot is not alone continue to play + self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) + return - elif self.__guild.voice_client.is_connected(): + # Finish the process + if self.__guild.voice_client.is_connected(): with self.__playerLock: with self.__playlistLock: - self.__playlist.clear() self.__playlist.loop_off() self.__playing = False await self.__guild.voice_client.disconnect() + # Send command to main process to finish this one + sleepCommand = VCommands(VCommandsType.SLEEPING) + self.__queueSend.put(sleepCommand) # Release semaphore to finish process self.__semStopPlaying.release() except Exception as e: print(f'[Error in Timeout] -> {e}') + def __isBotAloneInChannel(self) -> bool: + try: + if len(self.__guild.voice_client.channel.members) <= 1: + return True + else: + return False + except Exception as e: + print(f'[ERROR IN CHECK BOT ALONE] -> {e}') + return False + async def __ensureDiscordConnection(self, bot: VulkanBot) -> None: """Await in this point until connection to discord is established""" guild = None @@ -325,46 +346,3 @@ class PlayerProcess(Process): for member in guild_members: if member.id == self.__bot.user.id: return member - - async def __showNowPlaying(self) -> None: - # Get the lock of the playlist - with self.__playlistLock: - if not self.__playing or self.__playingSong is None: - embed = self.__embeds.NOT_PLAYING() - await self.__textChannel.send(embed=embed) - return - - if self.__playlist.isLoopingOne(): - title = self.__messages.ONE_SONG_LOOPING - else: - title = self.__messages.SONG_PLAYING - - info = self.__playingSong.info - embed = self.__embeds.SONG_INFO(info, title) - await self.__textChannel.send(embed=embed) - self.__messagesToDelete.append(await self.__getSendedMessage()) - - async def __deletePrevNowPlaying(self) -> None: - for message in self.__messagesToDelete: - try: - await message.delete() - except: - pass - self.__messagesToDelete.clear() - - async def __getSendedMessage(self) -> Message: - stringToIdentify = 'Uploader:' - last_messages: List[Message] = await self.__textChannel.history(limit=5).flatten() - - for message in last_messages: - try: - if message.author == self.__bot.user: - if len(message.embeds) > 0: - embed: Embed = message.embeds[0] - if len(embed.fields) > 0: - if embed.fields[0].name == stringToIdentify: - return message - - except Exception as e: - print(f'DEVELOPER NOTE -> Error cleaning messages {e}') - continue diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index 09cb929..9a1fdde 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -1,6 +1,5 @@ -from asyncio import Queue, Task import asyncio -from multiprocessing import Lock +from multiprocessing import Lock, Queue from multiprocessing.managers import BaseManager, NamespaceProxy from queue import Empty from threading import Thread @@ -15,7 +14,6 @@ from Music.Playlist import Playlist from Parallelism.ProcessInfo import ProcessInfo from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot -from Tests.LoopRunner import LoopRunner class ProcessManager(Singleton): @@ -31,8 +29,7 @@ class ProcessManager(Singleton): self.__manager = VManager() self.__manager.start() self.__playersProcess: Dict[Guild, ProcessInfo] = {} - # self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {} - self.__playersListeners: Dict[Guild, Task] = {} + self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {} self.__playersMessages: Dict[Guild, MessagesController] = {} def setPlayerInfo(self, guild: Guild, info: ProcessInfo): @@ -94,11 +91,11 @@ class ProcessManager(Singleton): processInfo = ProcessInfo(process, queueToSend, queueToListen, playlist, lock, context.channel) - task = asyncio.create_task(self.__listenToCommands(queueToListen, guild)) - # Create a Thread to listen for the queue coming from the Player Process - # thread = Thread(target=self.__listenToCommands, args=(queueToListen, guild), daemon=True) - self.__playersListeners[guildID] = task - # thread.start() + # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async + thread = Thread(target=self.__listenToCommands, + args=(queueToListen, guild), daemon=True) + self.__playersListeners[guildID] = (thread, False) + thread.start() # Create a Message Controller for this player self.__playersMessages[guildID] = MessagesController(self.__bot) @@ -118,48 +115,46 @@ class ProcessManager(Singleton): queueToSend = Queue() process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, queueToListen, guildID, textID, voiceID, authorID) - processInfo = ProcessInfo(process, queueToSend, queueToListen, playlist, lock) + processInfo = ProcessInfo(process, queueToSend, queueToListen, + playlist, lock, context.channel) - task = asyncio.create_task(self.__listenToCommands(queueToListen, guild)) - # Create a Thread to listen for the queue coming from the Player Process - # thread = Thread(target=self.__listenToCommands, args=(queueToListen, guild), daemon=True) - self.__playersListeners[guildID] = task - # thread.start() - - # Create a Message Controller for this player - self.__playersMessages[guildID] = MessagesController(self.__bot) + # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async + thread = Thread(target=self.__listenToCommands, + args=(queueToListen, guild), daemon=True) + self.__playersListeners[guildID] = (thread, False) + thread.start() return processInfo - async def __listenToCommands(self, queue: Queue, guild: Guild) -> None: - shouldEnd = False + def __listenToCommands(self, queue: Queue, guild: Guild) -> None: guildID = guild.id - while not shouldEnd: + while True: shouldEnd = self.__playersListeners[guildID][1] + if shouldEnd: + break + try: - print('Esperando') - command: VCommands = await queue.get() + command: VCommands = queue.get(timeout=5) commandType = command.getType() args = command.getArgs() print(f'Process {guild.name} sended command {commandType}') if commandType == VCommandsType.NOW_PLAYING: - print('Aqui dentro') - await self.__showNowPlaying(args, guildID) + asyncio.run_coroutine_threadsafe(self.showNowPlaying( + guild.id, args), self.__bot.loop) elif commandType == VCommandsType.TERMINATE: # Delete the process elements and return, to finish task - self.__terminateProcess() + self.__terminateProcess(guildID) return elif commandType == VCommandsType.SLEEPING: # The process might be used again - self.__sleepingProcess() + self.__sleepingProcess(guildID) return else: print(f'[ERROR] -> Unknown Command Received from Process: {commandType}') except Empty: continue except Exception as e: - print(e) print(f'[ERROR IN LISTENING PROCESS] -> {guild.name} - {e}') def __terminateProcess(self, guildID: int) -> None: @@ -179,12 +174,10 @@ class ProcessManager(Singleton): queue2.close() queue2.join_thread() - async def __showNowPlaying(self, guildID: int, song: Song) -> None: + async def showNowPlaying(self, guildID: int, song: Song) -> None: messagesController = self.__playersMessages[guildID] processInfo = self.__playersProcess[guildID] - print('Aq1') await messagesController.sendNowPlaying(processInfo, song) - print('Aq2') class VManager(BaseManager): diff --git a/UI/Views/PlayerView.py b/UI/Views/PlayerView.py index aca753c..cb6050e 100644 --- a/UI/Views/PlayerView.py +++ b/UI/Views/PlayerView.py @@ -1,3 +1,4 @@ +from discord import Message from discord.ui import View from Config.Emojis import VEmojis from UI.Buttons.PauseButton import PauseButton @@ -15,9 +16,10 @@ emojis = VEmojis() class PlayerView(View): - def __init__(self, bot: VulkanBot, timeout: float = 180): + def __init__(self, bot: VulkanBot, timeout: float = 6000): super().__init__(timeout=timeout) self.__bot = bot + self.__message: Message = None self.add_item(BackButton(self.__bot)) self.add_item(PauseButton(self.__bot)) self.add_item(PlayButton(self.__bot)) @@ -27,3 +29,12 @@ class PlayerView(View): self.add_item(LoopOneButton(self.__bot)) self.add_item(LoopOffButton(self.__bot)) self.add_item(LoopAllButton(self.__bot)) + + async def on_timeout(self) -> None: + # Disable all itens and, if has the message, edit it + self.disable_all_items() + if self.__message is not None and isinstance(self.__message, Message): + await self.__message.edit(view=self) + + def set_message(self, message: Message) -> None: + self.__message = message From 7f1ffb6b230ec0d8e056915b39f8399b22d3281d Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Fri, 29 Jul 2022 00:41:03 -0300 Subject: [PATCH 9/9] Fixing erros with buttons handlers and updating README --- .gitignore | 1 - Assets/playermenu.jfif | Bin 0 -> 34528 bytes Assets/vulkan-logo.png | Bin 0 -> 9753 bytes Assets/vulkancommands.jfif | Bin 0 -> 76559 bytes DiscordCogs/MusicCog.py | 6 --- Handlers/PlayHandler.py | 4 +- Handlers/PrevHandler.py | 12 ++--- Handlers/SkipHandler.py | 13 ++++- Parallelism/ProcessManager.py | 21 ++++---- README.md | 97 +++++++++++++--------------------- UI/Views/PlayerView.py | 9 ++-- 11 files changed, 72 insertions(+), 91 deletions(-) create mode 100644 Assets/playermenu.jfif create mode 100644 Assets/vulkan-logo.png create mode 100644 Assets/vulkancommands.jfif diff --git a/.gitignore b/.gitignore index 40aa9d2..71d50e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .vscode -assets/ __pycache__ .env .cache diff --git a/Assets/playermenu.jfif b/Assets/playermenu.jfif new file mode 100644 index 0000000000000000000000000000000000000000..bd102078e353fe830109d2589f96fc15b6369991 GIT binary patch literal 34528 zcmeFYWmH^Sw;)=$yCt|3Zh_zuT&nQG-GaMYAOv?WBuH>~_u%djT!Itao#b)8+h3p4 zqr1oLe&fD>Z|+fZ%~~#V@3p3^KZ}2U04P`RmWs`L*%a&VGEQ@h$#?_WX-B`42kkFWOlRBJr9h`;}&}_y=wB58A`k z(e*XY&|mpY9i9K8S6^u%J6pHE+WHIr>M^>Ry}IgasP+0I1-Jkp02zSzEB#*zf6u2w zE&#xH0|3Cq{T*kL1^_e#0|0N9|BeG@0{~b-06^oUqp`E`zd8f|ieb&o0f5UQ002W9 z0Kl6908n-R-O<0x{=F9eo3T>9mLYu|r^D-G0k8#_0Vn|=fIYwz!1hXU0^R{Q06c$I z01^N=*uUT}4EGA~i12>_G9n@ZA__7pDhe_R3Mx7lIw~3_8VU*q4hAL`HZ~46DmpG6 zE;imPjr~_8Fn^VVgGYWXh>eDV_8R+t34gi(ILI&(u!?Xn)Bso<7&si5KYakQ*X4tS zgL#F2&j0`c5grK{77pdFWG!p}3>-Y*uRf#0!674H0AOHW|05vcAmLKsai}IB;}cSg zO{+kR;|XXu-*fXgIlI)<)`@=!kPOTr0`f|jxOMluF%3$bq2-cN)6jHaIIqpZ!Xf@OUc}d2e=!*>4jdIcE(d~`im_Atmz)|rYR+>+ ze28=R^ybBTuG|>{8u5VI9+QMiB*H(-0Cc#2mB0ar0Ct!lot(P^U)~NUlFPBwW|>vU zWE0G{rz}HD9WvCL<-KgRl&PyR14m_5BcEKo>3`GZ(d@hh2D_KG>tPwRdWnfR{xXm* zlY;j13QDzF>D-Pd8)!`>+$TjJ6m;?a29x={{MduPWn)2Fq(6Wtc`ImFVNRMS1}zHCN#l=Wjq6FWEa=Gv zH|r*0=p-EtMHZ|i>Wx_{Y?BRqWCNKkq7O!gf%S$l27K!5WU-T){aTumwi!utKg@$s zyQaxrm`|$>#W%vcXZnBiaR+1rVKD>4XYYcOo-OSpWVsTWN1#g@RnntUmr@0U>FX$T z+^685V##5(ZS=JHMNV_*2JQ<51)>#Ch82R9 zm`14Oip_y^%nZ!*{GiS+k13{tST`04JuMU(=8!}y>H#q_v7gGO4({ih-}i^kbughP z$@+1ZG}X%)Za?6uP)L|m1OQm!YadCZrkTGkH4ULQo-H7mdr-(B3;h9Tu2Tv7(W{@M zwU|IU&y5sYhUJ-Ec?)E=<`V%^ci@ilZH`TxK^15ClU9E5syNIH-o`riy4=I6y-5g3xAw6sYO1(wt3P@hL)_T@e)1A#Vp63FcPHb9Buz- zLQJ$u9UF6?OkVY5FKMxK>JKVl1J zpJMt|?JuCtC<`GznM)wq+uf&G+nD}d*y&qmz$DbuY;6QrDv3{eBm3+82}L z7Xety+f(x$fB6{3&u0EAOZd|fCK1-y{x0#wS@V@Q&U&tEIl{vc)NgbhV4>|$AU-hb zYAVy?Xw#s!EVJ>%(tFLeb-?)cP0Z7yn@}A#iR{ka#xCU_K;U*~pihOv3inpt7~{V% zbAKCag^OY8(2EHBp!)J3KtY2cxB%-ToB`$P5Votp*+m#93XnKF*6iG;nB zxT(<1!aO;&0C)xO;+&0{jR7}%fLLR%{U*_8D6jH1l(KZ9Fpp*)#!7C-Ay@iZz0>}* z0eaMHlDV&Wgb_#Z&gxVMR4>Ouu<=DR8Cu2b@v0YLn3^QT1+9cU$k zGNHR>G>lBiHqF{G&+i;04iI?c!CKQ-e?OxxB9Ib5Q$w=OsSW(zf^b0eEJ<+HbV!mq}Iq7uG!F6Did4)?(rl(!g8xc_;c|s&Oz5DfxHY_YdwU z12yG?Vh3%>YyIiu9K3HWu>1DjTMbt3-5(v%4g7$FxPK8 z{-8y6+>d<8ffpNKap-zMzJL66W~QdF`?zQE73zQBGmk%j>=`?zsJx(1;e1fBe);OC zg9q7jeXzQb$92G6I}N6r)X zVhlMKgBD`bQDOMun6fFAnC1+ED z-D1Y*HjY7HG@rV2DyzbZ9Hz^PD$We_0}1^{1}-s=i}d|R0Z}uQ-dX?JAIY=UF2BuC z?8bs&gc-wUC^F+0HO2KMnu|nm}SlJ$1Q)0wfti3mMYgF9aU0?Ly(O( zb`%c7pe%IwoRb^JqnO9)!Mjh=uA4FDPB3zZxHHPBl`x{V6FedjDKQa@Ps;?>lduK? z3-nQt+{)Vi!MW&slZL{|xHL&#R#)S8!P=}TgXQkbjS9A4rzbvz$>cM#@7jj;N74OO z{pDN*w9K+hdyGm3mjEu?t=$h7$$FKVKuO8a^*snRNHQcYOBpw*Ihc`oMwA5p&h4nx z{~u$+=jCsGNPBqMZ58lBet1b~)r?uHA|N+b{1=8qt}ze(Yr2d6g>%_mtH}Pr1YsRB z?f>LL>O1hoKi+$W|G)Svr~W;4F!QmZTYisslHi$kg}N5IVZG#}xc)ArxT;WI(VDxP zKLF*5A1Ypi!br4DY#UtIpBalF!g~wnG=Bg%7p2Xz5Ojhn$My(d>a^WV5jDod@|Izv zyg`6O*r9)}x9jQjfsEqsKY$rOV(sdvN&md)Gd+dMar=O)rpDfpIi&3XAgAI7TwL|e0$qVK?n>71c+ZwPn1BL7 z+6nz5??=!}pm`o^@@T>41>p+DFHF4E_-PR<;ojS?t8Wx>=edScI6&S%4_ynehV4Yu zMee^ES9gw2IB_Z#os$OG*;W0fTvlUzr&=H@p~GI)w@-HJ)bfrG09#i(^TrCjkt=xy zH~to;^bV{M(WlxXfE%T_iX8`|D^oP1t0HnyYNpRw^+V3%?EOoFI$M2XSHub2antli z+xL7Hc(<8{Nd?S|wgrq}YDiKp?SE|m;cyxYacejlW5yVe>g1T6S&nF^-8f^xRQYWl zF>d7)60R~=6}W(mlc|bJH)z#|4W+WDWdaFz1$b+YYx;ok#>(Oj(^9oM&>#iLp5C9GQ263~Ky7I;ul7=6s{eW57dT&}{+6uU z_m|dT^U9Q{-odX8_chs#XPf^maXG@1$sts>{k=vwChd359o$;pM`5W3$LV;BI2o(yza$5T41i{c1LxBY3AJn+{ z%Ys($FDfcfy2-uxr@RMcCg^EWfwuT>YzD`>qh(J?GLrq znH7pbhc$=Xp+&<{!85@ha?30=JAK|4?#acC2S*qLRHPF^nM(M4b+zh zysIAQ1KV;|Hqhy?fe+EjT}y<`G(NSFWDXU_gbg*O9`d0KO`GTG;%vN($1BwV7~mcmpNhtDd^j()~L>!{=p z4{yvk+nuoMc8nekd6uj*EjPQ1LZb;ZNy64@1vPZe$%ucahwcKYbnh*1%3D5BZjE=Y zabAc7FS%M0?#)~eD`^9BQpb~m5t?gb)PVWHu(HIIcbYyIy5WSt{%yL?Ufc5=@s9hv zeRy_#99nmN>tMQMUAa%ye*jB!D;c|z7%`6OxnD0*y+s!*g%rj)`kWGGX|P-Z5~#() zB9EB-ch?l)Z)IUVMJBkbj_`E2J~0nZI#v?}+QW0722QRL`(}3>%o30fB$|kRBSazX z*!}}3(Ic?kBqC(jNL_dmLFZh$ZHYjR=HX73D;LUjg(U@}s)2q@&DjMak8}2qI1u)+ zdqBGxa1$Mx+9<0vDkWWlfR|UyYb5iNV)NgXG}8^z>-a24$TbK~66`ht zm5i6YM|mz|oM;&+g9-96JpB1%c^*-iX$@!x8?-qWn@=u0x5=XSHie|@X?6#{6Ym83 zImq}uoCamfKA566|$3i?4t<`B@zpdM=8{vJG0*zwQOK?WLSL7fCzTa;gZ@ z-ssLODG8)IS${K-teIGK-Z*=FF(x6tkZNBkPAhJY%4e&98z6n$tqVQ!H+TFKg))A>-q-Y#U>OAM1<0%}LCd@oTlc3Ia=f>PX;%YO-6lPm9;eIbB?UMcNv)@(k z*jI#^Q(lK)x>>3cJfzxsbw6^ldYlVOO%X8)M}yNi0v%3`XttI zSb~kb)*o5vS#wQ2W3Wq6yR19yoVB2M2lK~ygUHzwl7!p60(@GOeLECK8jw&46NzG1 zmG|ZxS={=CVBY(n2jnd!S{6%JynMXv-isafb~dA?^T0@6PleSAJ+{%pK6i$!)v#L- z8&C6~wgDB=MPBPm>BrzAxvE-PlU z*gyLh;I;=i0vA0$J7%>XPJXyfRg_1KA73Wqflq49a!hcCaka6A2i!GvB>+!D`D?v& zJ+O<}l|I}f@{O7!iDbI{Z2ly$h0*WsesWJIL931Iq~Tg;<7>V}LgF|GBNBr`ras*9 z$p+J$7OGqRYpC+g_%dsW^8(EKte`ELG+FqHxJ}ZIY`_W0;`CzGl;T@&g>ywjl%)MD z^JU`J^9`oXu^!gg_7&&+o|a~?52Bj*5LsC|3KEpkK~y8-`hN1>%`q*)aX-k5weu`i zOnnlVbkl0;{N{Zn=lo$hlWo!!4-c1jKD$2i_!$ighG~zq%{A_aJ6iW@(ivBvTYyx% zb5_u~UOX7mL4cc7fWrt@`M(R83t2$bW_-d;xI^ranzrCJa;)B_4@srM{b)uNSOXA# zF`e+`sfu!jVpuOXy-EFT&&|H9_viG4X&><4sMx;gNWd2i%&S8eK0MC$d0ZYDUQWEW zd=}WZ!_i#|RdL1H>r6aYljf`ty(o_5?)e>ePkrIX{t??$+rDwdq>}OZO?rm&Y;4~i zqg)#e82*#Y%C*M##E;;e27577$f=B}zPOy_(ev-aL%Ok>-5T3ZTDnalDp9-fIo$f$ z#!SE*p~IsG$)qx0{x3+a4Z=rAKBU8DYi-y&bd?FqW+)nyOxX2DzCK@+RDexaq=qE@~^Q+ohj- z7jAKRb>&b3iPdCs>MsS&4RzG!y9`AEzplwNNLTGWgMCs%+O!T+s9JDl;_#D>jQf61 zn0~Lj)togxZpNUTG!l={I~S#nCiLLFZ5nM)eHp-&s8+!6Ew@T%D7K_cK|;WPExBC znhc1qJ52@gtbmwx=9T?%;{+1G{y*$VLsp1S?B(LO7#er(vQCm@IB~b_gNh7m;J;m# z&sX8xgQ7lOu#OQ~Y@E1jE9bk?(VVpQ<#wK)TK<9&UXva!|9XS$$^~LrMrXT=LJ&;a zx;IBN>c0eenQf4?m9CMwlvl}61gWyJ`gaZsk`^=V9d3yNo;^ydSa)T0zUj&2mdt2q z`RM0K-t1`jllC@WlgT)iPmmh%3q?x)<}m;cwtqbEA|lLH)QYD~-IbEI{;Kh$5Qo%P zz_ho}4#m>OxH-=K18^SBZ=7||-xC(@v#*+aXlV3{>}!?|1N9-LY=u4h$*J`TIq}zg2%Mv47e7+*n%hW0vVft;z37 zWJMivBxn1X?7LfMB#rTBmb3OV^_pgxte=a5wR^i-5vQ{Wjq6QAvf`s?GRgpL7^HNW z4BP7`CVHwZ-#-AV0g^uQXoN@`RZVWHWG{WlTtOUR<*rS}%@qB1Ytc+g=XX7oH}0vH z@ka0ywH^m;BE8jnEq#1)Q6vc_TwmJMUznH#Rn|T;g17Zc45wrTkt8)QlMvZ(?1_BY zUZ@S4QZb$0PPmCZlUksC1g^^a85k((qd!i14AyU~sOf2jdv)S{9Of7^xi!|UJqZ8e zW$|O#Wj+-Iq)D@4l2IowW)${0tO1vOT~IWBZ#w33sul@AJo)hH_0ys)-fs46yA|UV zvdHHP-JP5mbPAbTt6YC$Rk(yKW1ir{1#Hfab!9XI1F||453UQifoB3Gt4Ih|n;Up_ zop>853Fl9g?z4{WoSUun^()0*!3QNxByLRNGhm2oetb1nPrhgHz$yI&c+H{CyyYF# zuA#!d_X{DqC5MiaVN&&&`HF3Ebc{*nL40R}TCj2Fs)^u-4YO}d?}4G@=A84($Rt7| za1x?{3!7R57ByY47k0Dvo>fe4B`p(nys*;M_5!amTfST(E0Ibp?#-~!FzT$X}4o0vV<3Ef1C(8a8V3VBt)4^Skj2r3~^E%Ucb@uoV z`&LWUdQKA0Hrnf*y-x~$b_<^ka8J&rC(#Cvqxl-Cv{{_jpJOa`7(*m7x=o>?v{ERb zQ$S{bho06Sz$WWj09KUkya@TCKezE={XvWZX+ZUb^f0?uSDk0>MHtffAh zOx>7AnoO#8=5m1!CH2ClUKWkk={ncv@c?*aA)U9i#PG>2-UhYG3=5PjH7rE}^&nqv`=cem$d?)H;V zs144~VMtOYShxy@=!$<)4DFPGB$f#m@ptGoEAFGK_WDQ=d;IqCQKcPbQuavAFA;TS zz#Gj~Osob}g!^u`ucQ!ItX49nj-|V^h5r0-^}8lJg$&*W7Ls^BpT6F;Z{;I&NI8GW zV!au9{IaZlx5Yh%+fRgvBOwnR zr)shOBvTZkC9>|)+uOIy$2`5Pyd*y&(d?*j6L^*%dO&-q)2NG}!wQDMIr??@Z++PbjOG!Jh{-6WeHDn3ZJA+;CLShS1(XJF=$ zk1PdQ*xS2sP2?%63XJ;kmV{A8TWo>y3OPagby|I;#Sak)-_jIXWts<@b#sL8rS=LK zMfnm|gDCSF6q8_>5F~?bZI5Br^z8I!fX?)afu`FLnx z)ve9~)^aKVFu=gH1bt&CA}EJPukFO1hc0){QCuvJjC_9+n_TqMGL7CJKtfhf1(@vR zM<6?Dq=mEb5PJwSx{~wyXjDHtk)9|5n2|||2>{`7SL(>d_zc{yQgHW4t<<(z8@K zdT3nF=qsUQdEmfhrH+jFw-!{cUE{XX12V`{ioCszi;3hIThFNp1*@42gLcIuuB?6T zI@*G=7+FigpBZd@iYm+IhaI#}!7`=2zItmBv(y6QvUedvXz*~eoplD|Rh$-L&BO8@ z6knX350)4x8Gj^)gZ)L~1n~ig^}Gvh%9SA91SqSR(4k|q=;@MRse0efUnYzqh^rM= zxDG%G^N;bBb^z=SM>UQKD|_MY88%kH@C@Lcqz$iTdY#=Wr)}x{TIVb=Yv%jFPjR^T z$y1LIWQJW@BbeM=KVgA|p1Uxuh2$2Cx(=sf6^xWom5Rf-GY+c;afuA}p0>_py>GVaj+D8)`pN4MQ2pTSUm^li9iDp-B zg*v`YG6zb+(;EvQRVf_lM-Lb=*GGxMj}2o^S$tug)p+doC$xDdD2J+ji~``KBz%@( zX%5xu6JU2X5e3H9w-b{Uj3%6EUJ(no%3D4Ug%mWO+0U#=$Eoqlg1v(oY^orlh_Fvq z?!8{==3aBn%?=d__0b5Hcd;wl9-k_Gcf(aSB5{&;g$Z_PlNhNG1)0oX1XZSXRW_Cp z8`iWm%j(llmesd(NF-*~*eX~9a8Vv%fJe_qf|he#&B;Gk03;;CMDw6mzX_80tjVH! zo%3Ve9kb>;+XM4Bx_zP5Xev38KL8oW?`I_O%<^MQ-B?t{=O( zr?`abKoGSBoNR4XybUHtksFa^MjIEmq}BeQ}XmVKziX>|A*% zg7T-dgwLm@oR{Iu{Vsfqj;oMtUvhME*xwEE2}Lc7VX!_Gq{PbC1hf%vTV)15TP)C)_2K+@l{lE+R{KLVSDDfFV zicM2NdCsc|)w*7;@x5Il1KawjiBRJV6w?SBR=g((Nf`=@IB=LlLgiM>!CHG#y)P4V zHOUnYGx^vTR05x4CDbOXJRhbp@i?#hkn!VPv2w+fvBZsxajqapCt+DnN39{46|ADY zE4u`=w1m;ftAFR3ahR>+g4nnE<+OE52fZ8HORRzEc+4i1;m|pmNFKfO9HD89T{zzf}Wk6M3*m2eK1x-pDz@D;KP6d9!We*;kJ8a z>5?M^8#jryZgPeBwmuj12k?WbIx%y-cb<1HY(zcKpb{1N#BD_6ZC-;%A$fp8AA*SBO`f$Fvl)j>kgoxY{<4B}P869Q zxXzJ8jmQLTu>MZ;dzJ1q`oXv8S3hi*{X@7j=j&K2~2@f9s6C=Y4|8?Z>~;{ouR!U6nZy5Qx;lY#?m zKHVVYnp(2nWb_eI7oBnrQMIJA*D-8AG6BXgUABFK0hEi!jvtqPbf{B&GSr6q)^^!2 z>-$(0LO@W&LNxq^$92f;-qP+UT~^l;+-7gY{{@hx_VVLOL&Ex9B-M?}6JNS}3)cRN z5qrw5^RrJW`E(qK+S}v>JLRH*jK&NnT)8~3LwZuc^3>-6Kl)7_XbryA3F4g9TM5Ol zAoghbu(fWpC^n`_;tD=agBf~HEcm`SuC;frkSLRoE3(Lh${OMn*X9;jpft@dHi3i< zwaZ_O4i|Y8sa}q+^h>lj!x>r&|pArYHi0KFEN+Mtyn7Q61h;8WKz~(R4^foU#70#?g3-icd z3+#9n++`piw?MEn3CQzdYBtuHU=`No%j8J}!z|y!IPfheU~#ZrUb&DRJ;Uk1ZErx1 zEjaVM+t9U7PI^p)!Um`X?WGD{+=GgnHOE}DazrsaY>Xa^4}VH&ri7V(ohIT`i%V1Fk`w>sa31QCoJ0EDW4#iOa3{kR?c5_P3E! z_{#t$*r!fSVc2?36KJE)zBDL*Ga)rmMHW(=pEhYMv%*zen}_Tu32 zCiva)-ATt_WsTP%H5YRmV7&A&XEEg|kpr6D|5GbV)foN_kmcw~ejqfay%&Q@QCCot z>&t!rV+7YeOzz-n=Cret45i6DlRMM9Xpdt{M%#ow}Q|GhVGnIXXZMsojqUU z$ViSKC>%a?U$NNZVNj7OV;cEINe&dmcKIaB?a?%GTO!Pyi3kju5S-LB;yXXmN?uV$ z|I){jsAk@ADVu86tF$c?-8ROBSJh{kvc4iD6sm0&U#wtJPW;@P=N-zbWy}{ z;8R7MLTvw6WcZgdW`coT#6diYsB-58POdgphhe#Rf46?d1zcG-0UgBzF0t|k$6Sd! z(Dc?&swwr(ZYR@nKnnmHx6)!R6EOjH3bi%!3^c_JJ8yYpCwjv1BynjM-{(1#B~hjW z!7l(N5eaf*t0}1w-`7ApObsHK}M zznU|w>%&FD8cI-lR{#v`9rM`FM(8_5qE*gcJWj5yVt|ZB!WQ^Il7Jx}lz1{ki8|$E z96NWHN@rJz=w=<3yBkfLk<1rV#C!+$<_o8ly2wxz_Qspk9NYK9L!rhMJ&40JK1#fX zUJ~B-$OL9Yw5F%qVk~qtwNbf{Xch-uTe1ThW2X7G8G(he5KX5RiA023(}EW1x7y-r z$T#L_tVb4GiFLPkHmxnc>*|(5eHghy`7j(jU87xb7X=PmV~DJNcyebTV-o9hAG_Z8 z|CCN9)iA5VH22Y%Cu>J!<4s~6!fAueXF(`I*>_2GCa6FKyFxwOes~WNH%j-_^oi@5 z{bJ##x;m5v&sQ$>CFn1yz@;S7w@IYu2$cJ{o3gEuQ{&kMT(>zBhP@{PkJbc4Ku`U% zgPlvj5U&JR&=WTS=ic+8Bb7i%uludqs1#)Yq=;~H?`WeT>%{ek=X_OQkz}L)T?XZ| zo|c1W8fNO9361xSIR%*}E}8!Nwjs^t3=>9=2^x3%@C@OrkHesDI2aO^RKO5!nC(eY z+0a`~J;;UKn=JV8A(D8-)}8e-mM-i`kL>2*K{xO1b!xlzyW0z&7=g9^TjFS(w9ZBy zh)G?gMuZnZU4CO+wsZiJ#N5IMy7&oI|F_JgKD1|44Q(G; zr(Q8`DdMP#4*ynzKQ$Sz7G@TRXKcWE7%&)yK21pJPnvLe-a* zBW6f|HLL<^{J9RjD>pqKv-weZ6L?Y!w60*4gt^1J1FXo zJ{OIfsSj?hxwj4dmMd+Jb`>({)T@){TsARfs4Z#1+c0XP{%OjJ<&J_>3JJ{wF_96B z&8&(UX_ zZgwJZ9%XTru>xigfPmbz10Y=d)0%ss&I99UXq$mde@ESiwlgQ};uu>`F6zBYys$Gr zD*lCzmuk%o1UL6Xz+q{$N9%~wpai{92k9}+!iDu;?j@t%s#O(=d<6_;M`TCe_f;^L zH(b(_O;pG7nXi1FyumokW&sGHkdH z%|9OB9H}rvl5-o#ptztX8&aIwtU0c+Tp0RTs`WBRE?R8<>3F-ikmhTsmfs<%KQwZt9Xw(u_6; z8@-X_Jsmnt^9LnzNRFKUoJAaeavE-$MUo4CG_)9|R=}a%jfBj~wwJxG5-sc+fir9- z5f_~4hT+WLfe%{?n^N(VXK`GjX7=VfeO<~Ud1YMKIjiK|4nItZwN=8_oU*QyUbqTE z2XkeMEZI|edWe3M!5)+3l}l9Ef<>h~Z6S%aFDBv_@!M+boFxnf7Yv5$U%7cfOTpt_ zelKY0PG7l4sAuFO57yL3{pK@b3P0cgIp%?I#_8+0a<(6_LD=j#PJMbed6<*rs%3aZ zGMKMI*Mj*V;|4#C6r;wR`bbuuA#uPrScO`hO?fa}F(69@vgImD84OH{6GO@=vl^>7 z9kR0>%LEi|x+Lk#OMgRX5e@_6FVExV6wcveDdy?@fJ;)U*RwBFNTw*O*0E!rW&Fr@ zAd9AD2P~*ox)-DT+|%M7jE-j&k?JAWu_7BAB#&Y1Mu&vb?q)kCE!0t8qtK{IhdyD7 z>-UD55=F>}Ap!!8YD)NjW7YlJdi9@KgP9b*jnmx8To#WcT3pG#Oh1>32XxFNl`;Hn z?D#+Suwv9~t3tdS{Xi6G;he7<-4@0^5^D`6S8+p)xi|Bz^>U4$kgivsJvc2VXLg*V z`gw-Uw#qxQ9&|E4R+vcTn4ub#ur2E*R0@G8wQhM-3!K)jqG4*<06oL9W(X(GHl6Ot zM+(5LGMnnZm5}M2a^4%Eu?RkS9_B%-=ApEfTUValV z*Ac}OAQOx~M1??Ix7bmup5K27o9tBhLiHBi=+6#;IO@6U-nY7H0!WFOCt;yW%-xpfL~UR+Nx>K1{ef!hmY#g9+)lTa=1Il3YYm+G7(0J3Q3*`#kx+b1s$#OeHGz3ynYQbC>If$$nAONB zAq{eCi@u+55$Y1Cn)Nxu;WPWR1x>zl*g`}c#9rtH-99O7>0c7`2$7uoQssYVWxy!; zi5ftt3nN+u1v0?h#rDf%Ja4^EnV{c%Ai|mMMdlf53gc&ywdHq}9~_(4&Rk+GKI3m3 zabE;dxB@&3Q@OG~0_f#8<_tccEh4}?xL{bYeZNnpc*IOF_6xbdnju6a2)IyU=sP79 zuHW}~`|2~K>w8IiW9?a6_;Ez+I9+@LS->%;feM&aWZr@|gLsj=Ue(sE=E+WMVI05 zNwCa@>+jJr&xi*vtZbzv)s_JPa$ruhSF*BdeN*14uhTn9ouroI; zuPbzRMVSrR^XwaQ0xav5&(Tn6L7I?YU;vB~BS93L|2}=afY9YNZlhGIU!J0qP$8`r z>zhuzF#YmGheXv+-B&}LRNO9p>u=KRoY>$Qe{Kjo@oKyBp(Obt_2jVS9=Aa$-LH0brxSN z>KB)t(%o8#taU!}Z94V8N_@=?B{F7U&92uQt?JRAoZBW^&6?0d@v7HS$97~=9E0vE zxJC?&itCkid@-E^^6E=IQY^y|wS@04`)<7I<6z!}EIYp_#bgfQ7Ns0)T_e@z>(?>| zj&Jj_cON}tm^%O5Qr136q?0r5V-iFFV*0Yn?GfL(+nhL>%Qd0d!hE(Qi6OoHD*e?E8j>d6TO3UsjWj!(hL?x7ZA z4*Qx;GX4S7;|=Mkg|Xq~KbRx^=0tki&aU|Zr2&y_W_dt!Ww()8P;)dhql>|;cmzzC z&r6?i)w)b3o}93(`XI)nLU1+W$&cENL=_9iDK$vhn{Kx9(l#fjzzJ;*Nm&$`@G)7b z%t70e@C7mM!PZm?S$C|m*2N#+q(!dHFRx{7`&qF+?i7c~Ss^p379^E`tWY2`ixB@a z5*5v;#Hc3Qm=%v0r*u-ZEa_ww-!>_`O}ee*Ip{Ajd&b6xRdK@3VdOoCm@p5^T_h#Dmfb}B+&l4B&zWQc z`^~WD*RnDWg9fQ+>Jbf}%p|W`OrFBXwZ^t_qd_e*4`o|H#5ncO zg{t>V0_v>A_9aal#=(5#ZuLnw5bW#^Uq{M*%dIIq6?$rQf2XaVPKD*YYah%xZXjc} zX*#GHT;TKWd}W28OHLE9 zz*SM_bq%==tj?V&OTjQN&@36#rEZ)nJ`+e&qE9Q#lCWn~naj`oH5HTjShj)j0?*eC zwa6OSizgmPeoc8FO<9_{FeX%(j& zFpKc&8&mx6@Cuc#Oa!oyxnYnvqxeIb48U2lq%LZ6MRPeMi^>>N& zN!Q$Zt&beL3lFJwiNT&cs!YGU`<8|(sZW^J!=6i3z~9W~1V`qar4uL^Tuh4(nckx> zyL6Yhl z`*IJb7Jcl>*5L8xoUggzyGO6QV(+ttHoQ$ee!cXz?pbTBRD07uihZ0g&~7;26W6QO zBe2d&bII*v9R4j6hmDRk?_XwL94{Xe9gia>UaKk6$SlJbl5vx{+5SiNbzCBCP~ zb-&##FJgDBIMLUSN~-~@z)r1t*W8_Qq&)vYe5A#t3AZMD814N%+<(p_B9$@-1$G;Vm>Swfp z@T=xbF&p7BQ2?)pesYqIU9p(-r^}AM_uE27~5%fGUb$_1B(9m6OU>H=p z?9PEl(8ZNEyKxZz94$mvz&-LT-MGbCi;3_Wo+F<^_#`-|1EftmH3yFF*T{@dD8Qy6 zz(u?5LoVE(jh~>JuA^FjLPBb*Y^;Mw9qsDu9dW?e)9;KwB?&Qh*)yS$SoWTNbk-pa z|Lkc{|Dx-Qb$AKU5;#>2L8lY)3&0ri=GC~1O0yiskLx4SD@f;HYQX+_tmBMMD?yk{ zrC}gUZSE^=?YysT2PQ-bkn2KGXaLPc1f;cv$4kw-4IMexP*oyL`!(nGBbaS`@m46V zWp#A5rN29Du?(2xG7@Ll_h@8eb!F%D8*J>1!0Zfp8@mvvIeeZ!dxh{@Kz~>x(k3h2 z(pb2wWNe8GU%X~q6b6|-2fU8Xe?HHa)rI;97c)eDlm`1w+L=dkM}7M=>fQASalK0T z)+t|bvUD6?pczBJP~+@8l(t7-0f(iF9>De7z&eYiFRN8e0_&YDl(#O zV*1e>T~-j7Cd9TePb0coC^Id?-B`-bV`G4oKYBf?fdp8-U|Ys9>)gE}t-mYr*ds)Y z=4tg5jhn+pzwHybGGW>uLG$L-1T0o`;*`{df>+u(p`NEe#p})F6UUq{VGvNd%nrb% zU-nKmSaWtdLtz+W}QiZ{l7iR>AbqstZj*HC7WwZMn(btsgWpdP6`*-SBF zZoJ3i#R0t5AT}%qeHVS4@1nsx%{&{9!9agju3iUHXV&V-Y30|MTg@1t5aNwexz~9Z z8UBN^Ef)MU7r>MQL8ZO8q_$Kq?&ZelMiX7l$kndw^UsU0(+!6UJaY+1e%hvh3gdI+ zgsM>sXYS2i>jFnSPTMvF*F{scR^FN36h`-Mn<&1uC`AUx1#kxx5b3pTr(YBWrF(HD*{v{aoT4w>8tmDCCCTGf z>QU^n!AmBnB&ShLYo%*6nmsE{-IQu5Z80zub{VOxSw&f;Tr&ug7<7-B>d2z2k1s*V zxn)8t`2{#NFj}KI+7z+PE#sPxsU?=BPP#=**o7Lu?I1*9VoM8&FcsDDvlpxC>;}y; zy_MmTc+BcH2VB*i@pDs|bqe9cD$RniZBd}F-@K?#n(NR9>%DfUKMm$R$v!~4NWCxM ze)ZPn_4M27=4zi=keDo+6#eCg>PO)(`W#YlB)cJdX^R&zb9|HNk#c%?EsnT8awHVC z&7x6n)_8??cm(~G<`$*g2Mo-i(#~c9S>v+x*sFabHCXVdkhKn!5>G3*MSzScu< z`R>Uq-nv2*_N7ipeBq_)ESMNm6$I+tj+O`5HK%E(ZdaDPe}*h@n_IVL;$a*3aoxhr zO&nD=1nAno$E-3Q?yu9$9dX<{@A#5$fz*?wiPAb(z;pkPB?myv>$Dqu<@{x&9bL@ zm?XnG;viv0=Ya~3KIpAQS3iHg+A9H_dxD!)gQ!2#9H z$`QW?2otsc*MQ*vPC(b$6*CF*)zc29POG6=_eXh)-f31nt5BXFCrSgl8qUHatX3=**lWm%X$AdzKI5P4j?@*dGn7OirWv&3Agjgjps*&-q&iY+CKwlL^n{V z?}(3V=A`fFFU`u2ct1Q(x4|uSPCyw=VDj2>TPbhnO*u_HNsU}Ms2*|tmiEevWy0vL ztP$?%A{w(`eK`>HGNPwy#$I2ra@~-Xm!`|CHE6tDBe`Gw5Z8S?z7BSZU*lPm>bydZ zR)L|Nx_IJ}cVN}}y^fn~hid;BK4e4(7>FgTrLf^gp`wI0D`9YiuzEA{hyOI65AQr+q2S)4RA zj_$0z=qil-Qv@~7BSUEpM6or$Py-x{aNMsR&$pG;%Chr7c<4>N;=D+gxKo=$*&0ijkyzauIg#Wr1(Y zF2dv;TDmv|vuW$bEgC1TajoZ1?nd{cMbCjkDH(vsvSNY-Qq?#I{h5ErB(KtPB#_`XUZy?PdW}B35IIGu*o9MS0MKHk-RM<^p zOxZqT7X{QVoCK|Jo=(_@3?^ZnklG{?pb0FF8)#E#_@%$aP}QC4QOZah=(2xzTCys; zGIM!d7jB-n{UEf4x84aEa3G6E=?66d_S0vWa|P8bax5R|Z_dl&*9X6CFks_R+`LEg znw56kV7GV1={M@zf*9-T)yiYiM9Whgw(>l=vcLb{oc#t@`qfsEUr*x?+Nl#`W1HpKVB7vSUF*-`FueG$FFY3bGJMrpW9mmor4`B zc*x}D5348>fk9+=T64matth@ARjnSXMvJN8x-Gs0f7DL694~XOi>OhXUV6QOA6esk zet*E6mkBwepGH4&EbI}j!G^3SL+K||{-GQ>jU$)Z%U1ol zINtqZ)LvFpN1c%rZ;o*7E}O^C1=g%o4PLShL2RW}DaESgFQ2}Cyf|&tau(&vMy{aZ zjU!OhDadoO=e_JUX;CXn@>ah@i2Hj92Zw$dd}ogcV{ExWf}Q-`+)p#E^dT$#wAil- z1LC?W9=$4zevxq{sR_dy?&+HbkDm^ElTCrZ35L}F`8B!Z&f(8 z_jPsQP}Ni+7<1*7ko!Syp$!*2Iusq`*KQyupo0n0K$@9(+^jvd=-oOyr%xC@dBswH z%Apl9t~IW4DwX(^lEy}kCce0ZCe6y3HP{E38AYc84X~~#MNt=d`&5l7b9p5L$rkb|nTR|JcCfb2r*kLoI-zVeKi_WuH4^egP^ zm%{02i#abL=SjlHjye3tO-(m#>@!BAF3-E>9X|^OFHkozB%eY}Sw_0w zGY146Vx}@Mg{?;28w6xwJpQy7YJEwdkwLW2ms`f)SJ`xdbEwX7lG#jlB(A#TyxnnPpng?npH=I0WBofa>KvC-t649{|BkXzgKscvXi`29Q=z(4eb2?@S)4K}?_ecHBnf;2=t64zXeRcKxgjveRVXH0j%_S zeQn*4K=w(|R~&CtEw}JmrC}uF@#*FkF%^T(%SN_I`niWc@8`}P5=5L59NS*0_d8E( zo)vzeDY%DDC#oNYP8hk|^&MmXY%!Csty=N!MOrx0sm^8G{Uh4>zy0}59r9PzCZ+g) zmX1+nXdBv~v!*X;&f24=!hRPU4~?2{&Cc|aUoMGio_3ii>OYq(OnE7rJv7V3Xk5Ub zfJ!=Czp)J(`kTvJxSLmUw8%77CunTz>J0YttonhtmB2Gyh*2zct|x zAg=hixa%r(%0#_CI_8)-Q&s!o-^ynH3a>KA)o--xqemxj1eDlNeh{3V7$v^-q=TH~ zxF^7i3F^+A-u$G#kv5xbFDc5ebR{R|O?kuk`~R>ut9F)g*vW=`99^}0b-e_p7zqhV zfvA!^8BsYfvd7fvfR=)6|+MTE<27^ zO{-i=S~Xk3KAx^ze118dIgPmTPha#8Vr{L!)Nmi)8pOR`1Z)V(nM&{GUw{Ec$@{YV z<%bU?hKjSMw||J}uNEf{S)zJc`6>F`J1KqABewB1+d?02mwx3TxN6Lfpgba9bTp~Z zICfI<$X_I{oO<}E)TuqKn(Q7V(TG4VFXCF-@JGP7nO zJ968f9S=>!&J7AS7c>A&LO+SotW0rKybC-%c(y)9%`3jmJB(2J4gK^3h8Lm`;Qla|>7zOOf!uAhAEYrpjOIGwZge!n~ z&$f4)X&LqWaAR{3>FKkt%EKL!nYM}VzTp;q`ukYHoC*Rtihj$jnAj`Gxg|=+sk#~& ziJoTf2pLhA7yS?%^ANR1fE!zQlGDU;CTlFHHyo(&TT@G{JJg&;8xE_pIMTb3M z9fXDFeJJ;t4Uc9k8tOU#@d^0FeP^Y5i)@pj0h4uEcvP|O1SY39gAZc-Obq%$*a%53`OPV&?RLimW(vxkN|C|P- z)RCc=T2-95DERM_L$M)K7}3XnP8!9A3>b{iO%?|mOdWkJ29ga>Q2nu1?%9$OhE6(25$oNOVk|zt@Kh)zs zMo+)WKvBjJJWr1$ukH5u=cnDzB_Ae%!3DdT+=NWFfi9~IwJ>4y=s$Jg&yNZg28zxO zgyzZ%P(07E{Fd2g53$*^i z?EV>K(H^kd-fsIQdluGLV)f@?@gwQU*6{koM=9?J6=b}e&fU716vFW&6P_(&s4!s~ z-7jA5-RW(h;`7eceWVVCt-rKY!H`=*FZ^i0B$b4kWmihI;^#!XE0_V^eb$zFV~QyE?mDX35g>IH6Gq9m{gsH z@3H^rlj@>ZDcj@og#^o0z)L_VUi}5T*S&O7yD_`B+6X9*H12DPzhDo9XR$PBPXmH= zI@;zuvdL7M$#2_qJn&)#4(F@PGjh|ZdusxQh3@AmfPi}DdB&(*YsNLW>8bQ&$BU3n z8;3v6B$oJT zz;?#T5$xmEI6l+rZkXB2(ZefrINboG@y0tRYvv;mp~5JVm)|EcPgj`CrOqt6N*DNe zU!}FqI&@0ZfbdFP`EY1gSS6VglC2LVv2A{;)}b?^6iLe;_Ykk}knb==uGCx2T8H1q zUb8L=D2;Y2@*~xcWOY>s(6F?1(>MeYAjis6oWZ1B&iqz$ciRsIId1XTHxPx6rh{{< zHH`X-447{PDOrNG^!G>c-D?52$`2F5)NWNgD`@bhp^7HxORD!PXeIV)jFLN+Hncqp zPfmq0=flOemL*#sY7&nXLW$+?h3Xs+N@gzXkHVvZx7t(*$zRrcTCBVie});s_;H|F z&ZfyVepiC3{xlRzE-YO&mn%M6P$HYqMNAIpe6O^8YLG!+Gi!Z$o03XG_PN02a4hjw zMu%~8KirqnN=!61p&-Cd!U=-_1N`cAHzgEHdLVqT^=BQI_2MO+6wf-)m9y zz9WIn<}A8FZ)L?kry;8F1=EPAEfWs@ZknK~r!tQ5nFIV6AkAutVoMcXsJ-u}QU9sQ zBZf*pLWV9{06tlR>L-u!NLHUNzjJzg!pg!^FUs?6&29H3e@Aa1yv*?gErEx9oOI+Y zZOBR`nQ?l%#yQ2fr#{QGb_2Jf!G1qF{>Pk|X0Pq5&FTdD+MqOWZLE5x{}0J}XQKS_ zPxz2i&X?2!%?%hbv&IRr-#i}0Vz+PW1FQPpfjwn`x?rve3rvC8sqSlsk;HXEETzpJ z%X$}FhZhH34cxPohD~x1R$4(-S^+(w+j*$j=KAV}ggkWku_3>~1780U_1lv+Us8^hR(=N~;fxwOiV6c_ zf5s=_lNdP4k1d_Mb6$H|Y+wOZx7as?a8~ zsvf_*`GfLe9!CHSKuLLn(f ze_D2ftpg_?g^P~en-nKs?jVAXdk_9v0tLiOxr(dgC2ELI!A27s?6y!#v9tz?ZIv0< zcwUxcNqzlg{~I4Ck^Jy$l+;}!!cnV?fTF2T+R`(obF8r{qMphlS$&pNx+flZZc7kG;R;!8BwflGU_3;5X-LY07`z#J*uTF!t{q=Koa9murO7%Uj@7N zl#!cWCtN-mm>sNO@(pwhr0mI-W6m0u$b=rne1m2}5~QC>(JQ zUWFZY)$vU9(nExbD=m)d~R{7d&a>lASlg=Ef^g%nLs9MKw+mZ55)Ei z|N5lo%FwvgXt6~pK80vA(RL6j2NLO>2>qy~ldl zRA~t3X=*e}{3G9x&d)d#7w5#Q6UxjOttQ$sH@3e3W28@SZ$vN1%SH~LZtAomb-!>| zHnuTnvesVDm9ABVuklsGSb7z#F17L|CBBJjBZzwHu5xzty;q0%rg-B7BRTOxExsg3 z*mUv_$Foq_DAQXh2y?gYZk4$5wXL{w&hjv@7r+zAEf}so_ey+mG?rJlqm@%-5TNi^ z56iZ9!*nA1X~;S{oRgU5OdOtvQ#Q?hZdC#03JMPyO_?_O0>n;I>FUW^*X?MqBXpQN zdG+1p`w>25uT$R>Z$X_sx^=$VeClE$Xfmf)g&ox3b%2feitSq@(K1okD!eR3(47vK zLa|Y2$(nsvRTq?wH^x9KbjTnc=!hV0pou_P);s_Ban%E^^HQw2LvpHDdEU6gcA5b9 zWzc3v%*1n^m+Pu(j~8cms>-pwZJNDTBhq_|v1`vUf^bvlLv~rzq(ZQJX<8Swn?&5U z->_fmGoa+NvApHa6JA1fufJ?E>s~8BI3UUS}tK{SzC~ztXN+#yT!7( zgE*Okv0o~_k19^p$3Y|9F(0C(A4ZCm<3xU&g-L=@s$!k)4L^;|vP2_*>C8KGCPS?*n6nlm z@(cn9(_pmWTFU>2HEpsY4!A0Sq=J#@XN9>S12N|H3q? z4NJBhmYmw0i$FwV!X!~iq#dDca-LPk{zi%pcPdNy&D5)VSV1iNG$EYWoTZR{0`_c} zC-6?@JGE&7T)&Yhnj2Eni>aYHE9rxbJnn+NESXbOwd6&~SLP|@B;O_(HM5}GRR4Ak zf%g}d6sP1b@9U{pL!-HfDh?x4_ukOeq|YVyG~|gn!6#MpQAnSNH@q>f)VXnc#odl0 zw`K2XyUR_8<+0$Vi$ZV~9c3LAZK%`7q0(`=wZ%y+7@gAz303AApu9*-LO}R@KQGlQ z<8M2X)vdoiUJ;&c*2govWVCucaKafQ<*C+&)@{#qnwIJlIVP|Dwly=-C{42p_MwN& ztqiz5u9n~^jAL*;_SNOdd%}(?aCQGWIF%ONp084$Sd1>21{b~D>>-Aq>FQ88!q_fc z(aME>SWtLFKigxz$Lhe}p#f{#@@=L=Ak;HIb9PBQ?|2X zKl|YMTWWZafq+^(N=9=GojHj~*RuRf?u*|Zt-7;51GyE;(vu9vPfmZ{vGjNMJxSqz z{wlUW`2OOUjSE7bEXazEX!f@6<0}?ZYhX*WH4HeuKZJiq+>6$>KK#+wA`T~_gQH|P zU~}sGz4F)ZhzrUEO)}Bz)7mc+?7!o*PiE^m-K%|o2PQE6Gh8S36O27G~h{oN{ zfqB#SxDA!zoR&X4Q1w~Cs0fn5vIN$||M{!lD6~ovJJCu<4rJELvg@FT0q-zw7+)>2 zO}?t3&q=08iz<&G-vS%`eb};ht)X5eb#5FV3$G{U23-B=`ZLmGe+8&!-tDhDe{T*9T$o=c8_NI0AdP^>KA1=|8S-kR?aELPcvTppV6njr4?KnVXQUDbWxkSKw! zZ4QLDcxH(qwbj8ii>!XD;BQm}ylY)GKS24Mt|s!zsO4ug`ts~l%YP!_e>KeifRPKG z{)IC9H(X_8FhGgQR2g|~)H0e0Se~ECV&!pn>CElve;%0jh$0s0M!;8xD>e@qf$1QU z1b-LJ{WFYImbg4N=Is-;LlCDk_2;1$W;o)k_($Oc2t_yv$w^TK}h>1hvN3?CtKvUH(5 z+l7J8sXWkAGmD%&Eck(-?iXMK)AbiXx7_=P#rqeawvF>fp@t^`n0dpe*Pu7E+=<*e zZ(>~{!iml(oS7n0oZv-SZyaD>MHgt+4i7#o^k8>U=K$JFo0NUp2B(5#n{nFZP?1_Q z!+vO1dQr=16LV?Ql&-GZ)D`Jh9IKW*b|?jc{4zq@9*_eQ$y=hIzHgQ7s{ z(1%FJtF0S#bxo{44>iy=<6d-AEoBR`>K{UBhld!feOv%EvEnx$HmrU|w8Qtce(IG) zzA_mNN_&z%cnU-r#Dh5>6)sEHe;yiyS+|6h(K3A9i_q%`wZSJ+~oQUX@C5J_*Nj`USv}iXLcMxv}g+_3{LVI{0xwEPG3lPA0jG>Js4d>eC z6||@AHgky16gZ$k8jKYPL>7lZ3>|kAmn0$Gko{@yW;sWyK9(xlHYdM@!cV<}H6t(4-mxy4t|X^_&#!x3n~VkTzKXh(F@jTHA{Y@t+utNr`gcxg^5O?#iZ$R#O6V)*wo$^n`i!O4HjYAn!R27 z3$8tU-l-7&%Y(Y`z%uGjdk(c-miVlA2fbL9=e(H-#qd!TiBhAJ2{j&3S&W!9~qx2&k(R{#QxoWt6OfJKBB~fnc zvT6IpZALcvxHbGsq4qH6icSz#VwLrGc){g>S(E28i7y#sCfNe5Z|e_*NiVGQK(5uk z<4M)W|90a3ZSJEVJ1A*srlV@8kY_?O6kp0ZPKQ2KZ6H~J6S@4J9RvrnE^ z>|Do(WnE!pm)NAN!v7k^ak%}?I#4K4QZd>mm!No2Ik@wTprXQJ92ORKkm#f6ghgbl z1(U?!0w#3&3WI`#c>IzJzUDyi5?8+{^d!Y7$UTyOoTQH=Pve!HRmlt@C|W2SqY(i; zkA~KZqdqJBg50KD!HReM!hY`h^X!p*S^YemfYq$VAUST=jy6+Js?j$uJ|5um3H2z8 zs!fFNJ93wm$Ej^M8&>07&H#32a_ue0sj(?&`azf%wr!_biZjwZ+KH**kcceq$E%;U zUh(uQ5eqN)qUMx)9hnArxyuie2F<&?OBD&{=gh4>{X9Ms4Rc2ML36gK3o$~mzt|t9 z4PGpB$LCaB{dRt{NF|_4SRcCKtXK`#NFOs74?JaHl#^%J_V{xB_-jhOlY1R6^kNI^ z;00$*Daomuig0<(rvJUR_%O=C`bRxz-k z$iEW!?J(}=D;4p_b?mP`uR!fWPW%Knfj2#Z;JQPSSR+2A?d@sYE4RJZaKR<;L6I{e z>(%)xMn5tkTi|(haTS&t6OBb0(gKss&uKxHsWm@H97kPWX!4@{V4^{xOYMuuwf>-l z+`?Q;6?%6i0(c1kGZy30L!*9!#(o;Hfc^p~e8PRsyEM?%{rCaF2W4BIo-o$z*1A^N zLUv`=Z{k>Im?s;%Rv=dYmu;#`i0tyW_~_x^!VDH_voA$C^fYMAG62rLOQr^!sF?Vuw$K7QGOS;ZXK{?XpMW zoMyjPhhet(4G*rzl(Ol!`Ml=~2z(gg*YufiOH%)vgTUMw5@Lr7wMm+F-F2@WD7#Q5B%K+MV~$yo>Te|>It0gX#$44 zbKz)OxDX#T4{exQ3ntOq`)sLiH&&RpIN??c{En-XbU#H=ZurxrTnvw6&FVcC44|kcG^2$m`Z?bMqIV zx2{TWXa9)RzQ&6whqX1zfM;cYFX=8Dv48Bgt+~>gR@t{+^L+e-?hmphB8DK}aT*>$ zuI+?J&NrNCXZ9<~bdEg`p;M~t1o<2T!z^azuXWZ>r|HMO7HU_j7(8LYqBV@jrl zc826KTw;cfgH`8z)C3+$!koM|7d|&dEKsj$@YTN4w(o62TGXvxq*68y)^P@f4z9l^ zjC7W!pIPd8zaU`U!~HPm{~qrrPH^XZ5lq6eHj8=%^_=QtM#YvAg4j=lJTZ{2P(`gY zr}@)TcJtKjS->wq4i3w8sA>poh1P*@9<3N=a~iP>nz3>*-Z<-LR4GZy9Q z!T;3TLTyHb;hG))SDo!Y!jjS-Yab%R?@+zvO1_kS>sw{V%Of{Fbcx2R28aVJTw`>} z{%}5b$BVI=gvk$^G26@OR7<+YY-7Cr?KHL)TH+NPBycNlt<2yTNOfA(P~Rh{f7P;f znYG)aUx1}%6~tC#Ouqn;Z9M<*%0341ZYMB1yJ~yAm*1|lpZBOGOwO10L7Hw0oBL?r zYkjRkP|36@%G_VqMoDjc+SZ+mokhCn5el{3-f}#ZoUptj=cs!Pv{H%afIjP)n;zq0t4Fd=B)>#;065>bH zX6LuN@~Y1d>2CN()-raKlr)ulKAOz6uk~7f03{7$aw(rZ?z~T+{oNw;Ii(#d5=7}E zU2pcr>xIwe;?XsUW@(h0iOWUIp@{(tX-Yw}5&jH4U ztnRlmwWFPfz{K-aQT)93jP*uvgXG0hhdGvuFde!#sJRT|sxeigfX8};Zx^(O>mfKB zcAce=7vZkzOkX;S4dh{sc@0gR1C%vB>T_&OhKK~Bnzr9A`tJcPd-7?_MJUw50-3_{ znW!+4^6>1??14QFD=UZm4_m&67$Mzo8<&}`8)D=Z3wBDL6LY}< zOT6y`P>YdouXE}IFEH<#d@=yV5+A#j|4TCJekXo|;M?I}fbTwhMqFJ=~UG%-u^-UJ>8XQQBgT|}*Y0(c7#~_TV z;`R|hs}+rfr(ygVIKo~fA3l16n|KmS39rlNl5kA$T~JDz6Daj&$ELc6qtx}berv;T z@lW=~y6qN|ZQK3?kO7d^Y_xtfLpnmI2YuEjD#iEwX3kTf+}J@gQI94Wu#EF01bN26 z**x)qA6;8(;N_5vkgEw7w~J!KSd<3Q4ZTUO;s~# z{{t5Z!6Qdd=+s{dfl_101=}cR?6p7kaJRj8%UO(_|(D$Sb{@)>BY9o zW%fYgJkvqA|JQ9?@MFmbnVO8ht4Zks8vndVdC517&3c|7q*NK-Ot@HWcy`VSSW!{> zj+jhwfY9Ah*hK-m@!5Psf4ooCiQEP%U6Yq`FEMnZuGk=zV@>AE#4um=4!zcGWS^+? zhqk}thuyqK#G-W~IZhS;0}ezblJ z0L73Q=8;aaOw!~sO$H@pBZusio`NCw6!kE|;f;jwI}IMFym<;nO((tyCoM>QzsHSt z?)6-MyEP(*t-~(myiK##{A4<`s)E~i+^&R? zBwFo*gP1Rn)6mZkld4oz);iCZJXX6D3XDv6yLtpwR>&|oQmxqPDkEu@4FqntFXE** z;C9ffP{T=NKkRx+!NeIMV#sKXAF6r(F608rQkt|~sak5+k)MS9-`&c-93OJs+KTc} z*EW^0mCduh)=kBcS6NS}glnWll>kI-8?~4Ay3=WwDqT;pf$vdr9lsO^=d@8WudQ z-U*~&)K<*YrhgOJvN&pv4`lkVcl4|*C{S{e?H^A{;fPLozm2eZB1LmGfuRpEj#rOw z;@%8woP@`rM*(wl8Q$dEu|%pnrZ}pTAi;<59qcapD)YfR>UWT5 z(fwvpj%0Po;p(&?^JVQPLH-5cJ{;6jCqOKKApiF?_2(*KtXPKF^I7~fKYRYdPL7pn z_MO32T?{+_UiFE~#|79}UcfMleC(W&jd(s-0NIBNKs*Z2F*r*cWVzDfwCJHDBZRqwldJMj?>|nA{sT~xKLaZ z%+t@F(eY})SskmFqRFPdJ}-JLs>jK1m47N0dWy>f78g~LTWQuFx$?$VWoPzjk1RYn zO#70<0as^yIYu?bJP%mLTn6&>7B|?6>2uO&CNT^7# zFFFbm5(+v78X7tp8U_x=-wOu=3mXR;3j-gYfB+w#oQ#Z&oa(<95E&U669bbN2ZxxH z5SNhjzl{Iao2Om?E(+id2!RLU0^o3g@VLOIegHoJ2!Mlw2Lb{Ay?}6tNC?O%@Tf2% z5b=L8002ZFJOUCj+|wce10JS_1CImKy7>64ngnLmNg5bp*Qqnxj())kHUR^Iiy^7h zz0XWkx;lCoQK!{xVC2QK2$(Jyg;}#q{S(GB8w_L8!FbVWFrFJsoemBym07h+1L2wb zuWp^&zz7jL3}QPPPfEQQLdc}q9SCEivVvinFw|#GI=cZ(E`nJ#U%D9W1B?rQ;-QU93EPAJAC6>NwOOQHem}z40rX@xsOhGibU8a=HI zJ+6U3>U{NLv{X)N7{VRpbp?V~*Y~Pk>~d%)Z|k@&eRLWu?pGxpQ-(+LFAb5+$7jUi zwPn?Q&`16qASS8cXVb@<3xu9hn_c49I_xzu!-2Egyh_glc72}ux#!m_sDE)-}%kmJpm@uJu-eO zf9;^i>0bK0XOvS7xx5>;@gJ-()d18)?i)OCsLV06h6GC?;AjRHzY>Ju2vKsk^XMI~ znAe{@0nC>Nk35m(D|WKH$sW7ry`c@~ByHnwB%Ei9J+KIx#eTEtbH5)=snK6^T#{u} zo#4+!s>uKZqfiGI5eLG+EV#DsISh{Vva8>Zr0wQTR{{3;e6Q^}0_N_qh5M^p84JxA zgt}^3lp9HVHYEfE{G30#ISkpfOQ7Bw^aGw@aLiyhz=S%s1)o*>+q;IDb_w)f@n8I> z0GGoZWcC7Pu`4e1%hz+BpALZAnc6iDO3r$#_M|Vq_xfvdA;7{5bLK0|6ZqpzTJMkp zU39!f)>XEVZOMD}#unskt^Hv!TRrEGq_$h6+HCI|k0vOn-urWC=5B@3!2uC)q>!Ft z0`D5Jw<-bky5{^q)L z1z4bM7L5}foEw)<=NjV?Kfli|OAtbyz9o03bd5_8f;N2C$&Ea_j_)4|?%O?32Oz+q zP`?j>DFTO@Gurw*7Uu?E>>%ZEjf-?IcLS)*3zb!JKhOK829WTVRH-x!&`tmV0;LehO`gp# zY+_Gb3cjPyZPe}5U%utcIRS_(#D8y8kR3HWnl!omC1#}|m7oZ~K|z3zdp3el@mbFK zp4I6vW+-H8x0Rz6V-Hz@(y57N-sjW;nmL*qSv9nG0i*^ZYy zhpq1@&x=0_@Co4fMb2F3+Mlj_PKkj#3--S(DlZ_lf1o_3%X+%zHy%OapC+F*BF?TlSx|%4BFfTMEgRx8&7mU+I(JF@8b;I{8wX2nNG)PfZu=q+3U&2@PH_yp)hEg@PIogmvk(~?!LEQ4t6JL7HCJJTO4 z-rYgM#XdukYjeFzepkNx`a3Zj@+aNHBqMt%9PW|Pg`0LLoPJHrF_@yGUN2K|@Xq*5 z;GMFcEdZzn)fd{i8tha3E!9XeL`-&noD@7KkQ?_%yy|Y9Mn}) z*QFK>F4u~c=Obnn}GHTBtAn<~OeTlyYM z{#3Vx;?l^HtCm)-Er!`Qkx^~=z2M5#Xq1f7_&L4r51998(j*!k)#hEJl$Y=5DO)I}5O9{6(s|vF?Upuo8b)cO zwNO|dYAJuJ^N+4^G|HO|Uw=!qmqga*-1+(qcUOf#*7)1r_Kr7Afjqskv>=E@&@Ta2 z2jMrk+S(&^9|RM&L8 z>i$q@5EJXXjTM%Ih2zVvC{JOlqdxf-5ceqAVFr!bR3?>QgI0c%+>VV-jNw^gF4wO) ziYY)><|ZRR>v5iAkA*5eMU7%iZ5aJx)DIt=ZM1)N4NQ%tHJWyRn>Ds8u-++>XRZH& zYg>`r#Z`qJO>yZB3}-vtRzpR)!LFjI!dkOxogVRnkOhsq|Ega%LThCMm4Pb>&(x!A zpQbo@!g!eT`cG#fNTyyVpPDYyMZu{+)<6;h2-CZ2b@X_-XUpnJR@Vuf{(3B65Q^>`p{sRyRyfDVU$glysQty#DJ=kdct@xJF^;uNm)|r9xhOghr zL>*vFkMs#HS$6KiXMQKuPct5gn>W3ahq!wDWk%Pnr`qkyTEnmJ`uF=6*(blEE6ynr zYDHF5!gER=DWYt_4}PZ-^*z5F95=Q~ZBoIRpA-8w+aHoRRdx51mQWjP`jYbG$DbMB zEn({Dsn|Pq9r^PuX2ue2rk&2Bm6#)Ia}i>BP6zF-ME$ne>3X+v3CWbh%Kf2BwTOJ8 zFQImGHg2A2T<^})Rjo(~dLWprYr5J|s_%pr#>agUy%J3yTDT-Pa|%^bWaSI?@$LD) zp9ntQvBg9@0c2yX2^Or0>gav^xV9*Ho1s)qIhFNEOq|hl&2K^)8+Zq&<&T42?Y~Hr zyK=}FOz6scKK}6G1g1WTyUF(+WxGX*-!R=GtBheJ4V$YWh~G^2JR@A zB#fJ`0@W?V&~oj>59hljIU})cx!atJ(R~-X^1{0*BOT8eboeMhH2NMZrc`b@;@^yjhh0=Zc z?(L7dS5P!~!?fr>PD|lx^!@n=M%L|dervt8gIZ4%RBDRV0%Ym0J zvn`1<*E=op%wIlpKvKCq^u-UGbLF)DW7s0 zlg1T?fy4QD1fNSky#i}bbY++Lhb3X>Z5{k*RtPOu+D)=gOrb%@|O09bz(ip7bxwnZzg|+7h;o4wmoD$h~q41HdIrec4St|h_$t*e2j1WC~7u< zDp$rGs**07{CY!@l;x+WTgkbJwYm<~D7>xs^<3u^JxZCmE+<1WvZw>y?Uq=yb_%V~ zz@n342Sfk}FJxD>J6=EYyW3K8KK~wUwBLLc)$+Amye>fD(4t+=Uml~h#$7WnAmB^vHxOD z$R?xs%M@{Lg&QIZ_80^_(mVkc^E})Lh568Ij@TV@W)<`~bpH784+pK8gLz7&61xYY zCdt+=8MBHo^tHaQz0XcXEs91F4SND$*ca92y!S~GZdqS3Lys4BR}J!x6jJeUA74LV zY&iU;S;l_p5fE|gThLOOWp0AMjbsfZWh=r{c&oVhD&EIVh)sdNi#0kq1*jhX+vyMg zTL`609$L_d4(F_W4cXx+g{FxR7UL`#E*DQ5p)lJE$0iokCFtdP(%h?Xmm#}Gq=4EqT26-emJa;1n-6qYVv-dd)9>~N-Oj>4?dg6q*1gWPrhXnDz6c#C{ zin$b@0GfUUNgXqOG}|>BYFt|Esr*bIf*uA=2rW8)?oCUMsT^eqLr%p&(;uqP@l{?s z1?e1Ogn2|!=1?^iR_3K#oOWWbGi}bOg}3F@e3*NYMZ3hIUu&AIMR?jGSI#3ww^y-{ zjYW1by;jw~xkNg~Y$^C@0CNM&or8|8Vcr{(%ACPJ72)h~E14M^D4%?dp8N!GyXQ5I z94yZDvdgW#-=2mhiF};RXjRBX))lqi94dQRnN0c3hLcm;Vbj5$z38o_uzU&OmauJ7 zwfkv;d+a{rs8llSBBR3EeJqwf044+y0&Tghrt+L} z-Lnp;SZR!^{IvWw7g~$*xX^ZTCat`VvBiZn@lxQd0)9;hu@2QbN!H3i+`flD_WX+K z;dgycXAoq41#*`Q-V>_k{a8dfLAp*WBxuOiuLNr9XlyGyQ3#VuLX2z=&6`LaRmU{E zKBrRD46|i|?tc%l7cL75Y;mHR#Ilc|0jst|)4TyW7a7u4^t-@6Ou+Ue7FZ_qoLwLy z0{*Qe0Rgzr8odSVEEu#JNH)ft zip{VCNp+@!W_3>#g%HrxEhXFh5I;#te*Y+Q7yRyncn>Gk-;pn9kE#_h$*cInc+rBO z%{iv`8RyQ(RExPy$C!>@o(qP=LG7*wqR5{vXQ9c;S8Wphs6O$~4YUW?-QWbKvnz3V zEF5C=I)A(Cil_F>zmiy*HQM-`A(9Xxzt_fgjFMEHYO{DZ%OpZh9)b+dtO*PsC2=(; z{>pVR`xqXAUN3pZTz_`u#t`?7hySz`zX6`6QhY7uX3ypDrvPZ)Q(D@d^a)@;w`YqZ z+|`CoVH9*7y5u{#LtFVH52J#-QTLt39tmd8R6`hDJAuSf5tJyg3~6SIy9p}T2OTe? zYU^S4TmQ1QgF)hIHUR4k_oyb33G){_ZxaU1dGl-%jTwt&%~031Z;_=34ES`-(xMzu zx91w@y#_hP5y>`PJEw=R;6{d5Fn-#i!~XWxc`Xv4*M^{wV6x)Yvq6MubnA(2Wf;1J zgSJLgQ6U61=Q4Y1;v$A4jAJ>H4yz~CnePbk<0aK9<3=_1!Hm|q#CKdnRj{@ z7wznokshEfrHG7`A|QN;j3!R&{QK`vzT&&&oW0XDQzcv~o8Dn7VEEE{%*sX~!e7%@ z@6(+pS-;&Vq}H^Od&mS@t;?7TA?Ds5d9F9sS>8tR$Ic@9UHA<;V-Z%Cz91~Dm{K4G z!BN%~DS)`<5fKF;RBk#9)%BiR9$B#ArrznQbsK?bCBNSEij=f*{v(5IL%Q@3`%WLR zm3QUOH?E)TE5)1?25~&lH%sz8A04FLvm3O+f*Jk7_yZ7!p8=Nv|5c8v_UDrZXSCzE zGh)B@`t1Y*oTL%e#aI#RBw>Xod1mI6^|mYPhlJH2dlab@Q+x4nC*3eCKNYB<%LnYB zNpac|B4^14asEU|p;LAEzHna% z1OuU~Tfsjrdt}B%%~q7?U_(D%25tD!O(f@v3})_w&-@XI_rdz>@BxgymqrKT`I<)xM-&sLlu6OKrbom z%%}+X9lyMx1#uvj;DF~F!&;6X^^R-o2?%ttGCv9dl77LK>8J_Mz51+U*Zi(7x^PAK z`1dRx8=QD8QxmBl{j=4UIe^J!1TNgsQp(*wF60Yi(Cd zd=hFRW`fKZ3cU7Q_VIP$+IpSlJtct%`WpA_6C!by-E-{H2PY%7UU%{c*EQmsZLu4@ zOpV)6RY#5KKgD}@*Slcbk4VEoGf`TeZqI##JwF4{9HU?%xd%{S4b7A>3NvO)h1;NL zOW-Gci-sT0gNguxI9bclAp+zIwDH|dN%1&J7v~tZHpF>Y3|~XQ})d;=RI;7#x%`#_e{j6Knejb=_zFi zs{Fs+|Lugol{PCQZOAW>t>OwrhbTLZ$9uJhMBEbHgxj~;cbOfTi*Hnh7DN~=jv`2) z!#@O!`g;J56e@>o1JVG^kCsq_2^H(B!}TA@eRWHqd z@yD-!cA8`@4ge3^3Vh9!-R?SZ*^n&f>L_BL0KEUl)sggFG!SnF90YJh3D;)1(!UcN@j zOYj0I{1_-y`y*RCeRbvVDb%O}BL^c}^^yB)cxy#Foj z8M4PHr_WKs71^!pRD6&dSD!7-@+^tkvz|`SEygGQJtH)&Lz-Bw2pp@1E}xcst8( zJmvNno&E#}ciVmfTw=pcE`qqQY#IR$8TNJ#`JXE5b2g2D%Y{cn4dRxRQmaG6!RIt} zC4{Bbs_OX@v^-`mf&XUFa6q60;PJdwc+qQak!aC3gmS{=B0fR{zR`d@pQ7Y8M1X(s z$p8MF_lLhX(>E*Ah0s>_Egs&_M{Z$^>N^VbZ&-J$~-ervzypxt*pb zX=G$4V=B?KV_fB?NS`p7nW-u7x-J75g6Zjfxip$Oadom&&c-wvCh7DXA+uc^bdwqV zU*S5G0%jwHUSnwK8wzXy3Ap%}_4vrdKv18yiu?VR4OM0{D@PSljAT5w4}7UjQ)h^D zOsb0v#Y)y}A5wFehIBvSeau&LM2$@O%+tl>y)9^Grt%uAzNRn&n*BCXx6)4i^^}|q z8UcS~hS|hb7=z}*@N$0oHBL=ca6LT&ZR`Ql zPE*Ds+RbmR#V)8mCC7$-3S%HxSJeLT*oh_6Zhof^zx2jcFiC+6)s#h%pYt=phh!s$@SH{T zLXTY(Q%I;%NXc|JSqEnsc*|$NOxZ*Cwdlis>V^yU$SeyAakk=i?zvzl6UUxeZ845? zX#d~7+{O#zBo(4mjg3O-67_0wn#1+c!Qf3mVq!~Ff5Rhb>6_R4+sTT4r-a6npgQRS zJa$u5Q~(y)=pmKh-GIq1UNSu$4Rn30ObYaAf{WMSn*;1@uLV+$p~YPk6KE}SK*oFH zBL|bK@rtOy4FLHqL%LZx2&4LonmlKms5`*_ag@^48sPR*dWrn96;u5V{>M(fF%ZXXdtpkF)^CN)eS z$^Vw4Lf}0T(w74 z<;YpGo}al+^w;yvx5Z4%*`dn^6h#Vqc=-6G2kxdl?*gD-Zud$RWWL~n*BM!J+>7Nb zC$Qk-ZAHS0ZgV<5oP#R%_<3ITe+L$?2#Yx8E6}rOo1B*%k7ztJDR~t_X9*38s>CF3 zZq)JGLn21Q@kO!B=PmndD$5NJ=UwR!RSMhTFL;B>4SbV4$Q0Nht>V^}v>+TmeULJS zv)VpOw}HTDt{W@FpdaUT<1pb9HII>R1FPS=!IM1wHI?eFf6{B`R9j zZelNH=q7hz%+F-%c^-Sqlsx45toiOqma{#E>r;7PaV$!=9k5z|Ws2ob3!-vMpb z+HyJ?l53d^71uiC_b`F-yeaRYeBIe*+k@bli&K9KnS&+MpV76K>I*v`lhevxuGmcr zc(1k{=qbsXp;{&%gzgsq>9~Gev)pd1Z6r@+*n|Xkys~$LS|@uA+T4#pevg8fo%rb5 zFQ9+g-`U)w+ghR{GLy#Rm6lP)8YE}H{j#I9wst7f){5zAPMH7cnFCAai;Ek5dFL(e zx~8ksdQPrSx*|x`zy^&{DZevDS}vpR3Lbbcg%j^2jR)e5To(qyBlKC&rIM08nT3yW z$JREyW%?aXPOHkH=4$jiI<0zx9~L0P6+W1w;YgdMCs&$KLsxFs{o+c#if@ibUt{XS z5^w_X5?u%yXXBY&jwq<6^J`YfxP(vI9zp8hgwA+P)68OJrCDH_KTY;SqxTnd1KK?W zur0->#Dua4TV@L2NnMl#2sjfc?vSWv}sfoJP3GZY$(5adY8oJ)bq!696s$l(?j@bVthcvN@HbBk;T&TiL7TTWt~wGN8FoQzuwozg_8+A%9_~fEnbxce((-| z!E@h^w9+Yn|Fu+Ii}VKt*Twh!c%BHCxXkwN{O{idd!z4_QuYap=?V88fG1_|FL!vu zsz}}%fx9Os`_9zHO{&-@b|oAwXf8ITn`r2s0Iy>~_VRjjDJ}$7&8~CSLyj~MT9>a; zY7Ev{sb@#L*I+yeJa*giRga-_~Y|)ZbnzNF_S5a~tLBXCr zb9C7EZk+}jV-mgGE$%)ZQ_K0wwu$n3N@W@>TY~g02V#xUq57~9i>1qW2WdleeO+91 z*kv+ni=WIH;)cVm(V_uLE1;z-AodSRsfjd_>B^RrXC~Qd%bI*AUpLEClrPCf@}rLH zx;}%ThPmr&Q!lm8@2>&g;^wDXD8IpAH2`Wdq`dM0om(PzZSq<{;rkOH<=ZDcV(DCC zdXnoLnaW9;U!oh?+Arm;vllXdV4sas5K_Pg*DhE8xnfJOunAPR;wb)DhIc`TGi!~< z(0KJZwq>*P31Bk9;wpuL2&<7oatmJJU=7vC-ucnWcn|fcdT|q~4yJ4di~9M^mwVg9>a?iJ2hEp5 zlyx0+H}M@Nt_LlMa-C^m?Y}XtPYvP0il~Z7>U08Xl5}{l0%vUv7rle0xyGt|zW0Sa zjt6KhqC|+{xGnAFVTgQIuWqbO#6vizutyb*KlW+6-d9$p+QCkTS}V!L)`tD^6g+eua;rvN%W~T(A$V@`WjmX)v)G+< Q8$*gS74@e#?oSK<161p)SpWb4 literal 0 HcmV?d00001 diff --git a/Assets/vulkancommands.jfif b/Assets/vulkancommands.jfif new file mode 100644 index 0000000000000000000000000000000000000000..588aaf6896fa80e48545ece293a40661c3f6fa58 GIT binary patch literal 76559 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 {e}') - @command(name='rafael') - async def rafael(self, ctx: Context) -> None: - view = PlayerView(self.__bot) - await ctx.send(view=view) - def setup(bot): bot.add_cog(MusicCog(bot)) diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index 4674336..505e793 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -38,7 +38,7 @@ class PlayHandler(AbstractHandler): # Get the process context for the current guild processManager = self.config.getProcessManager() - processInfo = processManager.getPlayerInfo(self.guild, self.ctx) + processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) playlist = processInfo.getPlaylist() process = processInfo.getProcess() if not process.is_alive(): # If process has not yet started, start @@ -119,7 +119,7 @@ class PlayHandler(AbstractHandler): await task song = songs[index] if not song.problematic: # If downloaded add to the playlist and send play command - processInfo = processManager.getPlayerInfo(self.guild, self.ctx) + processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) processLock = processInfo.getLock() acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) if acquired: diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index 1e25b4c..b1d63ef 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -13,8 +13,13 @@ class PrevHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: + if not self.__user_connected(): + error = ImpossibleMove() + embed = self.embeds.NO_CHANNEL() + return HandlerResponse(self.ctx, embed, error) + processManager = self.config.getProcessManager() - processInfo = processManager.getPlayerInfo(self.guild, self.ctx) + processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) if not processInfo: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() @@ -26,11 +31,6 @@ class PrevHandler(AbstractHandler): embed = self.embeds.NOT_PREVIOUS_SONG() return HandlerResponse(self.ctx, embed, error) - if not self.__user_connected(): - error = ImpossibleMove() - embed = self.embeds.NO_CHANNEL() - return HandlerResponse(self.ctx, embed, error) - if playlist.isLoopingAll() or playlist.isLoopingOne(): error = BadCommandUsage() embed = self.embeds.FAIL_DUE_TO_LOOP_ON() diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index c5f1e90..0640b85 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -1,6 +1,6 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler -from Config.Exceptions import BadCommandUsage +from Config.Exceptions import BadCommandUsage, ImpossibleMove from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot from Parallelism.Commands import VCommands, VCommandsType @@ -13,6 +13,11 @@ class SkipHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: + if not self.__user_connected(): + error = ImpossibleMove() + embed = self.embeds.NO_CHANNEL() + return HandlerResponse(self.ctx, embed, error) + processManager = self.config.getProcessManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Verify if there is a running process @@ -31,3 +36,9 @@ class SkipHandler(AbstractHandler): else: embed = self.embeds.NOT_PLAYING() return HandlerResponse(self.ctx, embed) + + def __user_connected(self) -> bool: + if self.author.voice: + return True + else: + return False diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index 9a1fdde..3136474 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -35,16 +35,9 @@ class ProcessManager(Singleton): def setPlayerInfo(self, guild: Guild, info: ProcessInfo): self.__playersProcess[guild.id] = info - def getPlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: - """Return the process info for the guild, if not and context is a instance - of discord.Context then create one, else return None""" + def getOrCreatePlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: + """Return the process info for the guild, the user in context must be connected to a voice_channel""" try: - if isinstance(context, Interaction): - if guild.id not in self.__playersProcess.keys(): - return None - else: - return self.__playersProcess[guild.id] - if guild.id not in self.__playersProcess.keys(): self.__playersProcess[guild.id] = self.__createProcessInfo(guild, context) else: @@ -102,12 +95,16 @@ class ProcessManager(Singleton): return processInfo - def __recreateProcess(self, guild: Guild, context: Context) -> ProcessInfo: + def __recreateProcess(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: """Create a new process info using previous playlist""" guildID: int = context.guild.id textID: int = context.channel.id - voiceID: int = context.author.voice.channel.id - authorID: int = context.author.id + if isinstance(context, Interaction): + authorID: int = context.user.id + voiceID: int = context.user.voice.channel.id + else: + authorID: int = context.author.id + voiceID: int = context.author.voice.channel.id playlist: Playlist = self.__playersProcess[guildID].getPlaylist() lock = Lock() diff --git a/README.md b/README.md index e7bf7f3..ccc634e 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,48 @@ -# **Vulkan** - -A Music Discord bot, written in Python, that plays *Youtube*, *Spotify* and *Deezer* links. Vulkan was designed so that anyone can fork this project, follow the instructions and use it in their own way, Vulkan can also be configured in Heroku to work 24/7. +

Vulkan

-# **Music** -- Play musics from Youtube, Spotify and Deezer links (Albums, Artists, Playlists and Tracks) -- Control loop of one or all musics -- Allow moving and removing musics in the queue -- Play musics in queue randomly -- Store played songs and allow bidirectional flow +A Music Discord Bot, that plays *Youtube*, *Spotify*, *Deezer* links or raw queries. Vulkan is open source, so everyone can fork this project, follow the instructions and use it in their own way, executing it in your own machine or hosting in others machines to work 24/7. -### Commands -```!play [title, spotify_url, youtube_url, deezer_url]``` - Start playing song +Vulkan uses multiprocessing and asynchronous Python modules to maximize Music Player response time, so the player doesn't lag when many commands are being processed and it can play in multiples discord serves at the same time without affecting the Music Player response time. -```!resume``` - Resume the song player -```!pause``` - Pause the song player +

+ +

-```!skip``` - Skip the currently playing song -```!prev``` - Return to play the previous song - -```!stop``` - Stop the playing of musics - -```!queue``` - Show the musics list in queue - -```!history``` - Show the played songs list - -```!loop [one, all, off]``` - Control the loop of songs - -```!shuffle``` - Shuffle the songs in queue - -```!remove [x]``` - Remove the song in position x - -```!move [x, y]``` - Change the musics in position x and y in Queue - -```!np``` - Show information of the currently song - -```!clear``` - Clear the songs in queue, doesn't stop the player - -```!reset``` - Reset the player, recommended if any error happen - -```!invite``` - Send the URL to invite Vulkan to your server - -```!help [command]``` - Show more info about the command selected +# **Music 🎧** +- Play musics from Youtube, Spotify and Deezer links (Albums, Artists, Playlists and Tracks). +- Play musics in multiple discord server at the same time. +- The player contain buttons to shortcut some commands. +- Manage the loop of one or all playing musics. +- Manage the order and remove musics from the queue. +- Shuffle the musics queue order. -# **Usage:** +

+ +

-### **API Keys** + + +# **How to use it** + + +### **Requirements** +Installation of Python 3.8+ and the dependencies in the requirements.txt file, creation of your own Bot in Discord and Spotify Keys. +``` +pip install -r requirements.txt +``` +### **🔑 API Keys** +You have to create your own discord Bot and store your Bot Token * Your Discord Application - [Discord](https://discord.com/developers) * You own Spotify Keys - [Spotify](https://developer.spotify.com/dashboard/applications) - This information must be stored in an .env file, explained further. -### **Requirements** -- Installation of Python 3.8+ and the dependencies in the requirements.txt file. -``` -pip install -r requirements.txt -``` - - -- **Installation of FFMPEG**
+### **Installation of FFMPEG**
FFMPEG is a module that will be used to play music, you must have this configured in your machine *FFMPEG must be configured in the PATH for Windows users. Check this [YoutubeVideo](https://www.youtube.com/watch?v=r1AtmY-RMyQ&t=114s&ab_channel=TroubleChute).*

You can download the executables in this link `https://www.ffmpeg.org/download.html` and then put the .exe files inside a ffmpeg\bin folder in your C:\ folder. Do not forget to add 'ffmpeg\bin' to your PATH. @@ -78,8 +58,8 @@ BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan ``` -### **Config File** -The config file, located in ```./config``` folder doesn't require any change, but if you acquire the knowledged of how it works, you can change it to the way you want. +### **⚙️ Configs** +The config file is located at ```./config/Configs.py```, it doesn't require any change, but if you can change values to the way you want. ### **Initialization** @@ -87,25 +67,22 @@ The config file, located in ```./config``` folder doesn't require any change, bu - Run ```python main.py``` in console to start -## **Heroku** -To run your Bot in Heroku 24/7, you will need the Procfile located in root, then follow the instructions in this [video](https://www.youtube.com/watch?v=BPvg9bndP1U&ab_channel=TechWithTim). In addition, also add these two buildpacks to your Heroku Application: +## **🚀 Heroku** +To deploy and run your Bot in Heroku 24/7, you will need the Procfile located in root, then follow the instructions in this [video](https://www.youtube.com/watch?v=BPvg9bndP1U&ab_channel=TechWithTim). In addition, also add these two buildpacks to your Heroku Application: - https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git - https://github.com/xrisk/heroku-opus.git -## Testing + +## 🧪 Tests The tests were written manually with no package due to problems with async function in other packages, to execute them type in root:
`python run_tests.py`
-## License +## 📖 License - This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE). -## Contributing +## 🏗️ Contributing - If you are interested in upgrading this project i will be very happy to receive a PR or Issue from you. See TODO project to see if i'm working in some feature now. - - -## Acknowledgment - - See the DingoLingo [project](https://github.com/Raptor123471/DingoLingo) from Raptor123471, it helped me a lot to build Vulkan. \ No newline at end of file diff --git a/UI/Views/PlayerView.py b/UI/Views/PlayerView.py index cb6050e..d7573c7 100644 --- a/UI/Views/PlayerView.py +++ b/UI/Views/PlayerView.py @@ -32,9 +32,12 @@ class PlayerView(View): async def on_timeout(self) -> None: # Disable all itens and, if has the message, edit it - self.disable_all_items() - if self.__message is not None and isinstance(self.__message, Message): - await self.__message.edit(view=self) + try: + self.disable_all_items() + if self.__message is not None and isinstance(self.__message, Message): + await self.__message.edit(view=self) + except Exception as e: + print(f'[ERROR EDITING MESSAGE] -> {e}') def set_message(self, message: Message) -> None: self.__message = message