diff --git a/src/esportsbot/bot.py b/src/esportsbot/bot.py index b608fd7a..b4af6686 100644 --- a/src/esportsbot/bot.py +++ b/src/esportsbot/bot.py @@ -226,16 +226,32 @@ async def on_command_error(ctx: Context, exception: Exception): @client.event async def on_message(message): if not message.author.bot: - # Ignore self messages - guild_id = message.guild.id - music_channel_in_db = db_gateway().get('music_channels', params={'guild_id': guild_id}) - if len(music_channel_in_db) > 0 and message.channel.id == music_channel_in_db[0].get('channel_id'): - # The message was in a music channel and a song should be found - music_cog_instance = client.cogs.get('MusicCog') - await music_cog_instance.on_message_handle(message) - - # If message was command, perform the command - await client.process_commands(message) + # Process non-dm messages + if message.guild is not None: + # Start pingable role cooldowns + if message.role_mentions: + roleUpdateTasks = client.handleRoleMentions(message) + + # Handle music channel messages + guild_id = message.guild.id + music_channel_in_db = client.MUSIC_CHANNELS.get(guild_id) + if music_channel_in_db: + # The message was in a music channel and a song should be found + music_cog_instance = client.cogs.get('MusicCog') + await music_cog_instance.on_message_handle(message) + await client.process_commands(message) + await message.delete() + else: + await client.process_commands(message) + + if message.role_mentions and roleUpdateTasks: + await asyncio.wait(roleUpdateTasks) + for task in roleUpdateTasks: + if e := task.exception(): + lib.exceptions.print_exception_trace(e) + # Process DM messages + else: + await client.process_commands(message) @client.command() @@ -250,20 +266,6 @@ async def initialsetup(ctx): await ctx.channel.send("This server has now been initialised") -@client.event -async def on_message(message: discord.Message): - if message.guild is not None and message.role_mentions: - roleUpdateTasks = client.handleRoleMentions(message) - await client.process_commands(message) - if roleUpdateTasks: - await asyncio.wait(roleUpdateTasks) - for task in roleUpdateTasks: - if e := task.exception(): - lib.exceptions.print_exception_trace(e) - else: - await client.process_commands(message) - - @client.event async def on_guild_role_delete(role: discord.Role): """Handles unregistering of pingme roles when deleted directly in discord instead of via admin command @@ -288,6 +290,7 @@ def launch(): # Generate Database Schema generate_schema() + client.update_music_channels() client.load_extension('esportsbot.cogs.VoicemasterCog') client.load_extension('esportsbot.cogs.DefaultRoleCog') diff --git a/src/esportsbot/cogs/MusicCog.py b/src/esportsbot/cogs/MusicCog.py index ebaa6a32..8ab01d2f 100644 --- a/src/esportsbot/cogs/MusicCog.py +++ b/src/esportsbot/cogs/MusicCog.py @@ -10,7 +10,7 @@ from enum import IntEnum from youtubesearchpython import VideosSearch -from discord import Message, VoiceClient, TextChannel, Embed, Colour, FFmpegOpusAudio +from discord import Message, VoiceClient, TextChannel, Embed, Colour, FFmpegPCMAudio, PCMVolumeTransformer from discord.ext import commands, tasks from discord.ext.commands import Context @@ -22,7 +22,6 @@ from urllib.parse import parse_qs, urlparse from random import shuffle -from collections import defaultdict from ..lib.discordUtil import send_timed_message from ..lib.stringTyping import strIsInt @@ -39,6 +38,7 @@ class MessageTypeEnum(IntEnum): url = 0 playlist = 1 string = 2 + invalid = 3 EMPTY_QUEUE_MESSAGE = "**__Queue list:__**\n" \ @@ -64,6 +64,11 @@ class MessageTypeEnum(IntEnum): BOT_INACTIVE_MINUTES = 2 +# TODO: Change usage of db to use of bot dict of Music Channels +# TODO: Update preview message to include volume and reaction controls +# TODO: Add move song command to move a song from one position in the queue to another + + class MusicCog(commands.Cog): def __init__(self, bot: EsportsBot, max_search_results=100): @@ -76,16 +81,9 @@ def __init__(self, bot: EsportsBot, max_search_results=100): self.__check_loops_alive() - self._time_allocation = defaultdict(lambda: self._allowed_time) - # Seconds of song (time / day) / server - # Currently 2 hours of playtime for each server per day - self._allowed_time = 7200 - self.__db_accessor = db_gateway() - async def __send_message_and_delete(self, to_send: Embed, to_delete: Message, timer=10): - await send_timed_message(to_delete.channel, embed=to_send, timer=timer) - await to_delete.delete() + self.user_strings: dict = bot.STRINGS["music"] @commands.command() @commands.has_permissions(administrator=True) @@ -94,9 +92,13 @@ async def setmusicchannel(self, ctx: Context, args: str = None, given_channel_id Sets the music channel for a given guild to a text channel in the given guild by passing the id of the channel or by tagging the channel with #. :param ctx: The Context of the message sent. + :type ctx: discord.ext.commands.Context :param args: Used to specify extra actions for the set command to perform. + :type args: str :param given_channel_id: The channel to set as the music channel. - :return: A boolean determining setting the channel was successful. + :type given_channel_id: str + :return: Whether the music channel was set successfully. + :rtype: bool """ if given_channel_id is None and args is not None: @@ -106,7 +108,7 @@ async def setmusicchannel(self, ctx: Context, args: str = None, given_channel_id if given_channel_id is None: # No given channel id.. exit - message = Embed(title="A channel id is a required argument", colour=EmbedColours.red) + message = Embed(title=self.user_strings["music_channel_set_missing_channel"], colour=EmbedColours.red) await send_timed_message(ctx.channel, embed=message, timer=30) return False @@ -119,7 +121,7 @@ async def setmusicchannel(self, ctx: Context, args: str = None, given_channel_id if not is_valid_channel_id: # The channel id given is not valid.. exit - message = Embed(title="The id given was not a valid id", colour=EmbedColours.red) + message = Embed(title=self.user_strings["music_channel_invalid_channel"], colour=EmbedColours.red) await send_timed_message(ctx.channel, embed=message, timer=30) return False @@ -127,7 +129,7 @@ async def setmusicchannel(self, ctx: Context, args: str = None, given_channel_id if not isinstance(music_channel_instance, TextChannel): # The channel id given not for a text channel.. exit - message = Embed(title="The id given must be of a text channel", colour=EmbedColours.red) + message = Embed(title=self.user_strings["music_channel_set_not_text_channel"], colour=EmbedColours.red) await send_timed_message(ctx.channel, embed=message, timer=30) return False @@ -144,14 +146,18 @@ async def setmusicchannel(self, ctx: Context, args: str = None, given_channel_id 'channel_id': int(cleaned_channel_id), 'guild_id': int(ctx.guild.id)}) await self.__setup_channel(ctx, int(cleaned_channel_id), args) + self._bot.update_music_channels() return True @commands.command() @commands.has_permissions(administrator=True) - async def getmusicchannel(self, ctx: Context): + async def getmusicchannel(self, ctx: Context) -> Message: """ Sends a tagged channel if the music channel has been set, otherwise will send an error message. :param ctx: The context of the message. + :type ctx: discord.ext.commands.Context + :return: The response message that was sent. + :rtype: discord.Message """ current_channel_for_guild = self.__db_accessor.get('music_channels', params={ @@ -160,17 +166,21 @@ async def getmusicchannel(self, ctx: Context): if current_channel_for_guild and current_channel_for_guild[0].get('channel_id'): # If the music channel has been set in the guild id_as_channel = ctx.guild.get_channel(current_channel_for_guild[0].get('channel_id')) - await ctx.channel.send(f"Music channel is set to {id_as_channel.mention}") + message = self.user_strings["music_channel_get"].format(music_channel=id_as_channel.mention) + return await ctx.channel.send(message) else: - await ctx.channel.send("Music channel has not been set") + return await ctx.channel.send(self.user_strings["music_channel_missing"]) @commands.command() @commands.has_permissions(administrator=True) - async def resetmusicchannel(self, ctx: Context): + async def resetmusicchannel(self, ctx: Context) -> Message: """ If the music channel is set, clear it and re-setup the channel with the correct messages. Otherwise send an error message. :param ctx: The context of the message. + :type ctx: discord.ext.commands.Context + :return: The response message that was sent. + :rtype: discord.Message """ current_channel_for_guild = self.__db_accessor.get('music_channels', params={ @@ -178,25 +188,77 @@ async def resetmusicchannel(self, ctx: Context): if current_channel_for_guild and current_channel_for_guild[0].get('channel_id'): # If the music channel has been set for the guild - await self.__setup_channel(ctx, arg='-c', channel_id=current_channel_for_guild[0].get('channel_id')) - message = "Successfully reset the music channel" - await send_timed_message(ctx.channel, message, timer=20) + channel_id = current_channel_for_guild[0].get('channel_id') + await self.__setup_channel(ctx, arg='-c', channel_id=channel_id) + channel = self._bot.get_channel(current_channel_for_guild[0].get('channel_id')) + if channel is None: + channel = self._bot.fetch_channel(current_channel_for_guild[0].get('channel_id')) + message = self.user_strings["music_channel_reset"].format(music_channel=channel.mention) + return await ctx.channel.send(message) else: - await ctx.channel.send("Music channel has not been set") + return await ctx.channel.send(self.user_strings["music_channel_missing"]) - @commands.command() - async def removesong(self, ctx: Context, song_index: int = None) -> bool: + @commands.command(aliases=["volume"]) + async def setvolume(self, ctx: Context, volume_level) -> bool: + """ + Sets the volume level of the bot. Does not persist if the bot disconnects. + :param ctx: The context of the message. + :type ctx: discord.ext.commands.Context + :param volume_level: The volume level to set the bot to. Must be between 0 and 100. + :type volume_level: int + :return: Whether the volume of the bot was changed successfully. + :rtype: bool + """ + + if not self._currently_active.get(ctx.guild.id): + # Not currently active + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), timer=10) + return False + + if not await self.__check_valid_user_vc(ctx): + # Checks if the user is in a valid voice channel + return False + + if not strIsInt(volume_level): + await send_timed_message(channel=ctx.channel, + embed=Embed(title=self.user_strings["volume_set_invalid_value"], + colour=EmbedColours.orange), + timer=10) + return False + + if int(volume_level) < 0: + volume_level = 0 + + if int(volume_level) > 100: + volume_level = 100 + + client = self._currently_active.get(ctx.guild.id).get("voice_client") + client.source.volume = float(volume_level) / float(100) + self._currently_active.get(ctx.guild.id)["volume"] = client.source.volume + message_title = self.user_strings["volume_set_success"].format(volume_level=volume_level) + await send_timed_message(channel=ctx.channel, + embed=Embed(title=message_title, colour=EmbedColours.music), + timer=10) + return True + + @commands.command(aliases=["remove", "removeat"]) + async def removesong(self, ctx: Context, song_index=None) -> bool: """ Remove a song at an index from the current queue. :param ctx: The context of the message. - :param song_index: The index of the song to remove. Index starting from 1. - :return: A boolean of if the removal of the song at the given index was successful. + :type ctx: discord.ext.commands.Context + :param song_index: THe index of the song to remove. Index starting from 1. + :type song_index: int + :return: Whether the removal of the song at the given index was successful. + :rtype: bool """ + if not self._currently_active.get(ctx.guild.id): # Not currently active - await self.__send_message_and_delete(Embed(title="I am not playing anything currently", - colour=EmbedColours.music), - ctx.message) + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10) return False if not await self.__check_valid_user_vc(ctx): @@ -204,54 +266,72 @@ async def removesong(self, ctx: Context, song_index: int = None) -> bool: return False if not strIsInt(song_index): - await self.__send_message_and_delete(Embed(title="To remove a song you must provide a number " - "of a song in the queue", colour=EmbedColours.orange), - ctx.message) + message_title = self.user_strings["song_remove_invalid_value"] + await send_timed_message(channel=ctx.channel, + embed=Embed(title=message_title, colour=EmbedColours.orange), + timer=10) + return False + + queue_length = len(self._currently_active.get(ctx.guild.id).get("queue")) + + if queue_length == 0: + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.orange), + timer=10) return False if int(song_index) < 1: - await self.__send_message_and_delete(Embed(title="The number of the song to remove must be greater than 1", - colour=EmbedColours.orange), - ctx.message) + message_title = self.user_strings["song_remove_invalid_value"] + message_body = self.user_strings["song_remove_valid_options"].format(start_index=1, end_index=queue_length) + await send_timed_message(channel=ctx.channel, + embed=Embed(title=message_title, + colour=EmbedColours.orange), + timer=10) return False if int(song_index) == 1: self.__pause_song(ctx.guild.id) + current_song = self._currently_active.get(ctx.guild.id).get("current_song").get("title") await self.__check_next_song(ctx.guild.id) - message = Embed(title=f"Removed the current song from playback, playing next song!", + message = Embed(title=self.user_strings["song_remove_success"].format(song_title=current_song, + song_position=song_index), colour=EmbedColours.green) - await self.__send_message_and_delete(message, ctx.message) + await send_timed_message(channel=ctx.channel, embed=message, timer=10) return True - if len(self._currently_active.get(ctx.guild.id).get('queue')) < (int(song_index) - 1): + if queue_length < (int(song_index) - 1): # The index given is out of the bounds of the current queue - message = Embed(title=f"There is no song at position {song_index} in the queue", - description=f"A valid number is between 1 " - f"and {len(self._currently_active.get(ctx.guild.id).get('queue'))}.", + message_title = self.user_strings["song_remove_invalid_value"] + message_body = self.user_strings["song_remove_valid_options"].format(start_index=1, end_index=queue_length) + message = Embed(title=message_title, + description=message_body, colour=EmbedColours.orange) - await self.__send_message_and_delete(message, ctx.message) + await send_timed_message(channel=ctx.channel, embed=message, timer=10) return False song_popped = self._currently_active[ctx.guild.id]['queue'].pop(int(song_index) - 1) await self.__update_channel_messages(ctx.guild.id) - message = Embed(title=f"Removed {song_popped.get('title')} from position {song_index} in the queue", + message = Embed(title=self.user_strings["song_remove_success"].format(song_title=song_popped, + song_position=song_index), colour=EmbedColours.green) - await self.__send_message_and_delete(message, ctx.message) + await send_timed_message(channel=ctx.channel, embed=message, timer=10) return True - @commands.command() + @commands.command(aliases=["pause", "stop"]) async def pausesong(self, ctx: Context) -> bool: """ If the bot is currently playing in the context's guild, pauses the playback, else does nothing. :param ctx: The context of the song. - :return: A boolean if the pausing of the current playback was successful. + :type ctx: discord.ext.commands.Context + :return: Whether the playback was paused in the guild which gave the command. + :rtype: bool """ if not self._currently_active.get(ctx.guild.id): # Not currently active - await self.__send_message_and_delete(Embed(title="I am not playing anything currently", - colour=EmbedColours.music), - ctx.message) + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), + timer=10) return False if not await self.__check_valid_user_vc(ctx): @@ -259,25 +339,28 @@ async def pausesong(self, ctx: Context) -> bool: return False if self.__pause_song(ctx.guild.id): - await self.__send_message_and_delete(Embed(title="Song Paused", colour=EmbedColours.music), ctx.message) + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["song_pause_success"], + colour=EmbedColours.music), + timer=5) return True - await ctx.message.delete() return False - @commands.command() + @commands.command(aliases=["resume", "play"]) async def resumesong(self, ctx: Context) -> bool: """ If the bot is currently paused, the playback is resumed, else does nothing. :param ctx: The context of the message. - :return: A boolean if the playback was resumed successfully. + :type ctx: discord.ext.commands.Context + :return: Whether the playback was resumed in the guild which gave the command. + :rtype: bool """ if not self._currently_active.get(ctx.guild.id): # Not currently active - await self.__send_message_and_delete(Embed(title="There is nothing to resume at the moment...", - colour=EmbedColours.music), - ctx.message) + await send_timed_message(channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), timer=10) return False if not await self.__check_valid_user_vc(ctx): @@ -285,52 +368,54 @@ async def resumesong(self, ctx: Context) -> bool: return False if self.__resume_song(ctx.guild.id): - await self.__send_message_and_delete(Embed(title="Song Resumed", colour=EmbedColours.music), ctx.message) + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["song_resume_success"], + colour=EmbedColours.music), + timer=5) return True - await ctx.message.delete() return False - @commands.command() + @commands.command(aliases=["kick"]) async def kickbot(self, ctx: Context) -> bool: """ Remove the bot from the voice channel. Will also reset the queue. :param ctx: The context of the message. - :return: A boolean if the bot was removed from the voice channel successfully. + :type ctx: discord.ext.commands.Context + :return: Whether the bot was removed from the voice channel. + :rtype: bool """ if not self._currently_active.get(ctx.guild.id): # Not currently active - await self.__send_message_and_delete(Embed(title="I am not in a channel at the moment", - colour=EmbedColours.music), - ctx.message) + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["bot_inactive"], + colour=EmbedColours.music), timer=10) return False if not await self.__check_valid_user_vc(ctx): # Checks if the user is in a valid voice channel - await ctx.message.delete() return False if await self.__remove_active_channel(ctx.guild.id): - await self.__send_message_and_delete(Embed(title="I have left the Voice Channel", - colour=EmbedColours.music), - ctx.message) + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["kick_bot_success"], + colour=EmbedColours.music), timer=10) return True - await ctx.message.delete() return False - @commands.command() + @commands.command(aliases=["skip"]) async def skipsong(self, ctx: Context) -> bool: """ Skips the current song. If there are no more songs in the queue, the bot will leave. :param ctx: The context of the message. - :return: None + :type ctx: discord.ext.commands.Context + :return: Whether the current song was skipped in the guild which gave the command. + :rtype: bool """ if not self._currently_active.get(ctx.guild.id): # Not currently active - await self.__send_message_and_delete(Embed(title="I am not currently active", colour=EmbedColours.music), - ctx.message) + await send_timed_message(channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], colour=EmbedColours.music), + timer=10) return False if not await self.__check_valid_user_vc(ctx): @@ -341,55 +426,60 @@ async def skipsong(self, ctx: Context) -> bool: if len(self._currently_active.get(ctx.guild.id).get('queue')) == 1: # Skipping when only one song in the queue will just kick the bot await self.__remove_active_channel(ctx.guild.id) - await ctx.message.delete() return True await self.__check_next_song(ctx.guild.id) - await self.__send_message_and_delete(Embed(title="Song Skipped!", colour=EmbedColours.music, time=5), - ctx.message) + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["song_skipped_success"], + colour=EmbedColours.music), + timer=5) return True - @commands.command() - async def listqueue(self, ctx: Context) -> bool: + @commands.command(aliases=["list", "queue"]) + async def listqueue(self, ctx: Context) -> str: """ Sends a message of the current queue to the channel the message was sent from. - :param ctx:The context of the message. - :return: A boolean if sending the message was successful. + :param ctx: The context of the message. + :type ctx: discord.ext.commands.Context + :return: The string representing the queue. + :rtype: str """ if not self._currently_active.get(ctx.guild.id): # Not currently active - await self.__send_message_and_delete(Embed(title="I am not currently active", colour=EmbedColours.music), - ctx.message) - return False + await send_timed_message(channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], colour=EmbedColours.music), + timer=10) + return "" # We don't want the song channel to be filled with the queue as it already shows it music_channel_in_db = self.__db_accessor.get('music_channels', params={'guild_id': ctx.guild.id}) if ctx.message.channel.id == music_channel_in_db[0].get('channel_id'): # Message is in the songs channel - await self.__send_message_and_delete(Embed(title="The queue is already visible in the music channel", - colour=EmbedColours.music), - ctx.message) - return False + message_title = self.user_strings["music_channel_wrong_channel"].format(command_option="cannot") + await send_timed_message(channel=ctx.channel, + embed=Embed(title=message_title, + colour=EmbedColours.music), timer=10) + return "" queue_string = self.__make_queue_list(ctx.guild.id) if await ctx.channel.send(queue_string) is not None: - return True - return False + return queue_string + return "" - @commands.command() + @commands.command(aliases=["clear", "empty"]) async def clearqueue(self, ctx: Context) -> bool: """ Clear the current queue of all songs. The bot won't leave the vc with this command. :param ctx: The context of the message. - :return: A boolean if the current queue was successfully cleared. + :type ctx: discord.ext.commands.Context + :return: Whether the queue was cleared in the guild that called the command. + :rtype: bool """ if not self._currently_active.get(ctx.guild.id): # Not currently active await self.__update_channel_messages(ctx.guild.id) - await ctx.message.delete() return False if not await self.__check_valid_user_vc(ctx): @@ -406,22 +496,27 @@ async def clearqueue(self, ctx: Context) -> bool: await self.__check_next_song(ctx.guild.id) await self.__update_channel_messages(ctx.guild.id) - await self.__send_message_and_delete(Embed(title="Queue Cleared!", colour=EmbedColours.music), ctx.message) + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["clear_queue_success"], + colour=EmbedColours.music), + timer=10) return True - @commands.command() + @commands.command(aliases=["shuffle", "randomise"]) async def shufflequeue(self, ctx: Context) -> bool: """ Shuffle the current queue of songs. Does not include the current song playing, which is index 0. Won't bother with a shuffle unless there are 3 or more songs. :param ctx: The context of the message. - :return: A boolean if the queue was shuffled. + :type ctx: discord.ext.commands.Context + :return: Whether the queue was shuffled in the guild that called the command. + :rtype: bool """ if not self._currently_active.get(ctx.guild.id): # Not currently active - await self.__send_message_and_delete(Embed(title="I am not currently active", colour=EmbedColours.music), - ctx.message) + await send_timed_message(channel=ctx.channel, + embed=Embed(title=self.user_strings["bot_inactive"], colour=EmbedColours.music), + timer=10) return False if not await self.__check_valid_user_vc(ctx): @@ -430,7 +525,6 @@ async def shufflequeue(self, ctx: Context) -> bool: if not len(self._currently_active.get(ctx.guild.id).get('queue')) > 2: # Nothing to shuffle - await ctx.message.delete() return False current_top = self._currently_active.get(ctx.guild.id).get('queue').pop(0) @@ -438,7 +532,9 @@ async def shufflequeue(self, ctx: Context) -> bool: self._currently_active.get(ctx.guild.id).get('queue').insert(0, current_top) await self.__update_channel_messages(ctx.guild.id) - await self.__send_message_and_delete(Embed(title="Queue shuffled!", colour=EmbedColours.green), ctx.message) + await send_timed_message(channel=ctx.channel, embed=Embed(title=self.user_strings["shuffle_queue_success"], + colour=EmbedColours.green), + timer=10) @tasks.loop(seconds=1) async def check_active_channels(self): @@ -487,20 +583,18 @@ async def check_marked_channels(self): # Stop the task when no channels to check self.check_marked_channels.stop() - @tasks.loop(hours=24) - async def reset_music_allowance(self): - """ - Reset the number of minutes a guild can use per day. Runs every 24hrs - """ - self._time_allocation = defaultdict(lambda: self._allowed_time) - async def __setup_channel(self, ctx: Context, channel_id: int, arg: str): """ Sends the preview and queue messages to the music channel and adds the ids of the messages to the database. If the music channel is not empty and the correct arg is set, also clears the channel. :param ctx: The context of the messages, used to send the messages to the channels. + :type ctx: discord.ext.commands.Context :param channel_id: The id of the music channel. + :type channel_id: int :param arg: Optional arg to perform extra utilities while setting the channel up. + :type arg: str + :return: None + :rtype: NoneType """ # Get a discord object of the channel. @@ -513,13 +607,12 @@ async def __setup_channel(self, ctx: Context, channel_id: int, arg: str): if len(channel_messages) > 1: # If there are messages in the channel. if arg is None: - await ctx.channel.send("The channel is not empty, if you want to clear the channel for use, " - f"use {self._bot.command_prefix}setmusicchannel -c ") + message = self.user_strings["music_channel_set_not_empty"].format(bot_prefix=self._bot.command_prefix) + await ctx.channel.send(message) elif arg == '-c': await channel_instance.purge(limit=int(sys.maxsize)) temp_default_preview = EMPTY_PREVIEW_MESSAGE.copy() - self.__add_time_remaining_field(ctx.guild.id, temp_default_preview) # Send the messages and record their ids. default_queue_message = await channel_instance.send(EMPTY_QUEUE_MESSAGE) @@ -535,8 +628,11 @@ async def __remove_active_channel(self, guild_id: int) -> bool: """ Disconnect the bot from the voice channel and remove it from the currently active channels. :param guild_id: The id of the guild to remove the bot from. - :return: True if the removal was successful, False otherwise. + :type guild_id: int + :return: If the guild specified was removed from the currently active channels. + :rtype: bool """ + if guild_id in self._currently_active: # If the guild is currently active. voice_client: VoiceClient = self._currently_active.get(guild_id).get('voice_client') @@ -548,29 +644,37 @@ async def __remove_active_channel(self, guild_id: int) -> bool: return False async def __check_next_song(self, guild_id: int): - # TODO: Check spam updates """ Check if there is another song to play after the current one. If no more songs, mark the channel as in active, otherwise play the next song. :param guild_id: The id of the guild to check the next song in. + :type guild_id: int + :return: None + :rtype: NoneType """ + if len(self._currently_active.get(guild_id).get('queue')) == 1: # The queue will be empty so will be marked as inactive self._currently_active.get(guild_id).get('queue').pop(0) self._marked_channels[guild_id] = time.time() await self.__update_channel_messages(guild_id) + if not self.check_marked_channels.is_running(): + self.check_marked_channels.start() elif len(self._currently_active.get(guild_id).get('queue')) > 1: # The queue is not empty, play the next song self._currently_active.get(guild_id).get('queue').pop(0) await self.__play_queue(guild_id) await self.__update_channel_messages(guild_id) - async def __add_song_to_queue(self, guild_id: int, song) -> bool: + async def __add_song_to_queue(self, guild_id: int, song: Union[dict, List[dict]]) -> bool: """ Add a list of songs or a single song to the queue. :param guild_id: The id of the guild to add the song to the queue. + :type guild_id: :param song: The song or list of songs. A song is a dict of information that is needed to play the song. - :return: True if the addition was successful. False otherwise. + :type song: Union[dict, List[dict]] + :return: If the song was added to the guild's queue or not. + :rtype: bool """ try: @@ -596,7 +700,9 @@ async def on_message_handle(self, message: Message) -> bool: """ The handle the is called whenever a message is sent in the music channel of a guild. :param message: The message sent to the channel. - :return: A boolean if the message was properly handled by the music cog. + :type message: discord.Message + :return: Whether the message was processed successfully as a music request. + :rtype: bool """ if message.content.startswith(self._bot.command_prefix): @@ -605,17 +711,17 @@ async def on_message_handle(self, message: Message) -> bool: if not message.author.voice: # User is not in a voice channel.. exit - await self.__send_message_and_delete(Embed(title="You must be in a voice channel to add a song", - colour=EmbedColours.orange), - message) + message_title = self.user_strings["no_voice_voice_channel"].format(author=message.author.mention) + await send_timed_message(channel=message.channel, + embed=Embed(title=message_title, + colour=EmbedColours.orange), timer=10) return True if not message.author.voice.channel.permissions_for(message.guild.me).connect: # The bot does not have permission to join the channel.. exit - await self.__send_message_and_delete(Embed(title="I need the permission `connect` " - "to be able to join that channel", - colour=EmbedColours.orange), - message) + message_title = self.user_strings["no_perms_voice_channel"].format(author=message.author.mention) + await send_timed_message(channel=message.channel, embed=Embed(title=message_title, + colour=EmbedColours.orange), timer=10) return True if not self._currently_active.get(message.guild.id): @@ -626,16 +732,19 @@ async def on_message_handle(self, message: Message) -> bool: else: if self._currently_active.get(message.guild.id).get('channel_id') != message.author.voice.channel.id: # The bot is already being used in the current guild. - await self.__send_message_and_delete(Embed(title="I am already in another voice channel in this server", - colour=EmbedColours.orange), - message) + message_title = self.user_strings["wrong_voice_voice_channel"].format(author=message.author.mention) + await send_timed_message(channel=message.channel, + embed=Embed(title=message_title, + colour=EmbedColours.orange), timer=10) return True # Check if the loops for marked and active channels are running. self.__check_loops_alive() # Splits multiline messages into a list. Single line messages return a list of [message] - split_message = message.content.split("\n") + cleaned_contents = message.content + cleaned_contents = re.sub(r"(`)+", "", cleaned_contents) + split_message = cleaned_contents.split("\n") split_message = [k for k in split_message if k not in ('', ' ')] partial_success = False total_success = True @@ -655,17 +764,18 @@ async def on_message_handle(self, message: Message) -> bool: self._marked_channels.pop(message.guild.id) if not total_success: - send = Embed(title="There were errors while adding some songs to the queue", colour=EmbedColours.red) + send = Embed(title=self.user_strings["song_error"], colour=EmbedColours.red) await send_timed_message(message.channel, embed=send, timer=10) - await message.delete() - async def process_song_request(self, message: Message, request: str) -> bool: """ Process the incoming message as a song request :param message: The instance of the discord message that sent the request. + :type message: discord.Message :param request: The contents of the request made. - :return The success value of if the song was added to the queue or not. + :type request: str + :return: Whether the request was able to be added to the queue of the guild. + :rtype: bool """ message_type = self.__determine_message_type(request) @@ -676,13 +786,20 @@ async def process_song_request(self, message: Message, request: str) -> bool: elif message_type == MessageTypeEnum.string: queried_song = self.__find_query(request) return await self.__add_song_to_queue(message.guild.id, queried_song) + elif message_type == MessageTypeEnum.invalid: + return False - def __get_youtube_api_info(self, request: str, message_type: int) -> Union[List[dict], None]: + @staticmethod + def __get_youtube_api_info(request: str, message_type: int) -> Union[List[dict], None]: """ Downloads the video information associated with a url as a list for each video in the request. :param request: The request to make to the YouTube API. + :type request: str :param message_type: The kind of request: Video url or Playlist url. - :return: A list of dicts for each video in the request. + :type message_type: int + :return: A list of dicts for the videos in the request each dict containing unique video information. + Or None if the request to the API failed + :rtype: Union[List[dict]] """ # Determines if we access the videos or playlist part of the YouTube API. @@ -714,13 +831,16 @@ def __get_youtube_api_info(self, request: str, message_type: int) -> Union[List[ return None return api_items - def __format_api_data(self, data: list) -> List[dict]: + def __format_api_data(self, data: List[dict]) -> List[dict]: """ Formats a list of data that was obtained from the YouTube API call, where each item in the list is a dict. - :param data: The list of dicts that was gained from the API call. + :param data: The list of dicts that obtained from the API call. + :type data: List[dict] :return: A formatted list of dicts. Each dict contains the YouTube url, the thumbnail url and the title of the video. + :rtype: List[dict] """ + formatted_data = [] for item in data: snippet = item.get("snippet") @@ -750,6 +870,9 @@ async def __update_channel_messages(self, guild_id: int): """ Update the queue and preview messages in the music channel. :param guild_id: The guild id of the guild to be updated. + :type guild_id: int + :return: None + :rtype: NoneType """ guild_db_data = self.__db_accessor.get('music_channels', params={'guild_id': guild_id})[0] @@ -780,7 +903,9 @@ def __make_updated_queue_message(self, guild_id: int) -> str: """ Update the queue message in a given guild. :param guild_id: The guild id of the guild to update the queue message in. - :return: A string of the queue that is to be the new queue message. + :type guild_id: int + :return: A string representing the queue. + :rtype: str """ if not self._currently_active.get(guild_id) or len(self._currently_active.get(guild_id).get('queue')) == 0: @@ -795,7 +920,9 @@ def __make_update_preview_message(self, guild_id: int) -> Embed: """ Update the preview message in a given guild. :param guild_id: The guild id of the guild to update the preview message in. - :return: An embed message for the updated preview message in a given guild id. + :type guild_id: int + :return: An Embed type that contains the updated information for the preview message in the given guild. + :rtype: discord.Embed """ if not self._currently_active.get(guild_id) or len(self._currently_active.get(guild_id).get('queue')) == 0: @@ -812,17 +939,18 @@ def __make_update_preview_message(self, guild_id: int) -> Embed: thumbnail = ESPORTS_LOGO_URL updated_preview_message.set_image(url=thumbnail) - self.__add_time_remaining_field(guild_id, updated_preview_message) - + updated_preview_message.set_footer(text="Definitely not made by fuxticks#1809 on discord") return updated_preview_message def __generate_link_data_from_queue(self, guild_id: int) -> Tuple[dict, dict]: """ - Get the opus stream, length and bitrate of the stream for a given YouTube url. + Download the actual song information such as the opus stream for the first song in the queue of a guild. :param guild_id: The guild id to get the queue from. - :return: A tuple of two dicts. First dict is the stream data, Second dict is the current song information - already gained. + :type guild_id: int + :return: A tuple of dicts. First dict containing playback data, Second dict containing song information. + :rtype: Tuple[dict, dict] """ + current_song = self._currently_active.get(guild_id).get('queue')[0] download_data = self.__download_video_info(current_song.get('link')) return self.__format_download_data(download_data), current_song @@ -835,16 +963,18 @@ def __check_loops_alive(self): self.check_active_channels.start() if not self.check_marked_channels.is_running(): self.check_marked_channels.start() - if not self.reset_music_allowance.is_running(): - self.reset_music_allowance.start() def __add_new_active_channel(self, guild_id: int, channel_id: str = None, voice_client: VoiceClient = None) -> bool: """ Add a new voice channel to the currently active channels. - :param guild_id: The id of the guild the voice channel is in. - :param channel_id: The id of the voice channel the bot is joining. - :param voice_client: The voice client instance of the bot. - :return: True if successfully added to the list of active channels. False otherwise. + :param guild_id: The id of the guild being made active. + :type guild_id: int + :param channel_id: The id of the voice channel the bot joined in the guild. + :type channel_id: int + :param voice_client: The instance of the bot's voice client + :type voice_client: discord.VoiceClient + :return: Whether the guild was able to be added to the currently active channels. + :rtype: bool """ if guild_id not in self._currently_active: @@ -854,6 +984,7 @@ def __add_new_active_channel(self, guild_id: int, channel_id: str = None, voice_ self._currently_active[guild_id]['voice_client'] = voice_client self._currently_active[guild_id]['queue'] = [] self._currently_active[guild_id]['current_song'] = None + self._currently_active[guild_id]['volume'] = 0.75 return True return False @@ -861,7 +992,9 @@ def __pause_song(self, guild_id: int) -> bool: """ Pauses the playback of a specific guild if the guild is playing. Otherwise nothing. :param guild_id: The id of the guild to pause the playback in. - :return: A boolean if the pause was successful. + :type guild_id: int + :return: Whether the guild's playback was paused. + :rtype: bool """ if self._currently_active.get(guild_id).get('voice_client').is_playing(): @@ -875,7 +1008,9 @@ def __resume_song(self, guild_id: int) -> bool: """ Resumes the playback of a specific guild if the guild is paused. Otherwise nothing. :param guild_id: The id of the guild to resume the playback in. - :return: A boolean if the playback resume was successful. + :type guild_id: int + :return: Whether the guild's playback was resumed. + :rtype: bool """ if self._currently_active.get(guild_id).get('voice_client').is_paused(): @@ -887,41 +1022,47 @@ def __resume_song(self, guild_id: int) -> bool: async def __check_valid_user_vc(self, ctx: Context) -> bool: """ - Checks if the user: A) Is in a voice channel, B) The voice channel is the same as the voice channel the bot is - connected to, C) The message sent was in the music text channel. + Checks if the user in in a valid voice channel, using the following criteria: + A) Is in a voice channel in the guild, + B) If the bot is in the voice channel, that the voice channel is the same as the user, + C) The message sent was in the music channel. :param ctx: The context of the message sent. - :return: If all the above conditions are met, True, otherwise False. + :type ctx: discord.ext.commands.Context + :return: True if all criteria are True, else False. + :rtype: bool """ music_channel_in_db = self.__db_accessor.get('music_channels', params={'guild_id': ctx.guild.id}) if ctx.message.channel.id != music_channel_in_db[0].get('channel_id'): # Message is not in the songs channel - await self.__send_message_and_delete(Embed(title="You are not in a valid voice channel", - colour=EmbedColours.music), - ctx.message) + message_title = self.user_strings["music_channel_wrong_channel"].format(command_option="can only") + await send_timed_message(channel=ctx.channel, embed=Embed(title=message_title, + colour=EmbedColours.music), timer=10) return False if not ctx.author.voice: # User is not in a voice channel - await self.__send_message_and_delete(Embed(title="You are not in a valid voice channel", - colour=EmbedColours.music), - ctx.message) + message_title = self.user_strings["no_voice_voice_channel"].format(author=ctx.author.mention) + await send_timed_message(channel=ctx.channel, embed=Embed(title=message_title, + colour=EmbedColours.music), timer=10) return False if self._currently_active.get(ctx.guild.id).get('channel_id') != ctx.author.voice.channel.id: # The user is not in the same voice channel as the bot - await self.__send_message_and_delete(Embed(title="You are not in a valid voice channel", - colour=EmbedColours.music), - ctx.message) + message_title = self.user_strings["wrong_voice_voice_channel"].format(author=ctx.author.mention) + await send_timed_message(channel=ctx.channel, embed=Embed(title=message_title, + colour=EmbedColours.music), timer=10) return False - return True - def __determine_message_type(self, message: str) -> int: + def __determine_message_type(self, message: str) -> MessageTypeEnum: """ - Determine if the message received is a video url, playlist url or a string that needs to be queried. - :param message: The message to determine the type of. - :return: An integer representing the message type. + Determine if the message received is a video url, playlist url, a string that needs to be queried, or some other + invalid url (not a YouTube url). + :param message: The string to determine the type of. + :type message: str + :return: An integer enum representing the message type. + :rtype: MessageTypeEnum """ if self.__is_url(message): @@ -932,14 +1073,21 @@ def __determine_message_type(self, message: str) -> int: else: return MessageTypeEnum.url else: - # The message is a string - return MessageTypeEnum.string + # TODO: Better URL identification + if "https://" in message or "http://" in message: + return MessageTypeEnum.invalid + else: + # The message is a string + return MessageTypeEnum.string - def __get_opus_stream(self, formats: list) -> Tuple[str, float]: + @staticmethod + def __get_opus_stream(formats: List[dict]) -> Tuple[str, float]: """ - Get the opus formatted streaming link from the formats dictionary. - :param formats: The formats dictionary that contains the different streaming links. - :return: A streaming url that links to an opus stream and the bit rate of the stream. + Get the opus formatted streaming url from the formats dictionary. + :param formats: A list format dictionaries that contain the different streaming urls. + :type formats: List[dict] + :return: A tuple of the opus stream url and the bitrate of the url. + :rtype: Tuple[str, float] """ # Limit the codecs to just opus, as that is required for streaming audio @@ -954,9 +1102,11 @@ def __get_opus_stream(self, formats: list) -> Tuple[str, float]: def __format_download_data(self, download_data: dict) -> dict: """ - Format a songs data to remove the useless data. + Format a songs data to only keep useful data. :param download_data: The song data to format. - :return: A dictionary of data which is a subset of the param download_data + :type download_data: + :return: A dictionary formatted to keep useful data from the download_data param. + :rtype: dict """ stream, rate = self.__get_opus_stream(download_data.get('formats')) @@ -966,26 +1116,32 @@ def __format_download_data(self, download_data: dict) -> dict: 'filename': download_data.get('filename')} return useful_data - def __is_url(self, string: str) -> bool: + @staticmethod + def __is_url(string: str) -> bool: """ - Returns if the string given is a url. + Checks if the string given is a YouTube url or a YouTube thumbnail url. :param string: The string to check. - :return: True if the string is a url. False otherwise. + :type string: str + :return: True if the string is a YouTube url, False otherwise. + :rtype: bool """ - # Match desktop, mobile and playlist links + # Match desktop, mobile and playlist urls re_desktop = r'(http[s]?://)?youtube.com/(watch\?v)|(playlist\?list)=' re_mobile = r'(http[s]?://)?youtu.be/([a-zA-Z]|[0-9])+' re_thumbnail = r'(http[s]?://)?i.ytimg.com/vi/([a-zA-Z]|[0-9])+' return bool(re.search(re_desktop, string) or re.search(re_mobile, string) or re.search(re_thumbnail, string)) - def __download_video_info(self, link: str, download: bool = False) -> dict: + def __download_video_info(self, url: str, download: bool = False) -> dict: """ - Download all the information about a given link from YouTube. - :param link: The link to find the information about. - :param download: If the song should also be downloaded to a file. - :return: The information about a YouTube url. + Download all the information about a given YouTube url. + :param url: The YouTube url. + :type url: str + :param download: If the video should be downloaded to a local file. + :type download: bool + :return: A dictionary of information pertaining to the YouTube url. + :rtype: dict """ ydl_opts = { @@ -998,36 +1154,19 @@ def __download_video_info(self, link: str, download: bool = False) -> dict: }], } with youtube_dl.YoutubeDL(ydl_opts) as ydl: - info = ydl.extract_info(link, download=download) + info = ydl.extract_info(url, download=download) file = ydl.prepare_filename(info) info['filename'] = file return info - def __add_time_remaining_field(self, guild_id: int, embed: Embed): - """ - Create the field for an embed that displays how much time a server has left to play songs in that day. - :param guild_id: The guild id of the guild. - :param embed: The embed message to add the field to. - """ - - # Get the time remaining - guild_time = self._time_allocation[guild_id] - guild_time_string = time.strftime('%H:%M:%S', time.gmtime(guild_time)) - - # Get the total time allowed. - allowed_time = self._allowed_time - allowed_time_string = time.strftime('%H:%M:%S', time.gmtime(allowed_time)) - - # Add the field to the embed. - embed.add_field(name=f"Time Remaining Today: {guild_time_string} / {allowed_time_string}", - value="Blame Ryan :upside_down:") - def __check_empty_vc(self, guild_id: int) -> bool: """ - Checks if the voice channel the bot is in has no members in it. - :param guild_id: The id of the guild that is being checked. - :return: True if the channel is empty or if the bot isn't in a channel. False otherwise. + Check if the voice channel the bot is in has no members in it. + :param guild_id: The id of the guild being checked. + :type guild_id: int + :return: True if the channel is empty or if the bot isn't in a channel, else False. + :rtype: bool """ voice_client = self._currently_active.get(guild_id).get('voice_client') @@ -1044,8 +1183,10 @@ def __check_empty_vc(self, guild_id: int) -> bool: async def __play_queue(self, guild_id: int) -> bool: """ Starts the playback of the song at the top of the queue. - :param guild_id: The id of the guild the bot is playing in. - :return: True if the playback was successful, False otherwise. + :param guild_id: The id of the guild to play in. + :type guild_id: int + :return: Whether the bot was able to start playing the queue. + :rtype: bool """ if len(self._currently_active.get(guild_id).get('queue')) < 1: @@ -1061,38 +1202,25 @@ async def __play_queue(self, guild_id: int) -> bool: # Get the next song extra_data, current_song = self.__generate_link_data_from_queue(guild_id) next_song = {**current_song, **extra_data} - length = next_song.get("length") - - time_remaining = self._time_allocation[guild_id] - length - - if time_remaining <= 0: - # If the allocated time is used up set it to 0 and exit out - message = Embed(title="The current song is too long for the remaining time today, trying the next song.", - colour=EmbedColours.orange) - channel_id = self.__db_accessor.get('music_channels', params={'guild_id': guild_id})[0].get('channel_id') - channel = self._bot.get_channel(channel_id) - if channel is None: - channel = await self._bot.fetch_channel(channel_id) - await send_timed_message(channel=channel, embed=message, timer=5) - self._currently_active.get(guild_id)["queue"] = [None] + self._currently_active.get(guild_id).get("queue") - await self.__check_next_song(guild_id) - return True - - self._time_allocation[guild_id] = self._time_allocation[guild_id] - length self._currently_active.get(guild_id)['current_song'] = next_song - voice_client.play(FFmpegOpusAudio(next_song.get("stream"), before_options=FFMPEG_BEFORE_OPT, - bitrate=int(next_song.get("bitrate")) + 10)) - voice_client.volume = 100 + # voice_client.play(FFmpegOpusAudio(next_song.get("stream"), before_options=FFMPEG_BEFORE_OPT, + # bitrate=int(next_song.get("bitrate")) + 10)) + source = PCMVolumeTransformer(FFmpegPCMAudio(next_song.get("stream"), + before_options=FFMPEG_BEFORE_OPT, options="-vn"), + volume=self._currently_active.get(guild_id).get("volume")) + voice_client.play(source) return True def __make_queue_list(self, guild_id: int) -> str: """ - Create a formatted string representing the queue from a server. - :param guild_id: The guild of the queue to turn into a string. - :return: A string representing the queue list. + Create a formatted string representing the queue from a guild. + :param guild_id: The guild to get the queue from. + :type guild_id: int + :return: A string representing the queue. + :rtype: str """ queue_string = EMPTY_QUEUE_MESSAGE @@ -1104,32 +1232,40 @@ def __make_queue_list(self, guild_id: int) -> str: extra = len(self._currently_active.get(guild_id).get('queue')) - 20 first_string = self.__song_list_to_string(first_part) - last_string = self.__song_list_to_string(last_part) + last_string = self.__song_list_to_string(last_part, start_index=extra+10) - queue_string += f"{first_string}... and `{extra}` more \n{last_string}" + queue_string += f"{first_string}\n\n... and **`{extra}`** more ... \n\n{last_string}" else: queue_string += self.__song_list_to_string(self._currently_active.get(guild_id).get('queue')) return queue_string - def __song_list_to_string(self, songs: list) -> str: + @staticmethod + def __song_list_to_string(songs: List[dict], start_index: int = 0) -> str: """ Turn a list into a string. :param songs: The list of songs to turn into a string. - :return: A string representing a queue list. + :type songs: List[dict] + :return: A string representing a queue. + :rtype: str """ - return "\n".join(str(songNum + 1) + ". " + song.get('title') for songNum, song in enumerate(songs)) + + return "\n".join(str(songNum + 1 + start_index) + ". " + + song.get('title') for songNum, song in enumerate(songs)) def __find_query(self, message: str) -> dict: """ - Query YouTube to find a search result and get return a link to the top hit of that query. + Query YouTube to find a search result and get return a url to the top hit of that query. :param message: The message to query YouTube with. - :return: A link and some other basic information regarding the query. + :type message: str + :return: A url and some other basic information regarding the video found. + :rtype: dict """ - # Finds the required data for + # Find the top results for a given query to YouTube. search_info = self.__query_youtube(message) + # top_hit is the actual top hit, while best_audio_hit is the top hit when searching for lyrics or audio queries. top_hit = search_info[-1] best_audio_hit = search_info[0] @@ -1143,11 +1279,13 @@ def __find_query(self, message: str) -> dict: return best_audio_hit - def __clean_query_results(self, results: list) -> list: + def __clean_query_results(self, results: List[dict]) -> List[dict]: """ Remove unnecessary data from a list of dicts gained from querying YouTube. - :param results: The list of YouTube information gathered from the query. - :return: A list of dicts containing the title, thumbnail url, video url and view count of a video. + :param results: A list of dictionaries gained from a YouTube query. + :type results: List[dict] + :return: A list of dictionaries with the useful information kept. + :rtype: List[dict] """ cleaned_data = [] @@ -1166,11 +1304,13 @@ def __clean_query_results(self, results: list) -> list: return cleaned_data - def __query_youtube(self, message: str) -> list: + def __query_youtube(self, message: str) -> List[dict]: """ Search YouTube with a given string. :param message: The message to query YouTube with. - :return: A dictionary having the information about the query. + :type message: str + :return: A list of dictionaries for each result returned from the query. + :rtype: List[dict] """ start = time.time() @@ -1178,7 +1318,8 @@ def __query_youtube(self, message: str) -> list: # Sort the list by view count top_results = sorted(results, - key=lambda k: int(re.sub(r'view(s)?', '', k['viewCount']['text']).replace(',', '')), + key=lambda k: 0 if k["viewCount"]["text"] is None or "No" in k["viewCount"]["text"] else + int(re.sub(r'view(s)?', '', k['viewCount']['text']).replace(',', '')), reverse=True) music_results = [] @@ -1201,7 +1342,7 @@ def __query_youtube(self, message: str) -> list: end = time.time() - print("Time taken to query youtube: " + str(end - start)) + print("Time taken to query YouTube: " + str(end - start)) return cleaned_results diff --git a/src/esportsbot/lib/client.py b/src/esportsbot/lib/client.py index 974095db..a351c656 100644 --- a/src/esportsbot/lib/client.py +++ b/src/esportsbot/lib/client.py @@ -42,10 +42,17 @@ def __init__(self, command_prefix: str, unknownCommandEmoji: Emote, userStringsF self.reactionMenus = ReactionMenuDB() self.unknownCommandEmoji = unknownCommandEmoji self.STRINGS: StringTable = toml.load(userStringsFile) + self.MUSIC_CHANNELS = {} signal.signal(signal.SIGINT, self.interruptReceived) # keyboard interrupt signal.signal(signal.SIGTERM, self.interruptReceived) # graceful exit request + def update_music_channels(self): + self.MUSIC_CHANNELS = {} + temp_channels = db_gateway().pure_return("SELECT guild_id, channel_id FROM music_channels") + for item in temp_channels: + self.MUSIC_CHANNELS[item.get("guild_id")] = item.get("channel_id") + return self.MUSIC_CHANNELS def interruptReceived(self, signum: signal.Signals, frame: FrameType): """Shut down the bot gracefully. diff --git a/src/esportsbot/user_strings.toml b/src/esportsbot/user_strings.toml index 6bd3229f..c6fc9e32 100644 --- a/src/esportsbot/user_strings.toml +++ b/src/esportsbot/user_strings.toml @@ -24,6 +24,52 @@ default_role_get = "Default role is set to {role_id}" default_role_removed = "Default role has been removed" default_role_removed_log = "{author_mention} has removed the default role" +[music] +music_channel_set = "The Music Channel has been bound to {music_channel}" +music_channel_set_log = "{author} has bound the Music Channel to {music_channel}" +music_channel_set_missing_channel = "You need to either use a # to mention the channel or paste the ID of the channel" +music_channel_set_invalid_channel = """The channel given was not valid, check the ID pasted or try using a # to mention + the channel""" +music_channel_set_not_text_channel = "You must provide a Text Channel to bind as the Music Channel" +music_channel_set_not_empty = """The channel given is not empty, if you want to clear the channel + use {bot_prefix}setmusicchannel -c """ + +music_channel_get = "The Music Channel is currently set to {music_channel}" +music_channel_missing = "The Music Channel has not been bound" + +music_channel_reset = "The Music Channel ({music_channel}) has been reset" + +music_channel_removed = "The Music Channel has been unbound from {music_channel}" +music_channel_removed_log = "{author} has unbound the Music Channel from {music_channel}" + +bot_inactive = "I am not currently active. Start playing some songs first by joining a channel and requesting one!" +song_error = "There were errors while adding some songs to the queue" + +music_channel_wrong_channel = "That command {command_option} be sent in the Music Channel" + +no_perms_voice_channel = "{author}, I need the permission `connect` to be able to join that Voice Channel" +no_voice_voice_channel = "{author}, You must be in a voice channel to request a song" +wrong_voice_voice_channel = "{author}, I am already in another voice channel in this server" + +volume_set_invalid_value = "The volume level must be between 0 and 100" +volume_set_success = "The volume has been set to {volume_level}%" + +song_remove_invalid_value = "The song number must be a value in the current queue." +song_remove_valid_options = "Valid options are from {start_index} to {end_index}" +song_remove_success = "The song **{song_title}** has been removed from position **{song_position}** in the queue" + +song_pause_success = "Song Paused!" + +song_resume_success = "Song Resumed!" + +song_skipped_success = "Song Skipped!" + +kick_bot_success = "I have left the Voice Channel and emptied the queue" + +clear_queue_success = "Queue Cleared!" + +shuffle_queue_success = "Queue Shuffled!" + [event_categories] success_channel = "✅ <#{channel_id!s}> is now visible to **{role_name}**!" success_event = """✅ New event category '{event_title}' created successfuly! @@ -92,4 +138,4 @@ admin_menu_updated = ["Event signin menu updated", "Event name: {event_title}\nM admin_role_menu_reset = ["Role menu reset", "id: {menu_id}\ntype: {menu_type}\n[Menu]({menu_url})"] admin_role_removed = ["Event Role removed", "Users: {users!s}\n<@&{event_role_id!s}>"] admin_shared_role_set = ["Shared role set", "<@&{role_id!s}>"] -admin_signin_visible = ["Event signin channel made visible", "<#{channel_id}>"] \ No newline at end of file +admin_signin_visible = ["Event signin channel made visible", "<#{channel_id}>"]