From 80d1cdc184b40c24002185766053a23aa8c18e83 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Fri, 11 Sep 2020 16:46:46 +0200 Subject: [PATCH] Beginning of large restructuring of music cog. Restructures the music cog into a module Renames cogs to cogmanager Fixes subcommand help invocation More to assignment expressions in settings --- bot.py | 2 +- cogs/{cogs.py => cogmanager.py} | 32 +- cogs/errors.py | 3 +- cogs/helpformatter.py | 31 +- cogs/music.py | 719 -------------------------- cogs/music/__init__.py | 104 ++++ cogs/music/advanced_commands.py | 0 cogs/music/basic_commands.py | 653 +++++++++++++++++++++++ cogs/music/events.py | 71 +++ cogs/{utils => music}/music_errors.py | 0 cogs/musicevents.py | 85 --- cogs/nodemanager.py | 2 +- cogs/settings.py | 40 +- cogs/utils/decorators.py | 3 +- localization/en_en/commands.yaml | 7 +- localization/en_en/nodemanager.yaml | 2 - localization/nb_no/commands.yaml | 4 +- localization/nb_no/nodemanager.yaml | 2 - localization/we_eb/commands.yaml | 4 +- 19 files changed, 889 insertions(+), 875 deletions(-) rename cogs/{cogs.py => cogmanager.py} (70%) delete mode 100644 cogs/music.py create mode 100644 cogs/music/__init__.py create mode 100644 cogs/music/advanced_commands.py create mode 100644 cogs/music/basic_commands.py create mode 100644 cogs/music/events.py rename cogs/{utils => music}/music_errors.py (100%) delete mode 100644 cogs/musicevents.py delete mode 100644 localization/en_en/nodemanager.yaml delete mode 100644 localization/nb_no/nodemanager.yaml diff --git a/bot.py b/bot.py index 9b3d0e0..956de03 100644 --- a/bot.py +++ b/bot.py @@ -20,7 +20,7 @@ from cogs.utils.settingsmanager import Settings initial_extensions = [ 'cogs.errors', - 'cogs.cogs', + 'cogs.cogmanager', 'cogs.settings', 'cogs.misc', 'cogs.helpformatter' diff --git a/cogs/cogs.py b/cogs/cogmanager.py similarity index 70% rename from cogs/cogs.py rename to cogs/cogmanager.py index 1c00b3d..f811fa0 100644 --- a/cogs/cogs.py +++ b/cogs/cogmanager.py @@ -3,20 +3,26 @@ from discord.ext import commands import traceback +# Bot Utilities +from cogs.helpformatter import commandhelper +from cogs.utils.paginator import Scroller -class Cogs(commands.Cog): + +class CogManager(commands.Cog): def __init__(self, bot): self.bot = bot self.settings = self.bot.settings - @commands.group(name='cogs', hidden=True) + @commands.group(name='cogmanager', hidden=True) @commands.is_owner() - async def _cogs(self, ctx): + async def _cogmanager(self, ctx): if ctx.invoked_subcommand is None: - await ctx.invoke(self.bot.get_command('help'), - ctx.command.qualified_name) + ctx.localizer.prefix = 'help' # Ensure the bot looks for locales in the context of help, not cogmanager. + paginator = commandhelper(ctx, ctx.command, ctx.invoker, include_subcmd=True) + scroller = Scroller(ctx, paginator) + await scroller.start_scrolling() - @_cogs.command() + @_cogmanager.command() @commands.is_owner() async def load(self, ctx, *, module): """Loads a module.""" @@ -26,11 +32,11 @@ class Cogs(commands.Cog): except Exception: await ctx.send(f'```py\n{traceback.format_exc()}\n```') - @_cogs.command() + @_cogmanager.command() @commands.is_owner() async def unload(self, ctx, *, module): """Unloads a module.""" - if module == "cogs": + if module == "cogmanager": return await ctx.send('Unloading this cog is not allowed') try: self.bot.unload_extension(f'cogs.{module}') @@ -38,7 +44,7 @@ class Cogs(commands.Cog): except Exception: await ctx.send(f'```py\n{traceback.format_exc()}\n```') - @_cogs.command(name='reload') + @_cogmanager.command(name='reload') @commands.is_owner() async def _reload(self, ctx, *, module): """Reloads a module.""" @@ -49,13 +55,13 @@ class Cogs(commands.Cog): except Exception: await ctx.send(f'```py\n{traceback.format_exc()}\n```') - @_cogs.command(name='reloadall') + @_cogmanager.command(name='reloadall') @commands.is_owner() async def _relaod_all(self, ctx): """Reloads all extensions""" try: for extension in self.bot.extensions: - if extension == 'cogs.cogs': + if extension == 'cogs.cogmanager': continue self.bot.unload_extension(f'{extension}') self.bot.load_extension(f'{extension}') @@ -63,7 +69,7 @@ class Cogs(commands.Cog): except Exception: await ctx.send(f'```py\n{traceback.format_exc()}\n```') - @_cogs.command(name='shutdown') + @_cogmanager.command(name='shutdown') @commands.is_owner() async def _shutdown(self, ctx): """Logs out and stops.""" @@ -72,4 +78,4 @@ class Cogs(commands.Cog): def setup(bot): - bot.add_cog(Cogs(bot)) + bot.add_cog(CogManager(bot)) diff --git a/cogs/errors.py b/cogs/errors.py index 4ffb5ea..e81588d 100644 --- a/cogs/errors.py +++ b/cogs/errors.py @@ -4,9 +4,10 @@ from discord.ext import commands import traceback +from cogs.music.music_errors import WrongTextChannelError, WrongVoiceChannelError + # Bot Utilities from cogs.helpformatter import commandhelper -from cogs.utils.music_errors import WrongTextChannelError, WrongVoiceChannelError from cogs.utils.paginator import Scroller diff --git a/cogs/helpformatter.py b/cogs/helpformatter.py index 2f77feb..8e42960 100644 --- a/cogs/helpformatter.py +++ b/cogs/helpformatter.py @@ -42,21 +42,24 @@ async def helper(ctx): async def coghelper(ctx, cog, ignore_subcommands=True): paginator = HelpPaginator(max_size=5000, max_fields=5, color=ctx.me.color, title=cog.__cog_name__) for cmd in cog.walk_commands(): - if ignore_subcommands: - if len(cmd.qualified_name.split()) > 1: - continue - if cmd.hidden: - continue try: - can_run = await cmd.can_run(ctx) - except commands.CommandError: # Was 'CommandError' guessed it was supposed to be what it is now - can_run = False - if not can_run: - continue - cmd_dict = get_cmd_dict(ctx, cmd.qualified_name) - if cmd_dict: - paginator.add_command_field(cmd_dict) - + if ignore_subcommands: + if len(cmd.qualified_name.split()) > 1: + continue + if cmd.hidden: + continue + try: + can_run = await cmd.can_run(ctx) + except commands.CommandError: # Was 'CommandError' guessed it was supposed to be what it is now + can_run = False + if not can_run: + continue + cmd_dict = get_cmd_dict(ctx, cmd.qualified_name) + if cmd_dict: + paginator.add_command_field(cmd_dict) + except Exception as e: + print(e) + print("Help failed for command: ", cmd) paginator.add_page_indicator(ctx.localizer, "{pageindicator}", _prefix=ctx.prefix) return paginator diff --git a/cogs/music.py b/cogs/music.py deleted file mode 100644 index 95ddd94..0000000 --- a/cogs/music.py +++ /dev/null @@ -1,719 +0,0 @@ -""" -Music commands -""" -# Discord Packages -import discord -import lavalink -from discord.ext import commands - -import asyncio -import json -import math -import re -import urllib - -from bs4 import BeautifulSoup - -from .utils import checks, thumbnailer, timeformatter -from .utils.decorators import require_playing, require_queue, require_voice_connection -from .utils.music_errors import WrongTextChannelError -from .utils.paginator import QueuePaginator, Scroller, TextPaginator -from .utils.selector import Selector - -time_rx = re.compile('[0-9]+') -url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') - - -class Music(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.logger = self.bot.main_logger.bot_logger.getChild("Music") - - async def cog_check(self, ctx): - if not ctx.guild: - raise commands.NoPrivateMessage - if textchannels := self.bot.settings.get(ctx.guild, 'channels.text', []): - if ctx.channel.id not in textchannels: - raise WrongTextChannelError('You need to be in the right text channel', channels=textchannels) - return True - - async def cog_before_invoke(self, ctx): - """ Ensures a valic player exists whenever a command is run """ - region = ctx.guild.region if isinstance(ctx.guild.region, str) else ctx.guild.region.value - # Creates a new only if one doesn't exist, ensures a valid player for all checks. - self.bot.lavalink.player_manager.create(ctx.guild.id, endpoint=region) - - async def connect_to(self, guild_id: int, channel_id: str): - """ Connects to the given voicechannel ID. A channel_id of `None` means disconnect. """ - ws = self.bot._connection._get_websocket(guild_id) - await ws.voice_state(str(guild_id), channel_id) - - @commands.command(name='play') - @require_voice_connection(should_connect=True) - async def _play(self, ctx, *, query: str): - """ Searches and plays a song from a given query. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - query = query.strip('<>') - - if not url_rx.match(query): - query = f'ytsearch:{query}' - - results = await player.node.get_tracks(query) - - if not results or not results['tracks']: - return await ctx.send(ctx.localizer.format_str("{nothing_found}")) - - embed = discord.Embed(color=ctx.me.color) - - if results['loadType'] == 'PLAYLIST_LOADED': - tracks = results['tracks'] - - if maxlength := self.max_track_length(ctx.guild, player): - numtracks = 0 - for track in tracks: - if track['info']['length'] <= maxlength: - thumbnail_url = await thumbnailer.ThumbNailer.identify( - self, track['info']['identifier'], track['info']['uri']) - track = lavalink.models.AudioTrack(track, ctx.author.id, thumbnail_url=thumbnail_url) - player.add(requester=ctx.author.id, track=track) - numtracks += 1 - else: - numtracks = len(tracks) - for track in tracks: - thumbnail_url = await thumbnailer.ThumbNailer.identify( - self, track['info']['identifier'], track['info']['uri']) - track = lavalink.models.AudioTrack(track, ctx.author.id, thumbnail_url=thumbnail_url) - player.add(requester=ctx.author.id, track=track) - - embed.title = '{playlist_enqued}' - embed.description = f'{results["playlistInfo"]["name"]} - {numtracks} {{tracks}}' - embed = ctx.localizer.format_embed(embed) - await ctx.send(embed=embed) - else: - track = results['tracks'][0] - await self.enqueue(ctx, track, embed) - embed = ctx.localizer.format_embed(embed) - await ctx.send(embed=embed) - - if not player.is_playing: - await player.play() - - @commands.command(name='seek') - @checks.dj_or(alone=True, track_requester=True) - @require_voice_connection() - @require_playing(require_user_listening=True) - async def _seek(self, ctx, *, time: str): - """ Seeks to a given position in a track. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if seconds := time_rx.search(time): - # Convert to milliseconds, include sign - milliseconds = int(seconds.group())*1000 * (-1 if time.startswith('-1') else 1) - - track_time = player.position + milliseconds - await player.seek(track_time) - msg = ctx.localizer.format_str("{seek.track_moved}", _position=timeformatter.format_ms(track_time)) - await ctx.send(msg) - else: - await ctx.send(ctx.localizer.format_str("{seek.missing_amount}")) - - @commands.command(name='skip') - @require_voice_connection() - @require_playing() - async def _skip(self, ctx): - """ Skips the current track. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - player.add_skipper(ctx.author) - total = len(player.listeners) - skips = len(player.skip_voters) - threshold = self.bot.settings.get(ctx.guild, 'vote_threshold', 'default_threshold') - - if skips/total >= threshold/100 or player.current.requester == ctx.author.id: - await player.skip() - if player.current: - song = f'**[{player.current.title}]({player.current.uri})**' - embed = discord.Embed(color=ctx.me.color, description=song, title='{now}') - thumbnail_url = player.current.extra["thumbnail_url"] - member = ctx.guild.get_member(player.current.requester) - if thumbnail_url: - embed.set_thumbnail(url=thumbnail_url) - if member.nick: - member_identifier = member.nick - else: - member_identifier = member.name - embed.set_footer(text=f'{{requested_by}} {member_identifier}', icon_url=member.avatar_url) - - embed = ctx.localizer.format_embed(embed) - await ctx.send(ctx.localizer.format_str("{skip.skipped}"), embed=embed) - else: - await ctx.send(ctx.localizer.format_str("{skip.skipped}")) - else: - if skips != 0: - needed = math.ceil(total*threshold/100) - msg = ctx.localizer.format_str("{skip.require_vote}", _skips=skips, _total=needed) - await ctx.send(msg) - - @commands.command(name='skipto') - @checks.dj_or(alone=True) - @require_playing(require_user_listening=True) - async def _skip_to(self, ctx, pos: int = 1): - """ Plays the queue from a specific point. Disregards tracks before the pos. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - if pos < 1: - return await ctx.send(ctx.localizer.format_str("{skip_to.invalid_pos}")) - if len(player.queue) < pos: - return await ctx.send(ctx.localizer.format_str("{skip_to.exceeds_queue}")) - - await player.skip(pos - 1) - msg = ctx.localizer.format_str("{skip_to.skipped_to}", _title=player.current.title, _pos=pos) - await ctx.send(msg) - - @commands.command(name='stop') - @checks.dj_or(alone=True) - @require_voice_connection() - @require_playing(require_user_listening=True) - async def _stop(self, ctx): - """ Stops the player and clears its queue. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - player.queue.clear() - await player.stop() - - embed = discord.Embed(color=ctx.me.color, title='{stop}') - await ctx.send(embed=ctx.localizer.format_embed(embed)) - - @commands.command(name='now') - @require_playing() - async def _now(self, ctx): - """ Shows some stats about the currently playing song. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - position = timeformatter.format_ms(player.position) - if player.current.stream: - duration = '{live}' - else: - duration = timeformatter.format_ms(player.current.duration) - song = f'**[{player.current.title}]({player.current.uri})**\n({position}/{duration})' - - member = ctx.guild.get_member(player.current.requester) - embed = discord.Embed(color=ctx.me.color, description=song, title='{now}') - thumbnail_url = player.current.extra["thumbnail_url"] - if thumbnail_url: - embed.set_thumbnail(url=thumbnail_url) - - if member.nick: - member_identifier = member.nick - else: - member_identifier = member.name - embed.set_footer(text=f'{{requested_by}} {member_identifier}', icon_url=member.avatar_url) - - embed = ctx.localizer.format_embed(embed) - await ctx.send(embed=embed) - - @commands.command(name='queue') - @require_queue(require_member_queue=True) - async def _queue(self, ctx, *, member: discord.Member = None): - """ Shows the player's queue. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if member is None: - queue = player.global_queue() - pagified_queue = QueuePaginator(ctx.localizer, queue, color=ctx.me.color) - - else: - member_queue = player.user_queue(member.id, dual=True) - pagified_queue = QueuePaginator(ctx.localizer, member_queue, color=ctx.me.color, - user_name=member.display_name) - - scroller = Scroller(ctx, pagified_queue) - await scroller.start_scrolling() - - @commands.command(name='myqueue') - @require_queue(require_author_queue=True) - async def _myqueue(self, ctx): - """ Shows your queue. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - user_queue = player.user_queue(ctx.author.id, dual=True) - pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, - user_name=ctx.author.display_name) - scroller = Scroller(ctx, pagified_queue) - await scroller.start_scrolling() - - @commands.command(name='pause') - @checks.dj_or(alone=True) - @require_voice_connection() - @require_playing() - async def _pause(self, ctx): - """ Pauses/Resumes the current track. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - if player.paused: - await player.set_pause(False) - await ctx.send(ctx.localizer.format_str("{resume.resumed}")) - else: - await player.set_pause(True) - await ctx.send(ctx.localizer.format_str("{resume.paused}")) - - @commands.command(name='shuffle') - @require_voice_connection() - @require_queue(require_author_queue=True) - async def _shuffle(self, ctx): - """ Shuffles your queue. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - player.shuffle_user_queue(ctx.author.id) - await ctx.send(ctx.localizer.format_str("{shuffle}")) - - @commands.command(name='move') - @require_voice_connection() - @require_queue(require_author_queue=True) - async def _move(self, ctx): - """ Moves a song in your queue. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - user_queue = player.user_queue(ctx.author.id, dual=True) - - # Setup for selector - identifiers = [] - for index, temp in enumerate(user_queue): - track, globpos = temp - identifiers.append(ctx.localizer.format_str("{queue.usertrack}", _index=index+1, - _globalindex=globpos+1, _title=track.title, - _uri=track.uri)) - - def ret_first_arg(*args): - return args[0] - - functions = [ret_first_arg]*len(user_queue) - arguments = [(i,) for i in range(len(user_queue))] - - # Set the different titles for the different selections - round_titles = ["{moved.choose_song}", "{moved.choose_pos}"] - round_titles = [ctx.localizer.format_str(i) for i in round_titles] - - selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, round_titles=round_titles, - color=ctx.me.color, title=ctx.localizer.format_str("{moved.choose}")) - - # Get the initial and final position - message, page, selections = await selector.start_scrolling() - pos_initial, pos_final = selections[0], selections[1] - - # Move the track - moved = player.move_user_track(ctx.author.id, pos_initial, pos_final) - - # Create a nice embed explaining what happened - song = f'**[{moved.title}]({moved.uri})**' - embed = discord.Embed(color=ctx.me.color, description=song, title='{moved.moved}') - thumbnail_url = moved.extra["thumbnail_url"] - member = ctx.guild.get_member(moved.requester) - if thumbnail_url: - embed.set_thumbnail(url=thumbnail_url) - if member.nick: - member_identifier = member.nick - else: - member_identifier = member.name - - embed.add_field(name="{moved.local}", value=f"`{pos_initial + 1} → {pos_final + 1}`", inline=True) - embed.add_field(name="{moved.global}", value=f"`{player.queue._loc_to_glob(ctx.author.id, pos_initial) + 1}\ - → {player.queue._loc_to_glob(ctx.author.id, pos_final) + 1}`", inline=True) - embed.set_footer(text=f'{{requested_by}} {member_identifier}', icon_url=member.avatar_url) - embed = ctx.localizer.format_embed(embed) - - await message.edit(embed=embed) - - @commands.command(name='remove') - @require_voice_connection() - @require_queue(require_author_queue=True) - async def _remove(self, ctx): - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - user_queue = player.user_queue(ctx.author.id, dual=True) - functions = [player.remove_user_track]*len(user_queue) - arguments = [(ctx.author.id, i) for i in range(len(user_queue))] - identifiers = [] - for index, temp in enumerate(user_queue): - track, globpos = temp - identifiers.append(ctx.localizer.format_str("{queue.usertrack}", _index=index+1, - _globalindex=globpos+1, _title=track.title, - _uri=track.uri)) - - selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, color=0xFF0000, - title='Remove song plz') - message, _, removed = await selector.start_scrolling() - await message.edit(content=ctx.localizer.format_str("{remove}", _title=removed.title), embed=None) - - @commands.command(name="DJremove") - @checks.dj_or() - @require_voice_connection() - @require_queue() - async def _djremove(self, ctx, pos: int, member: discord.Member = None): - """ Remove a song from either the global queue or a users queue""" - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - # TODO: add way to allow "member" as regular arg in require_queue() instead of only kwargs - queue = player.user_queue(member.id) if member else player.queue - - if pos <= len(queue) and pos >= 1: - if member is None: - removed = player.remove_global_track(pos - 1) - else: - removed = player.remove_user_track(member.id, pos - 1) - - requester = self.bot.get_user(removed.requester) - await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, _user=requester.name)) - else: - await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(queue))) - - @commands.command(name='removeuser') - @checks.dj_or(alone=True) - @require_voice_connection() - @require_queue(require_member_queue=True) - async def _user_queue_remove(self, ctx, *, member: discord.Member): - """ Remove a song from either the global queue or a users queue""" - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - player.remove_user_queue(member.id) - - embed = discord.Embed(description="{dj_remove_user}", color=ctx.me.color) - embed = ctx.localizer.format_embed(embed, _id=member.id) - - await ctx.send(embed=embed) - - @commands.command(name='search') - @require_voice_connection(should_connect=True) - async def _search(self, ctx, *, query): - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - if not query.startswith('ytsearch:') and not query.startswith('scsearch:'): - query = 'ytsearch:' + query - - results = await player.node.get_tracks(query) - - embed = discord.Embed(description='{nothing_found}', color=0x36393F) - if not results or not results['tracks']: - embed = ctx.localizer.format_embed(embed) - return await ctx.send(embed=embed) - - tracks = results['tracks'] - result_count = min(len(tracks), 15) - - # Setup for the selector - identifiers = [] - functions = [self.enqueue]*result_count - arguments = [(ctx, tracks[i], discord.Embed(color=ctx.me.color)) for i in range(result_count)] - - for index, track in enumerate(tracks, start=1): - track_title = track['info']['title'] - track_uri = track['info']['uri'] - duration = timeformatter.format_ms(int(track['info']['length'])) - identifiers.append(f'`{index}.` [{track_title}]({track_uri}) `{duration}`') - if index == result_count: - break - - search_selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, - color=ctx.me.color, title=ctx.localizer.format_str('{results}')) - # Let the user scroll through results - message, current_page, result = await search_selector.start_scrolling() - - result = ctx.localizer.format_embed(result) - await message.edit(embed=result) - - if not player.is_playing: - await player.play() - - @commands.command(name='disconnect') - @checks.dj_or(alone=True) - @require_voice_connection() - async def _disconnect(self, ctx): - """ Disconnects the player from the voice channel and clears its queue. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - player.queue.clear() - await player.stop() - await self.connect_to(ctx.guild.id, None) - embed = discord.Embed(description='{disconnect.disconnected}', color=ctx.me.color) - embed = ctx.localizer.format_embed(embed) - await ctx.send(embed=embed) - - @commands.command(name='reconnect') - @checks.dj_or(alone=True) - @require_voice_connection() - async def _reconnect(self, ctx): - """ Tries to disconnect then reconnect the player in case the bot gets stuck on a song """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - current_channel = player.channel_id - - await player.stop() - await self.connect_to(ctx.guild.id, None) - await asyncio.sleep(1) # Pretend stuff is happening/give everything some time to reset. - await self.connect_to(ctx.guild.id, current_channel) - await player.play() - - @commands.command(name='volume') - @checks.dj_or(alone=True, track_requester=True) - @require_playing(require_user_listening=True) - async def _volume(self, ctx, volume: int = None): - """ Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - if not volume: - return await ctx.send(f'🔈 | {player.volume}%') - - is_dj = checks.is_dj(ctx) - is_admin = getattr(ctx.author.guild_permissions, 'administrator', None) is True - if player.current and (int(player.current.requester) == ctx.author.id and not is_dj and not is_admin): - if not 50 <= volume <= 125: - return await ctx.send(ctx.localizer.format_str("{volume.out_of_range}")) - await player.set_volume(volume) - - embed = discord.Embed(description="{volume.set_to}", color=ctx.me.color) - embed = ctx.localizer.format_embed(embed, _volume=player.volume) - await ctx.send(embed=embed) - - @commands.command(name='normalize') - @checks.dj_or(alone=True) - @require_voice_connection() - async def _normalize(self, ctx): - """ Reset the equalizer and volume """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - await player.set_volume(100) - await player.bassboost(False) - - embed = discord.Embed(description='{volume.reset}', color=ctx.me.color) - embed = ctx.localizer.format_embed(embed) - await ctx.send(embed=embed) - - @commands.command(name='boost') - @checks.dj_or(alone=True) - async def _boost(self, ctx, boost: bool = None): - """ Set the equalizer to bass boost the music """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - if boost: - await player.bassboost(boost) - - embed = discord.Embed(color=ctx.me.color) - embed.description = '{boost.on}' if player.boosted else '{boost.off}' - - embed = ctx.localizer.format_embed(embed) - await ctx.send(embed=embed) - - @commands.command(name='history') - async def _history(self, ctx): - """ Show the last 10 songs played """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - history = player.get_history() - if not history: - embed = discord.Embed(description='{history.empty}', color=ctx.me.color) - embed = ctx.localizer.format_embed(embed) - return await ctx.send(embed=embed) - track = history[0] - description = ctx.localizer.format_str("{history.current}", _title=track.title, _uri=track.uri, - _id=track.requester) + '\n\n' - description += ctx.localizer.format_str("{history.previous}", _len=len(history)-1) + '\n' - thumbnail_url = track.extra["thumbnail_url"] - for index, track in enumerate(history[1:], start=1): - description += ctx.localizer.format_str("{history.track}", _index=-index, _title=track.title, - _uri=track.uri, _id=track.requester) + '\n' - - embed = discord.Embed(title=ctx.localizer.format_str('{history.title}'), color=ctx.me.color, - description=description) - - if thumbnail_url: - embed.set_thumbnail(url=thumbnail_url) - await ctx.send(embed=embed) - - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(name='lyrics') - async def _lyrics(self, ctx, *query: str): - - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - genius_access_token = self.bot.APIkeys.get('genius', None) - - if genius_access_token is None: - return await ctx.send('Missing API key') - - excluded_words = ['music', 'video', 'version', 'original', 'lyrics', 'lyric', - 'official', 'live', 'instrumental', 'audio', 'hd'] - - query = ' '.join(query) - - if not query and player.is_playing: - query = player.current.title - query = re.sub(r'[-()_\[\]]', '', query) - filtered_words = [word for word in query.split() if word.lower() not in excluded_words] - query = ' '.join(filtered_words) - - async def get_site_content(url): - async with self.bot.session.get(url) as resp: - response = await resp.read() - return response.decode('utf-8') - - embed = discord.Embed(description=':mag_right:') - status_msg = await ctx.send(embed=embed) - - try: - url = 'https://api.genius.com/search?' + urllib.parse.urlencode( - {'access_token': genius_access_token, 'q': query}) - result = await get_site_content(url) - response = json.loads(result) - song_id = response['response']['hits'][0]['result']['id'] - except Exception: - embed = discord.Embed(description=':x:', color=0xFF0000) - return await status_msg.edit(embed=embed) - - result = await get_site_content(f'https://api.genius.com/songs/{song_id}?access_token={genius_access_token}') - song = json.loads(result)['response']['song'] - - response = await get_site_content(song['url']) - scraped_data = BeautifulSoup(response, 'html.parser') - lyrics = scraped_data.find(class_='lyrics').get_text() - - paginator = TextPaginator(max_size=2000, max_lines=50, color=0xFFFF64) - for line in lyrics.split('\n'): - paginator.add_line(line) - - await status_msg.delete() - - paginator.pages[0].url = song['url'] - paginator.pages[0].title = song['full_title'] - paginator.pages[0].set_thumbnail(url=song['header_image_thumbnail_url']) - paginator.pages[0].set_author(name='Genius', icon_url='https://i.imgur.com/NmCTsoF.png') - - if len(paginator.pages) < 4: - for page in paginator.pages: - await ctx.send(embed=page) - else: - paginator.add_page_indicator(ctx.localizer) - await Scroller(ctx, paginator).start_scrolling() - - @commands.command(name='scrub') - @checks.dj_or(alone=True) - @require_voice_connection() - @require_playing(require_user_listening=True) - async def _scrub(self, ctx): - """ Lists the first 10 search results from a given query. """ - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - controls = '{scrub.controls}' - - scrubber = [ - ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', player.seek, -1000), - ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}', player.seek, -15), - ('\N{DOUBLE VERTICAL BAR}', player.set_pause, True), - ('\N{BLACK RIGHT-POINTING TRIANGLE}', player.set_pause, False), - ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}', player.seek, 15), - ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', player.seek, 1000), - ] - - selection = None - arg = None - - def check(reaction, user): - if user is None or user.id != ctx.author.id: - return False - - if reaction.message.id != msg.id: - return False - - for (emoji, func, _arg) in scrubber: - if emoji == reaction.emoji: - nonlocal selection - nonlocal arg - selection = func - arg = _arg - return True - return False - - embed = discord.Embed(description='{scrub.add}', color=ctx.me.color) - embed = ctx.localizer.format_embed(embed) - msg = await ctx.send(embed=embed) - - for (emoji, _, _) in scrubber: - await msg.add_reaction(emoji) - - embed.description = controls - embed = ctx.localizer.format_embed(embed) - await msg.edit(embed=embed) - scrubbing = True - while scrubbing: - try: - reaction, user = await self.bot.wait_for('reaction_add', timeout=15.0, check=check) - except asyncio.TimeoutError: - # PEP8EDIT scrolling = False - try: - await msg.delete() - await ctx.message.delete() - except discord.Forbidden: - pass - except discord.HTTPException: - pass - finally: - break - - try: - await msg.remove_reaction(reaction, user) - except discord.Forbidden: - pass - if selection is not None: - if not isinstance(arg, bool): - arg = player.position + arg * 1000 - await selection(arg) - - async def enqueue(self, ctx, track, embed): - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - - maxlength = self.max_track_length(ctx.guild, player) - if maxlength and track['info']['length'] > maxlength: - embed.description = ctx.localizer.format_str("{enqueue.toolong}", - _length=timeformatter.format_ms(track['info']['length']), - _max=timeformatter.format_ms(maxlength)) - return - - if isinstance(track, dict): - thumbnail_url = await thumbnailer.ThumbNailer.identify( - self, track['info']['identifier'], track['info']['uri']) - track = lavalink.models.AudioTrack(track, ctx.author.id, thumbnail_url=thumbnail_url) - - track, pos_global, pos_local = player.add(requester=ctx.author.id, track=track) - - if player.current is not None: - queue_duration = 0 - for i, track in enumerate(player.queue): - if i == pos_global: - break - queue_duration += int(track.duration) - - until_play = queue_duration + player.current.duration - player.position - until_play = timeformatter.format_ms(until_play) - embed.add_field(name="{enqueue.position}", value=f"`{pos_local + 1}({pos_global + 1})`", inline=True) - embed.add_field(name="{enqueue.playing_in}", value=f"`{until_play} ({{enqueue.estimated}})`", inline=True) - - embed.title = '{enqueue.enqueued}' - thumbnail_url = track.extra["thumbnail_url"] - - if thumbnail_url: - embed.set_thumbnail(url=thumbnail_url) - - duration = timeformatter.format_ms(int(track.duration)) - embed.description = f'[{track.title}]({track.uri})\n**{duration}**' - - return embed - - def max_track_length(self, guild, player): - is_dynamic = self.bot.settings.get(guild, 'duration.is_dynamic', 'default_duration_type') - maxlength = self.bot.settings.get(guild, 'duration.max', None) - if maxlength is None: - return None - if len(player.listeners): # Avoid division by 0. - listeners = len(player.listeners) - else: - listeners = 1 - if maxlength > 10 and is_dynamic: - return max(maxlength*60*1000/listeners, 60*10*1000) - else: - return maxlength*60*1000 - - -def setup(bot): - bot.add_cog(Music(bot)) diff --git a/cogs/music/__init__.py b/cogs/music/__init__.py new file mode 100644 index 0000000..d12546c --- /dev/null +++ b/cogs/music/__init__.py @@ -0,0 +1,104 @@ +# Discord Packages +import lavalink +from discord.ext import commands + +from ..utils import thumbnailer, timeformatter +from .music_errors import WrongTextChannelError + + +class Music(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.logger = self.bot.main_logger.bot_logger.getChild("Music") + + self.bot = bot + self.leave_timer.start() + self.logger = self.bot.main_logger.bot_logger.getChild("Errors") + bot.lavalink.add_event_hook(self.track_hook) + + async def cog_check(self, ctx): + if not ctx.guild: + raise commands.NoPrivateMessage + if textchannels := self.bot.settings.get(ctx.guild, 'channels.text', []): + if ctx.channel.id not in textchannels: + raise WrongTextChannelError('You need to be in the right text channel', channels=textchannels) + return True + + async def cog_before_invoke(self, ctx): + """ Ensures a valic player exists whenever a command is run """ + region = ctx.guild.region if isinstance(ctx.guild.region, str) else ctx.guild.region.value + # Creates a new only if one doesn't exist, ensures a valid player for all checks. + self.bot.lavalink.player_manager.create(ctx.guild.id, endpoint=region) + + async def connect_to(self, guild_id: int, channel_id: str): + """ Connects to the given voicechannel ID. A channel_id of `None` means disconnect. """ + ws = self.bot._connection._get_websocket(guild_id) + await ws.voice_state(str(guild_id), channel_id) + + async def enqueue(self, ctx, track, embed): + # TODO: rework + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + maxlength = self.max_track_length(ctx.guild, player) + if maxlength and track['info']['length'] > maxlength: + embed.description = ctx.localizer.format_str("{enqueue.toolong}", + _length=timeformatter.format_ms(track['info']['length']), + _max=timeformatter.format_ms(maxlength)) + return + + # TODO: make use of track vs track dict more consistent + if isinstance(track, dict): + thumbnail_url = await thumbnailer.ThumbNailer.identify( + self, track['info']['identifier'], track['info']['uri']) + track = lavalink.models.AudioTrack(track, ctx.author.id, thumbnail_url=thumbnail_url) + + track, pos_global, pos_local = player.add(requester=ctx.author.id, track=track) + + if player.current is not None: + queue_duration = 0 + for i, track in enumerate(player.queue): + if i == pos_global: + break + queue_duration += int(track.duration) + + until_play = queue_duration + player.current.duration - player.position + until_play = timeformatter.format_ms(until_play) + embed.add_field(name="{enqueue.position}", value=f"`{pos_local + 1}({pos_global + 1})`", inline=True) + embed.add_field(name="{enqueue.playing_in}", value=f"`{until_play} ({{enqueue.estimated}})`", inline=True) + + embed.title = '{enqueue.enqueued}' + thumbnail_url = track.extra["thumbnail_url"] + + if thumbnail_url: + embed.set_thumbnail(url=thumbnail_url) + + duration = timeformatter.format_ms(int(track.duration)) + embed.description = f'[{track.title}]({track.uri})\n**{duration}**' + + return embed + + def max_track_length(self, guild, player): + # TODO: Move somewhere else + is_dynamic = self.bot.settings.get(guild, 'duration.is_dynamic', 'default_duration_type') + maxlength = self.bot.settings.get(guild, 'duration.max', None) + if maxlength is None: + return None + if len(player.listeners): # Avoid division by 0. + listeners = len(player.listeners) + else: + listeners = 1 + if maxlength > 10 and is_dynamic: + return max(maxlength*60*1000/listeners, 60*10*1000) + else: + return maxlength*60*1000 + + # commands + from .basic_commands import ( + _boost, _disconnect, _djremove, _history, _lyrics, _move, _myqueue, _normalize, _now, _pause, _play, _queue, + _reconnect, _remove, _scrub, _search, _seek, _shuffle, _skip, _skip_to, _stop, _user_queue_remove, _volume) + # events + from .events import check_leave_voice, cog_unload, leave_check, leave_timer, on_voice_state_update, track_hook + + +def setup(bot): + bot.add_cog(Music(bot)) diff --git a/cogs/music/advanced_commands.py b/cogs/music/advanced_commands.py new file mode 100644 index 0000000..e69de29 diff --git a/cogs/music/basic_commands.py b/cogs/music/basic_commands.py new file mode 100644 index 0000000..61eee23 --- /dev/null +++ b/cogs/music/basic_commands.py @@ -0,0 +1,653 @@ +# Discord Packages +import discord +import lavalink +from discord.ext import commands + +import asyncio +import json +import math +import re +import urllib + +from bs4 import BeautifulSoup + +from ..utils import checks, thumbnailer, timeformatter +from ..utils.decorators import require_playing, require_queue, require_voice_connection +from ..utils.paginator import QueuePaginator, Scroller, TextPaginator +from ..utils.selector import Selector + +time_rx = re.compile('[0-9]+') +url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') + + +@commands.command(name='play') +@require_voice_connection(should_connect=True) +async def _play(self, ctx, *, query: str): + """ Searches and plays a song from a given query. """ + # TODO: rework + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + query = query.strip('<>') + + if not url_rx.match(query): + query = f'ytsearch:{query}' + + results = await player.node.get_tracks(query) + + if not results or not results['tracks']: + return await ctx.send(ctx.localizer.format_str("{nothing_found}")) + + embed = discord.Embed(color=ctx.me.color) + + if results['loadType'] == 'PLAYLIST_LOADED': + tracks = results['tracks'] + + if maxlength := self.max_track_length(ctx.guild, player): + numtracks = 0 + for track in tracks: + if track['info']['length'] <= maxlength: + thumbnail_url = await thumbnailer.ThumbNailer.identify( + self, track['info']['identifier'], track['info']['uri']) + track = lavalink.models.AudioTrack(track, ctx.author.id, thumbnail_url=thumbnail_url) + player.add(requester=ctx.author.id, track=track) + numtracks += 1 + else: + numtracks = len(tracks) + for track in tracks: + thumbnail_url = await thumbnailer.ThumbNailer.identify( + self, track['info']['identifier'], track['info']['uri']) + track = lavalink.models.AudioTrack(track, ctx.author.id, thumbnail_url=thumbnail_url) + player.add(requester=ctx.author.id, track=track) + + embed.title = '{playlist_enqued}' + embed.description = f'{results["playlistInfo"]["name"]} - {numtracks} {{tracks}}' + embed = ctx.localizer.format_embed(embed) + await ctx.send(embed=embed) + else: + track = results['tracks'][0] + await self.enqueue(ctx, track, embed) + embed = ctx.localizer.format_embed(embed) + await ctx.send(embed=embed) + + if not player.is_playing: + await player.play() + + +@commands.command(name='seek') +@checks.dj_or(alone=True, track_requester=True) +@require_voice_connection() +@require_playing(require_user_listening=True) +async def _seek(self, ctx, *, time: str): + """ Seeks to a given position in a track. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + if seconds := time_rx.search(time): + # Convert to milliseconds, include sign + milliseconds = int(seconds.group())*1000 * (-1 if time.startswith('-1') else 1) + + track_time = player.position + milliseconds + await player.seek(track_time) + msg = ctx.localizer.format_str("{seek.track_moved}", _position=timeformatter.format_ms(track_time)) + await ctx.send(msg) + else: + await ctx.send(ctx.localizer.format_str("{seek.missing_amount}")) + + +@commands.command(name='skip') +@require_voice_connection() +@require_playing() +async def _skip(self, ctx): + # TODO: rework + """ Skips the current track. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + player.add_skipper(ctx.author) + total = len(player.listeners) + skips = len(player.skip_voters) + threshold = self.bot.settings.get(ctx.guild, 'vote_threshold', 'default_threshold') + + if skips/total >= threshold/100 or player.current.requester == ctx.author.id: + await player.skip() + if player.current: + song = f'**[{player.current.title}]({player.current.uri})**' + embed = discord.Embed(color=ctx.me.color, description=song, title='{now}') + thumbnail_url = player.current.extra["thumbnail_url"] + member = ctx.guild.get_member(player.current.requester) + if thumbnail_url: + embed.set_thumbnail(url=thumbnail_url) + + embed.set_footer(text=f'{{requested_by}} {member.display_name}', icon_url=member.avatar_url) + + embed = ctx.localizer.format_embed(embed) + await ctx.send(ctx.localizer.format_str("{skip.skipped}"), embed=embed) + else: + await ctx.send(ctx.localizer.format_str("{skip.skipped}")) + else: + if skips != 0: + needed = math.ceil(total*threshold/100) + msg = ctx.localizer.format_str("{skip.require_vote}", _skips=skips, _total=needed) + + await ctx.send(msg) + + +@commands.command(name='skipto') +@checks.dj_or(alone=True) +@require_playing(require_user_listening=True) +async def _skip_to(self, ctx, pos: int = 1): + """ Plays the queue from a specific point. Disregards tracks before the pos. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + # TODO: Do all queue out of range messages the same way + if pos < 1: + return await ctx.send(ctx.localizer.format_str("{skip_to.invalid_pos}")) + if len(player.queue) < pos: + return await ctx.send(ctx.localizer.format_str("{skip_to.exceeds_queue}")) + + await player.skip(pos - 1) + + msg = ctx.localizer.format_str("{skip_to.skipped_to}", _title=player.current.title, _pos=pos) + await ctx.send(msg) + + +@commands.command(name='stop') +@checks.dj_or(alone=True) +@require_voice_connection() +@require_playing(require_user_listening=True) +async def _stop(self, ctx): + """ Stops the player and clears its queue. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + player.queue.clear() + await player.stop() + + embed = discord.Embed(color=ctx.me.color, title='{stop}') + await ctx.send(embed=ctx.localizer.format_embed(embed)) + + +@commands.command(name='now') +@require_playing() +async def _now(self, ctx): + # TODO: rework + """ Shows some stats about the currently playing song. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + position = timeformatter.format_ms(player.position) + if player.current.stream: + duration = '{live}' + else: + duration = timeformatter.format_ms(player.current.duration) + song = f'**[{player.current.title}]({player.current.uri})**\n({position}/{duration})' + + member = ctx.guild.get_member(player.current.requester) + embed = discord.Embed(color=ctx.me.color, description=song, title='{now}') + thumbnail_url = player.current.extra["thumbnail_url"] + if thumbnail_url: + embed.set_thumbnail(url=thumbnail_url) + + embed.set_footer(text=f'{{requested_by}} {member.display_name}', icon_url=member.avatar_url) + + embed = ctx.localizer.format_embed(embed) + await ctx.send(embed=embed) + + +@commands.command(name='queue') +@require_queue(require_member_queue=True) +async def _queue(self, ctx, *, member: discord.Member = None): + """ Shows the player's queue. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + if member is None: + queue = player.global_queue() + pagified_queue = QueuePaginator(ctx.localizer, queue, color=ctx.me.color) + + else: + member_queue = player.user_queue(member.id, dual=True) + + pagified_queue = QueuePaginator(ctx.localizer, member_queue, color=ctx.me.color, + user_name=member.display_name) + + scroller = Scroller(ctx, pagified_queue) + await scroller.start_scrolling() + + +@commands.command(name='myqueue') +@require_queue(require_author_queue=True) +async def _myqueue(self, ctx): + """ Shows your queue. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + user_queue = player.user_queue(ctx.author.id, dual=True) + pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, + user_name=ctx.author.display_name) + scroller = Scroller(ctx, pagified_queue) + await scroller.start_scrolling() + + +@commands.command(name='pause') +@checks.dj_or(alone=True) +@require_voice_connection() +@require_playing() +async def _pause(self, ctx): + """ Pauses/Resumes the current track. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + if player.paused: + await player.set_pause(False) + await ctx.send(ctx.localizer.format_str("{resume.resumed}")) + else: + await player.set_pause(True) + await ctx.send(ctx.localizer.format_str("{resume.paused}")) + + +@commands.command(name='shuffle') +@require_voice_connection() +@require_queue(require_author_queue=True) +async def _shuffle(self, ctx): + """ Shuffles your queue. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + player.shuffle_user_queue(ctx.author.id) + await ctx.send(ctx.localizer.format_str("{shuffle}")) + + +@commands.command(name='move') +@require_voice_connection() +@require_queue(require_author_queue=True) +async def _move(self, ctx): + """ Moves a song in your queue. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + user_queue = player.user_queue(ctx.author.id, dual=True) + + # Setup for selector + identifiers = [] + for index, temp in enumerate(user_queue): + track, globpos = temp + identifiers.append(ctx.localizer.format_str("{queue.usertrack}", _index=index+1, + _globalindex=globpos+1, _title=track.title, + _uri=track.uri)) + + def ret_first_arg(*args): + return args[0] + + functions = [ret_first_arg]*len(user_queue) + arguments = [(i,) for i in range(len(user_queue))] + + # Set the different titles for the different selections + round_titles = ["{moved.choose_song}", "{moved.choose_pos}"] + round_titles = [ctx.localizer.format_str(i) for i in round_titles] + + selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, round_titles=round_titles, + color=ctx.me.color, title=ctx.localizer.format_str("{moved.choose}")) + + # Get the initial and final position + message, page, selections = await selector.start_scrolling() + pos_initial, pos_final = selections[0], selections[1] + + # Move the track + moved = player.move_user_track(ctx.author.id, pos_initial, pos_final) + + # Create a nice embed explaining what happened + song = f'**[{moved.title}]({moved.uri})**' + embed = discord.Embed(color=ctx.me.color, description=song, title='{moved.moved}') + thumbnail_url = moved.extra["thumbnail_url"] + member = ctx.guild.get_member(moved.requester) + if thumbnail_url: + + embed.set_thumbnail(url=thumbnail_url) + + embed.add_field(name="{moved.local}", value=f"`{pos_initial + 1} → {pos_final + 1}`", inline=True) + embed.add_field(name="{moved.global}", value=f"`{player.queue._loc_to_glob(ctx.author.id, pos_initial) + 1}\ + → {player.queue._loc_to_glob(ctx.author.id, pos_final) + 1}`", inline=True) + embed.set_footer(text=f'{{requested_by}} {member.display_name}', icon_url=member.avatar_url) + embed = ctx.localizer.format_embed(embed) + + await message.edit(embed=embed) + + +@commands.command(name='remove') +@require_voice_connection() +@require_queue(require_author_queue=True) +async def _remove(self, ctx): + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + user_queue = player.user_queue(ctx.author.id, dual=True) + functions = [player.remove_user_track]*len(user_queue) + arguments = [(ctx.author.id, i) for i in range(len(user_queue))] + identifiers = [] + + for index, temp in enumerate(user_queue): + track, globpos = temp + identifiers.append(ctx.localizer.format_str("{queue.usertrack}", _index=index+1, + _globalindex=globpos+1, _title=track.title, + _uri=track.uri)) + + selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, color=0xFF0000, + title='Remove song plz') + message, _, removed = await selector.start_scrolling() + await message.edit(content=ctx.localizer.format_str("{remove}", _title=removed.title), embed=None) + + +@commands.command(name="DJremove") +@checks.dj_or() +@require_voice_connection() +@require_queue() +async def _djremove(self, ctx, pos: int, member: discord.Member = None): + """ Remove a song from either the global queue or a users queue""" + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + # TODO: add way to allow "member" as regular arg in require_queue() instead of only kwargs + queue = player.user_queue(member.id) if member else player.queue + + # TODO: Do all queue out of range messages the same way + if pos <= len(queue) and pos >= 1: + if member is None: + removed = player.remove_global_track(pos - 1) + else: + removed = player.remove_user_track(member.id, pos - 1) + + requester = self.bot.get_user(removed.requester) + await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, _user=requester.name)) + else: + await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(queue))) + + +@commands.command(name='removeuser') +@checks.dj_or(alone=True) +@require_voice_connection() +@require_queue(require_member_queue=True) +async def _user_queue_remove(self, ctx, *, member: discord.Member): + """ Remove a song from either the global queue or a users queue""" + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + player.remove_user_queue(member.id) + + embed = discord.Embed(description="{dj_remove_user}", color=ctx.me.color) + embed = ctx.localizer.format_embed(embed, _id=member.id) + + await ctx.send(embed=embed) + + +@commands.command(name='search') +@require_voice_connection(should_connect=True) +async def _search(self, ctx, *, query): + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + if not query.startswith('ytsearch:') and not query.startswith('scsearch:'): + query = 'ytsearch:' + query + + results = await player.node.get_tracks(query) + + embed = discord.Embed(description='{nothing_found}', color=0x36393F) + if not results or not results['tracks']: + embed = ctx.localizer.format_embed(embed) + return await ctx.send(embed=embed) + + tracks = results['tracks'] + result_count = min(len(tracks), 15) + + # Setup for the selector + identifiers = [] + functions = [self.enqueue]*result_count + arguments = [(ctx, tracks[i], discord.Embed(color=ctx.me.color)) for i in range(result_count)] + + for index, track in enumerate(tracks, start=1): + track_title = track['info']['title'] + track_uri = track['info']['uri'] + duration = timeformatter.format_ms(int(track['info']['length'])) + identifiers.append(f'`{index}.` [{track_title}]({track_uri}) `{duration}`') + if index == result_count: + break + + search_selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, + color=ctx.me.color, title=ctx.localizer.format_str('{results}')) + # Let the user scroll through results + message, current_page, result = await search_selector.start_scrolling() + + result = ctx.localizer.format_embed(result) + await message.edit(embed=result) + + if not player.is_playing: + await player.play() + + +@commands.command(name='disconnect') +@checks.dj_or(alone=True) +@require_voice_connection() +async def _disconnect(self, ctx): + """ Disconnects the player from the voice channel and clears its queue. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + player.queue.clear() + await player.stop() + await self.connect_to(ctx.guild.id, None) + embed = discord.Embed(description='{disconnect.disconnected}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed) + await ctx.send(embed=embed) + + +@commands.command(name='reconnect') +@checks.dj_or(alone=True) +@require_voice_connection() +async def _reconnect(self, ctx): + """ Tries to disconnect then reconnect the player in case the bot gets stuck on a song """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + current_channel = player.channel_id + + await player.stop() + await self.connect_to(ctx.guild.id, None) + await asyncio.sleep(1) # Pretend stuff is happening/give everything some time to reset. + await self.connect_to(ctx.guild.id, current_channel) + await player.play() + + +@commands.command(name='volume') +@checks.dj_or(alone=True, track_requester=True) +@require_playing(require_user_listening=True) +async def _volume(self, ctx, volume: int = None): + """ Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + if not volume: + return await ctx.send(f'🔈 | {player.volume}%') + + is_dj = checks.is_dj(ctx) + is_admin = getattr(ctx.author.guild_permissions, 'administrator', None) is True + if player.current and (int(player.current.requester) == ctx.author.id and not is_dj and not is_admin): + if not 50 <= volume <= 125: + return await ctx.send(ctx.localizer.format_str("{volume.out_of_range}")) + await player.set_volume(volume) + + embed = discord.Embed(description="{volume.set_to}", color=ctx.me.color) + embed = ctx.localizer.format_embed(embed, _volume=player.volume) + await ctx.send(embed=embed) + + +@commands.command(name='normalize') +@checks.dj_or(alone=True) +@require_voice_connection() +async def _normalize(self, ctx): + """ Reset the equalizer and volume """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + await player.set_volume(100) + await player.bassboost(False) + + embed = discord.Embed(description='{volume.reset}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed) + await ctx.send(embed=embed) + + +@commands.command(name='boost') +@checks.dj_or(alone=True) +async def _boost(self, ctx, boost: bool = None): + """ Set the equalizer to bass boost the music """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + if boost: + await player.bassboost(boost) + + embed = discord.Embed(color=ctx.me.color) + embed.description = '{boost.on}' if player.boosted else '{boost.off}' + + embed = ctx.localizer.format_embed(embed) + await ctx.send(embed=embed) + + +@commands.command(name='history') +async def _history(self, ctx): + """ Show the last 10 songs played """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + history = player.get_history() + if not history: + embed = discord.Embed(description='{history.empty}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed) + return await ctx.send(embed=embed) + track = history[0] + description = ctx.localizer.format_str("{history.current}", _title=track.title, _uri=track.uri, + _id=track.requester) + '\n\n' + description += ctx.localizer.format_str("{history.previous}", _len=len(history)-1) + '\n' + thumbnail_url = track.extra["thumbnail_url"] + for index, track in enumerate(history[1:], start=1): + description += ctx.localizer.format_str("{history.track}", _index=-index, _title=track.title, + _uri=track.uri, _id=track.requester) + '\n' + + embed = discord.Embed(title=ctx.localizer.format_str('{history.title}'), color=ctx.me.color, + description=description) + + if thumbnail_url: + embed.set_thumbnail(url=thumbnail_url) + await ctx.send(embed=embed) + + +@commands.cooldown(1, 5, commands.BucketType.guild) +@commands.command(name='lyrics') +async def _lyrics(self, ctx, *query: str): + + # TODO: + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + genius_access_token = self.bot.APIkeys.get('genius', None) + + if genius_access_token is None: + return await ctx.send('Missing API key') + + excluded_words = ['music', 'video', 'version', 'original', 'lyrics', 'lyric', + 'official', 'live', 'instrumental', 'audio', 'hd'] + + query = ' '.join(query) + + if not query and player.is_playing: + query = player.current.title + query = re.sub(r'[-()_\[\]]', '', query) + filtered_words = [word for word in query.split() if word.lower() not in excluded_words] + query = ' '.join(filtered_words) + + async def get_site_content(url): + async with self.bot.session.get(url) as resp: + response = await resp.read() + return response.decode('utf-8') + + embed = discord.Embed(description=':mag_right:') + status_msg = await ctx.send(embed=embed) + + try: + url = 'https://api.genius.com/search?' + urllib.parse.urlencode( + {'access_token': genius_access_token, 'q': query}) + result = await get_site_content(url) + response = json.loads(result) + song_id = response['response']['hits'][0]['result']['id'] + except Exception: + embed = discord.Embed(description=':x:', color=0xFF0000) + return await status_msg.edit(embed=embed) + + result = await get_site_content(f'https://api.genius.com/songs/{song_id}?access_token={genius_access_token}') + song = json.loads(result)['response']['song'] + + response = await get_site_content(song['url']) + scraped_data = BeautifulSoup(response, 'html.parser') + lyrics = scraped_data.find(class_='lyrics').get_text() + + paginator = TextPaginator(max_size=2000, max_lines=50, color=0xFFFF64) + for line in lyrics.split('\n'): + paginator.add_line(line) + + await status_msg.delete() + + paginator.pages[0].url = song['url'] + paginator.pages[0].title = song['full_title'] + paginator.pages[0].set_thumbnail(url=song['header_image_thumbnail_url']) + paginator.pages[0].set_author(name='Genius', icon_url='https://i.imgur.com/NmCTsoF.png') + + if len(paginator.pages) < 4: + for page in paginator.pages: + await ctx.send(embed=page) + else: + paginator.add_page_indicator(ctx.localizer) + await Scroller(ctx, paginator).start_scrolling() + + +@commands.command(name='scrub') +@checks.dj_or(alone=True) +@require_voice_connection() +@require_playing(require_user_listening=True) +async def _scrub(self, ctx): + """ Lists the first 10 search results from a given query. """ + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + controls = '{scrub.controls}' + + scrubber = [ + ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', player.seek, -1000), + ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}', player.seek, -15), + ('\N{DOUBLE VERTICAL BAR}', player.set_pause, True), + ('\N{BLACK RIGHT-POINTING TRIANGLE}', player.set_pause, False), + ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}', player.seek, 15), + ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', player.seek, 1000), + ] + + selection = None + arg = None + + def check(reaction, user): + if user is None or user.id != ctx.author.id: + return False + + if reaction.message.id != msg.id: + return False + + for (emoji, func, _arg) in scrubber: + if emoji == reaction.emoji: + nonlocal selection + nonlocal arg + selection = func + arg = _arg + return True + return False + + embed = discord.Embed(description='{scrub.add}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed) + msg = await ctx.send(embed=embed) + + for (emoji, _, _) in scrubber: + await msg.add_reaction(emoji) + + embed.description = controls + embed = ctx.localizer.format_embed(embed) + await msg.edit(embed=embed) + scrubbing = True + while scrubbing: + try: + reaction, user = await self.bot.wait_for('reaction_add', timeout=15.0, check=check) + except asyncio.TimeoutError: + # PEP8EDIT scrolling = False + try: + await msg.delete() + await ctx.message.delete() + except discord.Forbidden: + pass + except discord.HTTPException: + pass + finally: + break + + try: + await msg.remove_reaction(reaction, user) + except discord.Forbidden: + pass + if selection is not None: + if not isinstance(arg, bool): + arg = player.position + arg * 1000 + await selection(arg) diff --git a/cogs/music/events.py b/cogs/music/events.py new file mode 100644 index 0000000..5ddd5bf --- /dev/null +++ b/cogs/music/events.py @@ -0,0 +1,71 @@ + +# Discord Packages +import lavalink +import lavalink.events +from discord.ext import commands, tasks + + +def cog_unload(self): + self.bot.lavalink._event_hooks.clear() + + +async def track_hook(self, event): + if isinstance(event, lavalink.events.TrackEndEvent): + channel = self.bot.get_channel(event.player.fetch('channel')) + player = self.bot.lavalink.player_manager.get(channel.guild.id) + player.skip_voters.clear() + + if isinstance(event, lavalink.events.TrackStartEvent): + pass + if isinstance(event, lavalink.events.QueueEndEvent): + channel = self.bot.get_channel(event.player.fetch('channel')) + await self.check_leave_voice(channel.guild) + if isinstance(event, lavalink.events.PlayerUpdateEvent): + pass + if isinstance(event, lavalink.events.NodeDisconnectedEvent): + pass + if isinstance(event, lavalink.events.NodeConnectedEvent): + pass + if isinstance(event, lavalink.events.NodeChangedEvent): + pass + + +@commands.Cog.listener() +async def on_voice_state_update(self, member, before, after): + """ Updates listeners when the bot or a user changes voice state """ + if member.id == self.bot.user.id and after.channel is not None: + voice_channel = after.channel + player = self.bot.lavalink.player_manager.get(member.guild.id) + player.clear_listeners() + for member in voice_channel.members: + if not member.bot: + player.update_listeners(member, member.voice) + + if not member.bot: + player = self.bot.lavalink.player_manager.get(member.guild.id) + if player is not None: + player.update_listeners(member, after) + await self.check_leave_voice(member.guild) + + +async def check_leave_voice(self, guild): + """ Checks if the bot should leave the voice channel """ + # TODO, disconnect timer? + player = self.bot.lavalink.player_manager.get(guild.id) + if len(player.listeners) == 0 and player.is_connected: + if player.queue.empty and player.current is None: + await player.stop() + await self.connect_to(guild.id, None) + + +async def leave_check(self): + for player_id in self.bot.lavalink.player_manager.players: + await self.check_leave_voice(self.bot.get_guild(player_id)) + + +@tasks.loop(seconds=10.0) +async def leave_timer(self): + try: + await self.leave_check() + except Exception as err: + self.logger.debug("Error in leave_timer loop.\nTraceback: %s" % (err)) diff --git a/cogs/utils/music_errors.py b/cogs/music/music_errors.py similarity index 100% rename from cogs/utils/music_errors.py rename to cogs/music/music_errors.py diff --git a/cogs/musicevents.py b/cogs/musicevents.py deleted file mode 100644 index 74ed119..0000000 --- a/cogs/musicevents.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -A cog to separate events from regular music commands -""" - -# Discord Packages -import lavalink -import lavalink.events -from discord.ext import commands, tasks - - -class MusicEvents(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.leave_timer.start() - self.logger = self.bot.main_logger.bot_logger.getChild("Errors") - bot.lavalink.add_event_hook(self.track_hook) - - def cog_unload(self): - self.bot.lavalink._event_hooks.clear() - - async def track_hook(self, event): - if isinstance(event, lavalink.events.TrackEndEvent): - channel = self.bot.get_channel(event.player.fetch('channel')) - player = self.bot.lavalink.player_manager.get(channel.guild.id) - player.skip_voters.clear() - - if isinstance(event, lavalink.events.TrackStartEvent): - pass - if isinstance(event, lavalink.events.QueueEndEvent): - channel = self.bot.get_channel(event.player.fetch('channel')) - await self.check_leave_voice(channel.guild) - if isinstance(event, lavalink.events.PlayerUpdateEvent): - pass - if isinstance(event, lavalink.events.NodeDisconnectedEvent): - pass - if isinstance(event, lavalink.events.NodeConnectedEvent): - pass - if isinstance(event, lavalink.events.NodeChangedEvent): - pass - - async def connect_to(self, guild_id: int, channel_id: str): - """ Connects to the given voicechannel ID. A channel_id of `None` means disconnect. """ - ws = self.bot._connection._get_websocket(guild_id) - await ws.voice_state(str(guild_id), channel_id) - - @commands.Cog.listener() - async def on_voice_state_update(self, member, before, after): - """ Updates listeners when the bot or a user changes voice state """ - if member.id == self.bot.user.id and after.channel is not None: - voice_channel = after.channel - player = self.bot.lavalink.player_manager.get(member.guild.id) - player.clear_listeners() - for member in voice_channel.members: - if not member.bot: - player.update_listeners(member, member.voice) - - if not member.bot: - player = self.bot.lavalink.player_manager.get(member.guild.id) - if player is not None: - player.update_listeners(member, after) - await self.check_leave_voice(member.guild) - - async def check_leave_voice(self, guild): - """ Checks if the bot should leave the voice channel """ - # TODO, disconnect timer? - player = self.bot.lavalink.player_manager.get(guild.id) - if len(player.listeners) == 0 and player.is_connected: - if player.queue.empty and player.current is None: - await player.stop() - await self.connect_to(guild.id, None) - - async def leave_check(self): - for player_id in self.bot.lavalink.player_manager.players: - await self.check_leave_voice(self.bot.get_guild(player_id)) - - @tasks.loop(seconds=10.0) - async def leave_timer(self): - try: - await self.leave_check() - except Exception as err: - self.logger.debug("Error in leave_timer loop.\nTraceback: %s" % (err)) - - -def setup(bot): - bot.add_cog(MusicEvents(bot)) diff --git a/cogs/nodemanager.py b/cogs/nodemanager.py index ef08c9b..58c4265 100644 --- a/cogs/nodemanager.py +++ b/cogs/nodemanager.py @@ -22,7 +22,6 @@ class NodeManager(commands.Cog): music_extensions = [ 'cogs.music', - 'cogs.musicevents' ] if not hasattr(bot, 'lavalink'): @@ -104,6 +103,7 @@ class NodeManager(commands.Cog): @commands.is_owner() async def _node(self, ctx): if ctx.invoked_subcommand is None: + ctx.localizer.prefix = 'help' # Ensure the bot looks for locales in the context of help, not cogmanager. paginator = commandhelper(ctx, ctx.command, ctx.invoker, include_subcmd=True) scroller = Scroller(ctx, paginator) await scroller.start_scrolling() diff --git a/cogs/settings.py b/cogs/settings.py index 22c5285..5c10453 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -34,7 +34,6 @@ class Settings(commands.Cog): self.bot.aliaser.load_localizations() if locale in self.bot.aliaser.localization_table.keys(): - self.settings.set(ctx.guild, 'locale', locale) locale = self.settings.get(ctx.guild, 'locale', 'default_lang') await ctx.send(f'Locale set to `{locale}`') @@ -45,8 +44,7 @@ class Settings(commands.Cog): @commands.guild_only() @_set.command(name='serverprefix') async def _set_guild_prefix(self, ctx, *prefixes): - prefixes = list(prefixes) - if prefixes: + if prefixes := list(prefixes): self.settings.set(ctx.guild, 'prefixes', prefixes) prefixes = self.settings.get(ctx.guild, 'prefixes') await ctx.send(f'Server prefixes: {self.format_prefixes(prefixes)}') @@ -86,9 +84,7 @@ class Settings(commands.Cog): else: self.bot.settings.set(ctx.guild, 'channels.text', None) - textchannels = self.bot.settings.get(ctx.guild, 'channels.text') - - if textchannels: + if textchannels := self.bot.settings.get(ctx.guild, 'channels.text'): embed = discord.Embed(title='Music command channels set', color=ctx.me.color) channels = [ctx.guild.get_channel(channel) for channel in textchannels] mentioned = [channel.mention for channel in channels if channel is not None] @@ -108,9 +104,7 @@ class Settings(commands.Cog): else: self.bot.settings.set(ctx.guild, 'channels.music', None) - musicchannels = self.bot.settings.get(ctx.guild, 'channels.music') - - if musicchannels: + if musicchannels := self.bot.settings.get(ctx.guild, 'channels.music'): embed = discord.Embed(title='Music channels set', color=ctx.me.color) channels = [ctx.guild.get_channel(channel) for channel in musicchannels] mentioned = [channel.name for channel in channels if channel is not None] @@ -130,9 +124,7 @@ class Settings(commands.Cog): else: self.bot.settings.set(ctx.guild, 'channels.listen_only', None) - listenchannels = self.bot.settings.get(ctx.guild, 'channels.listen_only') - - if listenchannels: + if listenchannels := self.bot.settings.get(ctx.guild, 'channels.listen_only'): embed = discord.Embed(title='Listen only channels set', color=ctx.me.color) channels = [ctx.guild.get_channel(channel) for channel in listenchannels] mentioned = [channel.name for channel in channels if channel is not None] @@ -152,9 +144,7 @@ class Settings(commands.Cog): else: self.bot.settings.set(ctx.guild, 'roles.dj', None) - djroles = self.bot.settings.get(ctx.guild, 'roles.dj') - - if djroles: + if djroles := self.bot.settings.get(ctx.guild, 'roles.dj'): embed = discord.Embed(title='DJ roles set to', color=ctx.me.color) roles = [ctx.guild.get_role(role) for role in djroles] mentioned = [role.name for role in roles if role is not None] @@ -170,9 +160,7 @@ class Settings(commands.Cog): async def set_max_track_duration(self, ctx, duration: int = None): self.bot.settings.set(ctx.guild, 'duration.max', duration) - duration = self.bot.settings.get(ctx.guild, 'duration.max') - - if duration: + if duration := self.bot.settings.get(ctx.guild, 'duration.max'): embed = discord.Embed(title='Max track duration set to', color=ctx.me.color) embed.description = f'{duration} minutes' await ctx.send(embed=embed) @@ -186,10 +174,8 @@ class Settings(commands.Cog): async def set_track_duration_type(self, ctx, dynamic: bool = False): self.bot.settings.set(ctx.guild, 'duration.is_dynamic', dynamic) - is_dynamic = self.bot.settings.get(ctx.guild, 'duration.is_dynamic') - embed = discord.Embed(title='Max duration type set to', color=ctx.me.color) - if is_dynamic: + if self.bot.settings.get(ctx.guild, 'duration.is_dynamic'): embed.description = 'Dynamic' else: embed.description = 'Static' @@ -214,24 +200,20 @@ class Settings(commands.Cog): is_dynamic = self.bot.settings.get(ctx.guild, 'duration.is_dynamic', 'default_is_dynamic') embed.add_field(name='{current.dynamicmax}', value=is_dynamic) - duration = self.bot.settings.get(ctx.guild, 'maxduration') - if duration: + if duration := self.bot.settings.get(ctx.guild, 'maxduration'): embed.add_field(name='{current.maxduration}', value=f'{duration} minutes') - textchannels = self.bot.settings.get(ctx.guild, 'channels.text') - if textchannels: + if textchannels := self.bot.settings.get(ctx.guild, 'channels.text'): channels = [ctx.guild.get_channel(channel) for channel in textchannels] mentioned = [channel.mention for channel in channels if channel is not None] embed.add_field(name='{current.textchannels}', value='\n'.join(mentioned)) - voicechannels = self.bot.settings.get(ctx.guild, 'channels.music') - if voicechannels: + if voicechannels := self.bot.settings.get(ctx.guild, 'channels.music'): channels = [ctx.guild.get_channel(channel) for channel in voicechannels] mentioned = [channel.name for channel in channels if channel is not None] embed.add_field(name='{current.musicchannels}', value='\n'.join(mentioned)) - listenchannels = self.bot.settings.get(ctx.guild, 'channels.listen_only') - if listenchannels: + if listenchannels := self.bot.settings.get(ctx.guild, 'channels.listen_only'): channels = [ctx.guild.get_channel(channel) for channel in listenchannels] mentioned = [channel.name for channel in channels if channel is not None] embed.add_field(name='{current.listenchannels}', value='\n'.join(mentioned)) diff --git a/cogs/utils/decorators.py b/cogs/utils/decorators.py index bdb3e0d..99be11e 100644 --- a/cogs/utils/decorators.py +++ b/cogs/utils/decorators.py @@ -5,8 +5,7 @@ from discord.ext import commands import functools import inspect -# Bot Utilities -from cogs.utils.music_errors import WrongVoiceChannelError +from cogs.music.music_errors import WrongVoiceChannelError def require_voice_connection(should_connect=False): diff --git a/localization/en_en/commands.yaml b/localization/en_en/commands.yaml index 6f603da..5138b1f 100644 --- a/localization/en_en/commands.yaml +++ b/localization/en_en/commands.yaml @@ -88,6 +88,9 @@ lyrics: scrub: aliases: [scrub] description: 'Shows a temporary controller/scrubber.' +reconnect: + aliases: [rc] + description: 'disconnects then reconnects the bot in case of bugged songs' # Misc info: @@ -183,8 +186,8 @@ settings: description: 'Set the vote threshold required to skip a song.' # Cogs -cogs: - aliases: [cogs] +cogmanager: + aliases: [cogmanager] description: 'cog management' sub_commands: load: diff --git a/localization/en_en/nodemanager.yaml b/localization/en_en/nodemanager.yaml deleted file mode 100644 index 7ec4f85..0000000 --- a/localization/en_en/nodemanager.yaml +++ /dev/null @@ -1,2 +0,0 @@ -pageindicator: Viewing page {_current}/{_total} | Type "{_prefix}help " for more information -notcommand: Looks like "{_command}" is not a command or category. \ No newline at end of file diff --git a/localization/nb_no/commands.yaml b/localization/nb_no/commands.yaml index 836223a..7ed7617 100644 --- a/localization/nb_no/commands.yaml +++ b/localization/nb_no/commands.yaml @@ -165,8 +165,8 @@ settings: description: 'Setter grensen for hvor mange prosent som må stemme for å hoppe over en sang.' # Cogs -cogs: - aliases: [cogs] +cogmanager: + aliases: [cogmanager] description: 'behandle cogs' sub_commands: load: diff --git a/localization/nb_no/nodemanager.yaml b/localization/nb_no/nodemanager.yaml deleted file mode 100644 index ffa8294..0000000 --- a/localization/nb_no/nodemanager.yaml +++ /dev/null @@ -1,2 +0,0 @@ -pageindicator: Viser side {_current}/{_total} | Skriv "{_prefix}hjelp " for mer informasjon -notcommand: Ser ut som "{_command}" ikke er en kategori eller en kommando. \ No newline at end of file diff --git a/localization/we_eb/commands.yaml b/localization/we_eb/commands.yaml index 82c62f6..7007219 100644 --- a/localization/we_eb/commands.yaml +++ b/localization/we_eb/commands.yaml @@ -165,8 +165,8 @@ settings: description: 'Set the vote thweshowd wequiwed to skip a sonyg.' # Cogs -cogs: - aliases: [cogs] +cogmanager: + aliases: [cogmanager] description: 'cog management' sub_commands: load: