From 4cf88681fd022bd4a8031b0ceed8b80b8acddb35 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Tue, 8 Sep 2020 12:01:45 +0200 Subject: [PATCH 01/15] Fixes #33 and adds reconnect Small bug fixes, fixes skip bug in #33 and some typos. Adds reconnect command for when the bot get stuck on a song. --- cogs/music.py | 33 +++++++++++++++++++++++++------- cogs/musicevents.py | 5 ++++- localization/en_en/commands.yaml | 2 +- localization/we_eb/commands.yaml | 2 +- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/cogs/music.py b/cogs/music.py index c775177..8ebb988 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -96,7 +96,7 @@ class Music(commands.Cog): await player.play() @commands.command(name='seek') - @checks.dj_or(alone=True) + @checks.dj_or(alone=True, track_requester=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) @@ -223,7 +223,7 @@ class Music(commands.Cog): await ctx.send(embed=embed) @commands.command(name='queue') - async def _queue(self, ctx, user: discord.Member = None): + async def _queue(self, ctx, member: discord.Member = None): """ Shows the player's queue. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) @@ -232,15 +232,16 @@ class Music(commands.Cog): embed = ctx.localizer.format_embed(embed) return await ctx.send(embed=embed) - if user is None: + if member is None: queue = player.global_queue() pagified_queue = QueuePaginator(ctx.localizer, queue, color=ctx.me.color) else: - user_queue = player.user_queue(user.id, dual=True) + user_queue = player.user_queue(member.id, dual=True) if not user_queue: - return await ctx.send(ctx.localizer.format_str("{queue.empty}", _user=user.name)) - pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, user_name=user.name) + return await ctx.send(ctx.localizer.format_str("{queue.empty}", _user=member.display_name)) + pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, + user_name=member.display_name) scroller = Scroller(ctx, pagified_queue) await scroller.start_scrolling() @@ -487,6 +488,24 @@ class Music(commands.Cog): embed = ctx.localizer.format_embed(embed) await ctx.send(embed=embed) + @commands.command(name='reconnect') + @checks.dj_or(alone=True) + 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) + if not player.is_connected: + return await ctx.send(ctx.localizer.format_str("{not_connected}")) + + if not ctx.author.voice or (player.is_connected and ctx.author.voice.channel.id != int(player.channel_id)): + return await ctx.send(ctx.localizer.format_str("{disconnect.not_in_voice}")) + + current_channel = player.channel_id + await player.stop() + await self.connect_to(ctx.guild.id, None) + await asyncio.sleep(1) # Pretend to do stuff? + await self.connect_to(ctx.guild.id, current_channel) + await player.play() + @commands.command(name='volume') @checks.dj_or(alone=True, track_requester=True) async def _volume(self, ctx, volume: int = None): @@ -723,7 +742,7 @@ class Music(commands.Cog): # Add commands that require joining voice to work. should_connect = ctx.command.callback.__name__ in ('_play', '_find', '_search', '_sk') - # Add commands that don't require the you being in voice. + # Add commands that don't require you being in voice. without_connect = ctx.command.callback.__name__ in ('_queue', '_history', '_now', '_lyrics') if without_connect: diff --git a/cogs/musicevents.py b/cogs/musicevents.py index 04d5091..74ed119 100644 --- a/cogs/musicevents.py +++ b/cogs/musicevents.py @@ -20,7 +20,10 @@ class MusicEvents(commands.Cog): async def track_hook(self, event): if isinstance(event, lavalink.events.TrackEndEvent): - pass # Send track ended message to channel. + 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): diff --git a/localization/en_en/commands.yaml b/localization/en_en/commands.yaml index 99a396b..6f603da 100644 --- a/localization/en_en/commands.yaml +++ b/localization/en_en/commands.yaml @@ -72,7 +72,7 @@ volume: args: '' description: 'Sets the volume of the music.' normalize: - aliases: [nomralize, normal] + aliases: [normalize, normal] description: 'Resets the volume and the equalizer.' boost: aliases: [boost, bassboost] diff --git a/localization/we_eb/commands.yaml b/localization/we_eb/commands.yaml index f5d541a..82c62f6 100644 --- a/localization/we_eb/commands.yaml +++ b/localization/we_eb/commands.yaml @@ -72,7 +72,7 @@ volume: args: '' description: 'Sets the vowume of the music.' normalize: - aliases: [nomralize, normal] + aliases: [normalize, normal] description: 'Resets the volume and the equalizer.' boost: aliases: [boost, bassboost] From 6f373ce20d3618911999d4661a194d143754d183 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Tue, 8 Sep 2020 12:13:29 +0200 Subject: [PATCH 02/15] flake8 eat shit --- cogs/music.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/music.py b/cogs/music.py index 8ebb988..79d7370 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -240,7 +240,7 @@ class Music(commands.Cog): user_queue = player.user_queue(member.id, dual=True) if not user_queue: return await ctx.send(ctx.localizer.format_str("{queue.empty}", _user=member.display_name)) - pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, + pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, user_name=member.display_name) scroller = Scroller(ctx, pagified_queue) @@ -502,7 +502,7 @@ class Music(commands.Cog): current_channel = player.channel_id await player.stop() await self.connect_to(ctx.guild.id, None) - await asyncio.sleep(1) # Pretend to do stuff? + await asyncio.sleep(1) # Pretend to do stuff? await self.connect_to(ctx.guild.id, current_channel) await player.play() From 2cb03065a38beea13d43f9d404607f33aa1e456a Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Thu, 10 Sep 2020 16:51:00 +0200 Subject: [PATCH 03/15] Move to decorators for command requirements Cleans up music.py quite a bit, makes it easier to define the properties you want when creating a new command. Some other small changes to reduce code duplication. --- .vscode/settings.json | 4 +- cogs/errors.py | 6 + cogs/music.py | 281 +++++++++++---------------------------- cogs/utils/checks.py | 3 +- cogs/utils/decorators.py | 124 +++++++++++++++++ cogs/utils/selector.py | 4 + 6 files changed, 219 insertions(+), 203 deletions(-) create mode 100644 cogs/utils/decorators.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f76c37..5978804 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { "python.languageSever": "Pylance", "python.linting.enabled": true, - "python.linting.pylintEnabled": true, + "python.linting.pylintEnabled": false, "python.linting.flake8Enabled": true, - "python.linting.pycodestyleEnabled": true, + "python.linting.pycodestyleEnabled": false, "[python]": { "editor.rulers": [120], }, diff --git a/cogs/errors.py b/cogs/errors.py index 92117d7..a44fcc7 100644 --- a/cogs/errors.py +++ b/cogs/errors.py @@ -57,6 +57,12 @@ class Errors(commands.Cog): elif (err.original == 'You need to be in my voicechannel.'): return await send_error_embed('{errors.my_channel}') + elif (err.original == 'Not playing'): + return await send_error_embed('{not_playing}') + + elif (err.original == 'Not listening'): + return await send_error_embed('{have_to_listen}') + if isinstance(err, commands.CommandOnCooldown): await ctx.send(f"{ctx.message.author.mention} Command is on cooldown. " f"Try again in `{err.retry_after:.1f}` seconds.") diff --git a/cogs/music.py b/cogs/music.py index 79d7370..32ee0f9 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -11,7 +11,6 @@ import json import math import re import urllib -from typing import Optional from bs4 import BeautifulSoup @@ -19,6 +18,9 @@ from .utils import checks, thumbnailer, timeformatter from .utils.paginator import QueuePaginator, Scroller, TextPaginator from .utils.selector import Selector +from .utils.decorators import require_voice_connection, require_playing, require_queue + + time_rx = re.compile('[0-9]+') url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') @@ -31,14 +33,15 @@ class Music(commands.Cog): async def cog_check(self, ctx): if not ctx.guild: raise commands.NoPrivateMessage - textchannels = self.bot.settings.get(ctx.guild, 'channels.text', []) - if textchannels: + if textchannels := self.bot.settings.get(ctx.guild, 'channels.text', []): if ctx.channel.id not in textchannels: return False return True async def cog_before_invoke(self, ctx): - await self.ensure_voice(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 + 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. """ @@ -46,6 +49,7 @@ class Music(commands.Cog): 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) @@ -64,8 +68,7 @@ class Music(commands.Cog): if results['loadType'] == 'PLAYLIST_LOADED': tracks = results['tracks'] - maxlength = self.max_track_length(ctx.guild, player) - if maxlength: + if maxlength := self.max_track_length(ctx.guild, player): numtracks = 0 for track in tracks: if track['info']['length'] <= maxlength: @@ -97,37 +100,29 @@ class Music(commands.Cog): @commands.command(name='seek') @checks.dj_or(alone=True, track_requester=True) + @require_voice_connection() + @require_playing(ensure_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) - if not player.is_playing: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - - if ctx.author not in player.listeners: - return await ctx.send(ctx.localizer.format_str("{have_to_listen}")) - - seconds = time_rx.search(time) - if not seconds: - return await ctx.send(ctx.localizer.format_str("{seek.missing_amount}")) - - seconds = int(seconds.group()) * 1000 - if time.startswith('-'): - seconds *= -1 - - track_time = player.position + seconds - await player.seek(track_time) - msg = ctx.localizer.format_str("{seek.track_moved}", _position=timeformatter.format_ms(track_time)) - await ctx.send(msg) + 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) - if not player.is_playing: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - player.add_skipper(ctx.author) total = len(player.listeners) skips = len(player.skip_voters) @@ -160,46 +155,39 @@ class Music(commands.Cog): @commands.command(name='skipto') @checks.dj_or(alone=True) + @require_playing(ensure_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 ctx.author not in player.listeners: - return await ctx.send(ctx.localizer.format_str("{have_to_listen}")) - 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(ensure_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) - - if ctx.author not in player.listeners: - return await ctx.send(ctx.localizer.format_str("{have_to_listen}")) - - if not player.is_playing: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - 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) - if not player.current: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - position = timeformatter.format_ms(player.position) if player.current.stream: duration = '{live}' @@ -223,51 +211,43 @@ class Music(commands.Cog): await ctx.send(embed=embed) @commands.command(name='queue') - async def _queue(self, ctx, member: discord.Member = None): + @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 player.queue.empty: - embed = discord.Embed(description='{queue.empty}', color=ctx.me.color) - embed = ctx.localizer.format_embed(embed) - return await ctx.send(embed=embed) - if member is None: queue = player.global_queue() pagified_queue = QueuePaginator(ctx.localizer, queue, color=ctx.me.color) else: - user_queue = player.user_queue(member.id, dual=True) - if not user_queue: - return await ctx.send(ctx.localizer.format_str("{queue.empty}", _user=member.display_name)) - pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, + 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) - if not user_queue: - return await ctx.send(ctx.localizer.format_str("{my_queue}")) - - pagified_queue = QueuePaginator(ctx.localizer, user_queue, color=ctx.me.color, user_name=ctx.author.name) + 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 not player.is_playing: - return await ctx.send(ctx.localizer.format_str("{not_playing}")) - if player.paused: await player.set_pause(False) await ctx.send(ctx.localizer.format_str("{resume.resumed}")) @@ -276,29 +256,23 @@ class Music(commands.Cog): 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) - - user_queue = player.user_queue(ctx.author.id) - - if not user_queue: - return await ctx.send(ctx.localizer.format_str("{my_queue}")) - 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) - if player.queue.empty: - return - user_queue = player.user_queue(ctx.author.id, dual=True) - if not user_queue: - return await ctx.send(ctx.localizer.format_str("{my_queue}")) + # Setup for selector identifiers = [] for index, temp in enumerate(user_queue): @@ -346,19 +320,16 @@ class Music(commands.Cog): embed = ctx.localizer.format_embed(embed) await message.edit(embed=embed) - await message.clear_reactions() @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) - if player.queue.empty: - return - user_queue = player.user_queue(ctx.author.id, dual=True) - if not user_queue: - return await ctx.send(ctx.localizer.format_str("{my_queue}")) - + 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 @@ -366,68 +337,49 @@ class Music(commands.Cog): _globalindex=globpos+1, _title=track.title, _uri=track.uri)) - functions = [player.remove_user_track]*len(user_queue) - arguments = [(ctx.author.id, i) for i in range(len(user_queue))] - selector = Selector(ctx, identifiers, functions, arguments, num_selections=5, color=0xFF0000, title='Remove song plz') - message, page, removed = await selector.start_scrolling() + message, _, removed = await selector.start_scrolling() await message.edit(content=ctx.localizer.format_str("{remove}", _title=removed.title), embed=None) - await message.clear_reactions() @commands.command(name="DJremove") @checks.dj_or() - async def _djremove(self, ctx, pos: int, user: discord.Member = None): + @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) - if user is None: - if player.queue.empty: - return + # 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(player.queue) or pos < 1: - return await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(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) - removed = player.remove_global_track(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: - if player.queue.empty: - return - - user_queue = player.user_queue(user.id) - if not user_queue: - return - - if pos > len(user_queue) or pos < 1: - return await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(user_queue))) - - removed = player.remove_user_track(user.id, pos - 1) - await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, _user=removed.requester.name)) + await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(queue))) @commands.command(name='removeuser') @checks.dj_or(alone=True) - async def _user_queue_remove(self, ctx, user: discord.Member): + @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) - if player.queue.empty: - return - - user_queue = player.user_queue(user.id) - if not user_queue: - return - - player.remove_user_queue(user.id) - - embed = discord.Embed(color=ctx.me.color) - embed.description = "{dj_remove_user}" - embed = ctx.localizer.format_embed(embed, _id=user.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) @@ -464,23 +416,17 @@ class Music(commands.Cog): result = ctx.localizer.format_embed(result) await message.edit(embed=result) - await message.clear_reactions() 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) - if not player.is_connected: - return await ctx.send(ctx.localizer.format_str("{not_connected}")) - - if not ctx.author.voice or (player.is_connected and ctx.author.voice.channel.id != int(player.channel_id)): - return await ctx.send(ctx.localizer.format_str("{disconnect.not_in_voice}")) - player.queue.clear() await player.stop() await self.connect_to(ctx.guild.id, None) @@ -490,24 +436,21 @@ class Music(commands.Cog): @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) - if not player.is_connected: - return await ctx.send(ctx.localizer.format_str("{not_connected}")) - - if not ctx.author.voice or (player.is_connected and ctx.author.voice.channel.id != int(player.channel_id)): - return await ctx.send(ctx.localizer.format_str("{disconnect.not_in_voice}")) - current_channel = player.channel_id + await player.stop() await self.connect_to(ctx.guild.id, None) - await asyncio.sleep(1) # Pretend to do stuff? + 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) @@ -517,11 +460,11 @@ class Music(commands.Cog): is_dj = checks.is_dj(ctx) is_admin = getattr(ctx.author.guild_permissions, 'administrator', None) is True - if int(player.current.requester) == ctx.author.id and not is_dj and not is_admin: + 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(color=ctx.me.color) embed.description = "{volume.set_to}" embed = ctx.localizer.format_embed(embed, _volume=player.volume) @@ -529,17 +472,16 @@ class Music(commands.Cog): @commands.command(name='normalize') @checks.dj_or(alone=True) + @require_voice_connection() async def _normalize(self, ctx): - """ Reset the equalizer and """ + """ 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(color=ctx.me.color) - embed.description = '{volume.reset}' + 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') @@ -548,15 +490,11 @@ class Music(commands.Cog): """ Set the equalizer to bass boost the music """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) - if boost is not None: + if boost: await player.bassboost(boost) embed = discord.Embed(color=ctx.me.color) - - if player.boosted: - embed.description = '{boost.on}' - else: - embed.description = '{boost.off}' + embed.description = '{boost.on}' if player.boosted else '{boost.off}' embed = ctx.localizer.format_embed(embed) await ctx.send(embed=embed) @@ -653,16 +591,13 @@ class Music(commands.Cog): @commands.command(name='scrub') @checks.dj_or(alone=True) + @require_voice_connection() + @require_playing(ensure_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}' - embed = discord.Embed(description='{nothing_playing}', color=ctx.me.color) - embed = ctx.localizer.format_embed(embed) - - if player.current is None: - return await ctx.send(embed=embed) scrubber = [ ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', player.seek, -1000), @@ -692,7 +627,7 @@ class Music(commands.Cog): return True return False - embed.description = '{scrub.add}' + embed = discord.Embed(description='{scrub.add}', color=ctx.me.color) embed = ctx.localizer.format_embed(embed) msg = await ctx.send(embed=embed) @@ -727,60 +662,6 @@ class Music(commands.Cog): arg = player.position + arg * 1000 await selection(arg) - async def ensure_voice(self, ctx, do_connect: Optional[bool] = None): - """ This check ensures that the bot and command author are in the same voicechannel. """ - - # If region doesn't return a known server location - try: - region = ctx.guild.region.value - except AttributeError: - region = ctx.guild.region - - player = self.bot.lavalink.player_manager.create(ctx.guild.id, endpoint=region) - # Create returns a player if one exists, otherwise creates. - - # Add commands that require joining voice to work. - should_connect = ctx.command.callback.__name__ in ('_play', '_find', '_search', '_sk') - - # Add commands that don't require you being in voice. - without_connect = ctx.command.callback.__name__ in ('_queue', '_history', '_now', '_lyrics') - - if without_connect: - return - - if not ctx.author.voice or not ctx.author.voice.channel: - raise commands.CommandInvokeError('Join a voicechannel first.') - - if not player.is_connected: - if not should_connect: - raise commands.CommandInvokeError('Not connected.') - - voice_channel = ctx.author.voice.channel - - permissions = voice_channel.permissions_for(ctx.me) - - if not permissions.connect or not permissions.speak: # Check user limit too? - raise commands.CommandInvokeError('I need the `CONNECT` and `SPEAK` permissions.') - - voice_channels = self.bot.settings.get(ctx.guild, 'channels.music', []) - - if voice_channels: - if voice_channel.id not in voice_channels: - response = ctx.localizer.format_str('{settings_check.voicechannel}') - for channel_id in voice_channels: - channel = ctx.guild.get_channel(channel_id) - if channel is not None: - response += f'{channel.name}, ' - await ctx.send(response[:-2]) - raise commands.CommandInvokeError('You need to be in the right voice channel') - - player.store('channel', ctx.channel.id) - await self.connect_to(ctx.guild.id, str(ctx.author.voice.channel.id)) - - else: - if int(player.channel_id) != ctx.author.voice.channel.id: - raise commands.CommandInvokeError('You need to be in my voicechannel.') - async def enqueue(self, ctx, track, embed): player = self.bot.lavalink.player_manager.get(ctx.guild.id) diff --git a/cogs/utils/checks.py b/cogs/utils/checks.py index 75e22db..96a1a16 100644 --- a/cogs/utils/checks.py +++ b/cogs/utils/checks.py @@ -62,6 +62,7 @@ def dj_or(alone: bool = False, track_requester: bool = False): async def predicate(ctx): try: player = ctx.bot.lavalink.player_manager.get(ctx.guild.id) + # Make this false if no player is_alone = (ctx.author in player.listeners and len(player.listeners) == 1) and alone requester = (player.current.requester == ctx.author.id) and track_requester @@ -70,6 +71,6 @@ def dj_or(alone: bool = False, track_requester: bool = False): is_alone = False dj = is_dj(ctx) - + print(dj, is_alone, requester) return dj or is_alone or requester return commands.check(predicate) diff --git a/cogs/utils/decorators.py b/cogs/utils/decorators.py new file mode 100644 index 0000000..b5c613a --- /dev/null +++ b/cogs/utils/decorators.py @@ -0,0 +1,124 @@ +import functools +import discord +from discord.ext import commands +import inspect + + +def ensure_voice_connection(should_connect=False): + """ + Checks if the bot is in a valid voice channel for the command + should_connect indicates whether the bot should try to join a channel + """ + def ensure_voice_proper(func): + @functools.wraps(func) + async def ensure_voice_inner(self, ctx, *command_args, **kwargs): + """ This check ensures that the bot and command author are in the same voicechannel. """ + + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + if not ctx.author.voice or not ctx.author.voice.channel: + raise commands.CommandInvokeError('Join a voicechannel first.') + + if not player.is_connected: + if not should_connect: + raise commands.CommandInvokeError('Not connected.') + + user_voice_channel = ctx.author.voice.channel + permissions = user_voice_channel.permissions_for(ctx.me) + + if not permissions.connect or not permissions.speak: # Check user limit too? + raise commands.CommandInvokeError('I need the `CONNECT` and `SPEAK` permissions.') + + # Check against music channel restrictions in bot settings + if voice_channels := self.bot.settings.get(ctx.guild, 'channels.music', []): + if user_voice_channel.id not in voice_channels: + response = ctx.localizer.format_str('{settings_check.voicechannel}') + for channel_id in voice_channels: + channel = ctx.guild.get_channel(channel_id) + if channel is not None: + response += f'{channel.name}, ' + # TODO: Make this send the channels in question to the error, if not it is not that useful of + # an error message + await ctx.send(response[:-2]) + raise commands.CommandInvokeError('You need to be in the right voice channel') + + player.store('channel', ctx.channel.id) + await self.connect_to(ctx.guild.id, str(ctx.author.voice.channel.id)) + + elif int(player.channel_id) != ctx.author.voice.channel.id: + raise commands.CommandInvokeError('You need to be in my voicechannel.') + + await func(self, ctx, *command_args, **kwargs) + + return ensure_voice_inner + return ensure_voice_proper + + +def require_playing(ensure_user_listening=False): + """ + Checks if the bot is currently playing a track + ensure_user_listening: also checks if the user is listening to the bot + """ + def ensure_play(func): + @functools.wraps(func) + async def ensure_play_inner(self, ctx, *command_args, **kwargs): + + if player := self.bot.lavalink.player_manager.get(ctx.guild.id): + + if not player.is_playing: + raise commands.CommandInvokeError('Not playing') + + if ensure_user_listening and (ctx.author not in player.listeners): + raise commands.CommandInvokeError('Not listening') + + else: + raise Exception('Player object does not yet exist') + + await func(self, ctx, *command_args, **kwargs) + + return ensure_play_inner + return ensure_play + + +def require_queue(require_member_queue=False, require_author_queue=False): + """ + Checks if there is something queued + require_member_queue also checks if the queue of a member is empty, only works when member is an argument + """ + def ensure_queue(func): + if require_member_queue: + inspection = inspect.getfullargspec(func) + assert 'member' in inspection.kwonlyargs, """require_member_error can only be used + on commands with a member keyword argument""" + # assert str(inspection.annotations['member']) == "", """ + # Member must be a discord member type""" + + @functools.wraps(func) + async def ensure_queue_inner(self, ctx, *command_args, **kwargs): + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + if require_member_queue: + try: + if member := kwargs['member']: # Ignore if member is None + user_queue = player.user_queue(member.id) + if not user_queue: + embed = discord.Embed(description='{queue.user_empty}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed, _user=member.display_name) + return await ctx.send(embed=embed) + except KeyError: + raise Exception("require_member_error can only be used on commands with a member keyword argument") + + if require_author_queue: + user_queue = player.user_queue(ctx.author.id) + if not user_queue: + embed = discord.Embed(description='{my_queue}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed) + return await ctx.send(embed=embed) + + if player.queue.empty: + embed = discord.Embed(description='{queue.empty}', color=ctx.me.color) + embed = ctx.localizer.format_embed(embed) + return await ctx.send(embed=embed) + + await func(self, ctx, *command_args, **kwargs) + return ensure_queue_inner + return ensure_queue diff --git a/cogs/utils/selector.py b/cogs/utils/selector.py index 3e93e60..db54d1c 100644 --- a/cogs/utils/selector.py +++ b/cogs/utils/selector.py @@ -191,6 +191,10 @@ class Selector(TextPaginator): self.args = None self.control_input = False if len(results) == 1: + self.stopped = True + await self.message.clear_reactions() return self.message, self.pages[self.current_page], results[0] else: + self.stopped = True + await self.message.clear_reactions() return self.message, self.pages[self.current_page], results From ee9ed63ba8679b2db95f2313f582f19b110c06c4 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Thu, 10 Sep 2020 17:28:26 +0200 Subject: [PATCH 04/15] Stupid flake 8 >:( --- cogs/errors.py | 3 +++ cogs/music.py | 15 +++++++-------- cogs/utils/checks.py | 2 -- cogs/utils/decorators.py | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cogs/errors.py b/cogs/errors.py index a44fcc7..ab9a645 100644 --- a/cogs/errors.py +++ b/cogs/errors.py @@ -63,6 +63,9 @@ class Errors(commands.Cog): elif (err.original == 'Not listening'): return await send_error_embed('{have_to_listen}') + if isinstance(err, commands.CheckFailure): + pass + if isinstance(err, commands.CommandOnCooldown): await ctx.send(f"{ctx.message.author.mention} Command is on cooldown. " f"Try again in `{err.retry_after:.1f}` seconds.") diff --git a/cogs/music.py b/cogs/music.py index 32ee0f9..96eaab3 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -41,6 +41,7 @@ class Music(commands.Cog): 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): @@ -101,7 +102,7 @@ class Music(commands.Cog): @commands.command(name='seek') @checks.dj_or(alone=True, track_requester=True) @require_voice_connection() - @require_playing(ensure_user_listening=True) + @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) @@ -155,7 +156,7 @@ class Music(commands.Cog): @commands.command(name='skipto') @checks.dj_or(alone=True) - @require_playing(ensure_user_listening=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) @@ -172,7 +173,7 @@ class Music(commands.Cog): @commands.command(name='stop') @checks.dj_or(alone=True) @require_voice_connection() - @require_playing(ensure_user_listening=True) + @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) @@ -239,7 +240,6 @@ class Music(commands.Cog): scroller = Scroller(ctx, pagified_queue) await scroller.start_scrolling() - @commands.command(name='pause') @checks.dj_or(alone=True) @require_voice_connection() @@ -465,8 +465,7 @@ class Music(commands.Cog): return await ctx.send(ctx.localizer.format_str("{volume.out_of_range}")) await player.set_volume(volume) - embed = discord.Embed(color=ctx.me.color) - embed.description = "{volume.set_to}" + 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) @@ -480,7 +479,7 @@ class Music(commands.Cog): await player.set_volume(100) await player.bassboost(False) - embed = discord.Embed(description='{volume.reset}' , color=ctx.me.color) + embed = discord.Embed(description='{volume.reset}', color=ctx.me.color) embed = ctx.localizer.format_embed(embed) await ctx.send(embed=embed) @@ -592,7 +591,7 @@ class Music(commands.Cog): @commands.command(name='scrub') @checks.dj_or(alone=True) @require_voice_connection() - @require_playing(ensure_user_listening=True) + @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) diff --git a/cogs/utils/checks.py b/cogs/utils/checks.py index 96a1a16..cce1aa2 100644 --- a/cogs/utils/checks.py +++ b/cogs/utils/checks.py @@ -62,7 +62,6 @@ def dj_or(alone: bool = False, track_requester: bool = False): async def predicate(ctx): try: player = ctx.bot.lavalink.player_manager.get(ctx.guild.id) - # Make this false if no player is_alone = (ctx.author in player.listeners and len(player.listeners) == 1) and alone requester = (player.current.requester == ctx.author.id) and track_requester @@ -71,6 +70,5 @@ def dj_or(alone: bool = False, track_requester: bool = False): is_alone = False dj = is_dj(ctx) - print(dj, is_alone, requester) return dj or is_alone or requester return commands.check(predicate) diff --git a/cogs/utils/decorators.py b/cogs/utils/decorators.py index b5c613a..522239f 100644 --- a/cogs/utils/decorators.py +++ b/cogs/utils/decorators.py @@ -4,7 +4,7 @@ from discord.ext import commands import inspect -def ensure_voice_connection(should_connect=False): +def require_voice_connection(should_connect=False): """ Checks if the bot is in a valid voice channel for the command should_connect indicates whether the bot should try to join a channel @@ -53,7 +53,7 @@ def ensure_voice_connection(should_connect=False): return ensure_voice_proper -def require_playing(ensure_user_listening=False): +def require_playing(require_user_listening=False): """ Checks if the bot is currently playing a track ensure_user_listening: also checks if the user is listening to the bot @@ -67,7 +67,7 @@ def require_playing(ensure_user_listening=False): if not player.is_playing: raise commands.CommandInvokeError('Not playing') - if ensure_user_listening and (ctx.author not in player.listeners): + if require_user_listening and (ctx.author not in player.listeners): raise commands.CommandInvokeError('Not listening') else: From 274ed02aa7e8ae5dd3cc1e4527301cceb5b1b371 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Thu, 10 Sep 2020 17:31:14 +0200 Subject: [PATCH 05/15] isort >:( --- cogs/music.py | 4 +--- cogs/utils/decorators.py | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/music.py b/cogs/music.py index 96eaab3..df1cf6f 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -15,12 +15,10 @@ 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 -from .utils.decorators import require_voice_connection, require_playing, require_queue - - time_rx = re.compile('[0-9]+') url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') diff --git a/cogs/utils/decorators.py b/cogs/utils/decorators.py index 522239f..f271bc1 100644 --- a/cogs/utils/decorators.py +++ b/cogs/utils/decorators.py @@ -1,6 +1,8 @@ -import functools +# Discord Packages import discord from discord.ext import commands + +import functools import inspect From 898ea665369456a80580a028bae52d451568766c Mon Sep 17 00:00:00 2001 From: Roxedus Date: Thu, 10 Sep 2020 19:18:17 +0200 Subject: [PATCH 06/15] Crude custom error setup --- cogs/errors.py | 15 +++++++++++---- cogs/music.py | 11 ----------- cogs/utils/decorators.py | 10 ++++++---- cogs/utils/music_errors.py | 12 ++++++++++++ 4 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 cogs/utils/music_errors.py diff --git a/cogs/errors.py b/cogs/errors.py index ab9a645..152cf4c 100644 --- a/cogs/errors.py +++ b/cogs/errors.py @@ -6,6 +6,7 @@ import traceback # Bot Utilities from cogs.helpformatter import commandhelper +from cogs.utils.music_errors import WrongVoiceChannelError from cogs.utils.paginator import Scroller @@ -34,9 +35,11 @@ class Errors(commands.Cog): if isinstance(err, (commands.CommandNotFound)): return - async def send_error_embed(description): + async def send_error_embed(description, **kwargs): embed = await self.base_msg(ctx, state=0xFC0303) embed.description = description + if kwargs.get("title"): + embed.title = kwargs["title"] embed = ctx.localizer.format_embed(embed) return await ctx.send(embed=embed) @@ -51,9 +54,6 @@ class Errors(commands.Cog): elif (err.original == 'I need the `CONNECT` and `SPEAK` permissions.'): return await send_error_embed('{errors.need_permission}') - elif (err.original == 'You need to be in the right voice channel'): - return await send_error_embed('{errors.right_channel}') - elif (err.original == 'You need to be in my voicechannel.'): return await send_error_embed('{errors.my_channel}') @@ -63,6 +63,13 @@ class Errors(commands.Cog): elif (err.original == 'Not listening'): return await send_error_embed('{have_to_listen}') + if isinstance(err, WrongVoiceChannelError): + if (err.original == 'You need to be in the right voice channel'): + kwargs = {} + if err.channels: + kwargs['title'] = err.channels + return await send_error_embed('{errors.right_channel}', **kwargs) + if isinstance(err, commands.CheckFailure): pass diff --git a/cogs/music.py b/cogs/music.py index df1cf6f..2b7a0c1 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -713,17 +713,6 @@ class Music(commands.Cog): else: return maxlength*60*1000 - @commands.Cog.listener() - async def on_command_error(self, ctx, err): - if isinstance(err, commands.CheckFailure): - textchannels = self.bot.settings.get(ctx.guild, 'channels.text', []) - if textchannels: - if ctx.channel.id not in textchannels: - response = ctx.localizer.format_str('{settings_check.textchannel}') - for channel_id in textchannels: - response += f'<#{channel_id}>, ' - await ctx.send(response[:-2]) - def setup(bot): bot.add_cog(Music(bot)) diff --git a/cogs/utils/decorators.py b/cogs/utils/decorators.py index f271bc1..9a0d22e 100644 --- a/cogs/utils/decorators.py +++ b/cogs/utils/decorators.py @@ -5,6 +5,9 @@ from discord.ext import commands import functools import inspect +# Bot Utilities +from cogs.utils.music_errors import WrongVoiceChannelError + def require_voice_connection(should_connect=False): """ @@ -38,10 +41,9 @@ def require_voice_connection(should_connect=False): channel = ctx.guild.get_channel(channel_id) if channel is not None: response += f'{channel.name}, ' - # TODO: Make this send the channels in question to the error, if not it is not that useful of - # an error message - await ctx.send(response[:-2]) - raise commands.CommandInvokeError('You need to be in the right voice channel') + + raise WrongVoiceChannelError( + 'You need to be in the right voice channel', channels=response[:-2]) player.store('channel', ctx.channel.id) await self.connect_to(ctx.guild.id, str(ctx.author.voice.channel.id)) diff --git a/cogs/utils/music_errors.py b/cogs/utils/music_errors.py new file mode 100644 index 0000000..502a2fe --- /dev/null +++ b/cogs/utils/music_errors.py @@ -0,0 +1,12 @@ +# define Python user-defined exceptions +# Discord Packages +from discord.ext.commands import CommandError + + +class WrongVoiceChannelError(CommandError): + """Raised when a command requires the user to be in the correct voicechannel""" + + def __init__(self, e, channels=None): + self.original = e + self.channels = channels + super().__init__('You need to be in the right voice channel') From fc5bf719fbe64c96213d50961bf62547904ceb61 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Thu, 10 Sep 2020 21:16:13 +0200 Subject: [PATCH 07/15] Added errors for wrong voice and text channels --- cogs/errors.py | 29 +++++++++++++++++++++++------ cogs/music.py | 3 ++- cogs/utils/decorators.py | 8 +------- cogs/utils/music_errors.py | 18 +++++++++++++----- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/cogs/errors.py b/cogs/errors.py index 152cf4c..4ffb5ea 100644 --- a/cogs/errors.py +++ b/cogs/errors.py @@ -6,7 +6,7 @@ import traceback # Bot Utilities from cogs.helpformatter import commandhelper -from cogs.utils.music_errors import WrongVoiceChannelError +from cogs.utils.music_errors import WrongTextChannelError, WrongVoiceChannelError from cogs.utils.paginator import Scroller @@ -64,11 +64,28 @@ class Errors(commands.Cog): return await send_error_embed('{have_to_listen}') if isinstance(err, WrongVoiceChannelError): - if (err.original == 'You need to be in the right voice channel'): - kwargs = {} - if err.channels: - kwargs['title'] = err.channels - return await send_error_embed('{errors.right_channel}', **kwargs) + changed = False + response = ctx.localizer.format_str('{settings_check.voicechannel}') + for channel_id in err.channels: + channel = ctx.guild.get_channel(channel_id) + if channel is not None: + response += f'{channel.name}, ' + changed = True + if changed: + return await send_error_embed(response[:-2], title='{errors.right_channel}') + else: + return await send_error_embed('{errors.right_channel}') + + if isinstance(err, WrongTextChannelError): + try: + await ctx.message.delete() + except Exception as e: + self.logger.debug("Error deleting message: %s\nTraceback: %s" % (e, err)) + response = ctx.localizer.format_str('{settings_check.textchannel}') + for channel_id in err.channels: + response += f'<#{channel_id}>, ' + msg = await send_error_embed(response[:-2], title='{errors.right_channel}') + return await msg.delete(delay=5) if isinstance(err, commands.CheckFailure): pass diff --git a/cogs/music.py b/cogs/music.py index 2b7a0c1..95ddd94 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -16,6 +16,7 @@ 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 @@ -33,7 +34,7 @@ class Music(commands.Cog): raise commands.NoPrivateMessage if textchannels := self.bot.settings.get(ctx.guild, 'channels.text', []): if ctx.channel.id not in textchannels: - return False + raise WrongTextChannelError('You need to be in the right text channel', channels=textchannels) return True async def cog_before_invoke(self, ctx): diff --git a/cogs/utils/decorators.py b/cogs/utils/decorators.py index 9a0d22e..bdb3e0d 100644 --- a/cogs/utils/decorators.py +++ b/cogs/utils/decorators.py @@ -36,14 +36,8 @@ def require_voice_connection(should_connect=False): # Check against music channel restrictions in bot settings if voice_channels := self.bot.settings.get(ctx.guild, 'channels.music', []): if user_voice_channel.id not in voice_channels: - response = ctx.localizer.format_str('{settings_check.voicechannel}') - for channel_id in voice_channels: - channel = ctx.guild.get_channel(channel_id) - if channel is not None: - response += f'{channel.name}, ' - raise WrongVoiceChannelError( - 'You need to be in the right voice channel', channels=response[:-2]) + 'You need to be in the right voice channel', channels=voice_channels) player.store('channel', ctx.channel.id) await self.connect_to(ctx.guild.id, str(ctx.author.voice.channel.id)) diff --git a/cogs/utils/music_errors.py b/cogs/utils/music_errors.py index 502a2fe..24a2f41 100644 --- a/cogs/utils/music_errors.py +++ b/cogs/utils/music_errors.py @@ -3,10 +3,18 @@ from discord.ext.commands import CommandError -class WrongVoiceChannelError(CommandError): - """Raised when a command requires the user to be in the correct voicechannel""" - - def __init__(self, e, channels=None): +class WrongChannelError(CommandError): + def __init__(self, e: str, channels=None): self.original = e self.channels = channels - super().__init__('You need to be in the right voice channel') + super().__init__() + + +class WrongVoiceChannelError(WrongChannelError): + def __init__(self, e: str, channels=None): + super().__init__(e, channels=channels) + + +class WrongTextChannelError(WrongChannelError): + def __init__(self, e: str, channels=None): + super().__init__(e, channels=channels) 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 08/15] 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: From acbad1a61a0c8d3a24d595bd6fc2a465cad9ed72 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Sat, 12 Sep 2020 18:16:51 +0200 Subject: [PATCH 09/15] Moved decorators added node change command --- .vscode/settings.json | 12 +- cogs/music/__init__.py | 6 +- cogs/music/basic_commands.py | 231 +++------------------------- cogs/{utils => music}/decorators.py | 0 cogs/music/events.py | 2 + cogs/music/extra_commands.py | 215 ++++++++++++++++++++++++++ cogs/nodemanager.py | 20 ++- 7 files changed, 271 insertions(+), 215 deletions(-) rename cogs/{utils => music}/decorators.py (100%) create mode 100644 cogs/music/extra_commands.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 5978804..8500405 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,14 @@ "[Dockerfile]": { "editor.formatOnSave": false }, - "editor.tabSize": 4 - + "editor.tabSize": 4, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/*.pyc": {"when": "$(basename).py"}, + "**/__pycache__": true, + }, } \ No newline at end of file diff --git a/cogs/music/__init__.py b/cogs/music/__init__.py index d12546c..e72d0f3 100644 --- a/cogs/music/__init__.py +++ b/cogs/music/__init__.py @@ -94,10 +94,12 @@ class Music(commands.Cog): # 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) + _disconnect, _djremove, _move, _myqueue, _now, _pause, _play, _queue, _reconnect, _remove, _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 + # commands + from .extra_commands import _boost, _history, _lyrics, _normalize, _scrub def setup(bot): diff --git a/cogs/music/basic_commands.py b/cogs/music/basic_commands.py index 61eee23..b6c7169 100644 --- a/cogs/music/basic_commands.py +++ b/cogs/music/basic_commands.py @@ -1,20 +1,20 @@ +""" +Commands that interacti with just discord and lavalink and only present the basic commands needed for a music bot +""" + # 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.paginator import QueuePaginator, Scroller from ..utils.selector import Selector +from .decorators import require_playing, require_queue, require_voice_connection time_rx = re.compile('[0-9]+') url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') @@ -340,7 +340,7 @@ async def _djremove(self, ctx, pos: int, member: discord.Member = None): 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)) + await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, _user=requester.display_name)) else: await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(queue))) @@ -426,11 +426,20 @@ async def _reconnect(self, ctx): 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() + async def recon(): + 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) + + if player.current: + track = player.current + start_time = player.position + await recon() + if track: + await player.play(track, start_time=start_time) + else: + recon() @commands.command(name='volume') @@ -453,201 +462,3 @@ async def _volume(self, ctx, volume: int = None): 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/utils/decorators.py b/cogs/music/decorators.py similarity index 100% rename from cogs/utils/decorators.py rename to cogs/music/decorators.py diff --git a/cogs/music/events.py b/cogs/music/events.py index 5ddd5bf..c1fbc69 100644 --- a/cogs/music/events.py +++ b/cogs/music/events.py @@ -28,6 +28,8 @@ async def track_hook(self, event): pass if isinstance(event, lavalink.events.NodeChangedEvent): pass + if isinstance(event, lavalink.events.TrackStuckEvent): + print(event) @commands.Cog.listener() diff --git a/cogs/music/extra_commands.py b/cogs/music/extra_commands.py new file mode 100644 index 0000000..779540c --- /dev/null +++ b/cogs/music/extra_commands.py @@ -0,0 +1,215 @@ +""" +Commands that add to the music bot experience, like equalizers, commands that access the web, like lyrics, and +commands that are complex and mostly for show, like scrubber. +""" + +# Discord Packages +import discord +from discord.ext import commands + +import asyncio +import json +import re +import urllib + +from bs4 import BeautifulSoup + +from ..utils import checks +from ..utils.paginator import Scroller, TextPaginator +from .decorators import require_playing, require_voice_connection + + +@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, 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/nodemanager.py b/cogs/nodemanager.py index 58c4265..fce427a 100644 --- a/cogs/nodemanager.py +++ b/cogs/nodemanager.py @@ -119,7 +119,7 @@ class NodeManager(commands.Cog): @_node.command(name='add') @commands.is_owner() async def _add(self, ctx, host, port, password, region, name=None): - self.bot.lavalink.add_node(host, port, password, region, name) + self.bot.lavalink.add_node(host, port, password, region, name=name) self.logger.debug("Adding Lavalink node: %s on %s with the port %s in %s" % (host, port, region, name,)) embed = await self._node_presenter(ctx, {'host': host, 'port': port, 'password': password, 'region': region, 'name': name}) @@ -152,6 +152,24 @@ class NodeManager(commands.Cog): if not sent_feedback: await ctx.send('No node found') + @_node.command(name='change') + @commands.is_owner() + async def _nodechange(self, ctx, node=None): + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + if node is None: + await player.change_node(player.node) + + else: + newnode = None + for _node in self.bot.lavalink.node_manager.nodes: + if (_node.name or _node.host) == node: + newnode = _node + + if not newnode: + return + await player.change_node(newnode) + def setup(bot): bot.add_cog(NodeManager(bot)) From 430c58991b10ac04a6b3c015ca30d424c8159be8 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Sat, 12 Sep 2020 18:21:58 +0200 Subject: [PATCH 10/15] gina linetti spaghetti confetti interacti - Rox --- cogs/music/basic_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/music/basic_commands.py b/cogs/music/basic_commands.py index b6c7169..c3612d7 100644 --- a/cogs/music/basic_commands.py +++ b/cogs/music/basic_commands.py @@ -1,5 +1,5 @@ """ -Commands that interacti with just discord and lavalink and only present the basic commands needed for a music bot +Commands that interact with just discord and lavalink and only present the basic commands needed for a music bot """ # Discord Packages From 35cc8fdb1f0c402535008674f848c1d052cdb3cf Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Sat, 12 Sep 2020 19:55:14 +0200 Subject: [PATCH 11/15] Store nodes in settings, edded error for full voice channels. Edit settingsmanager to allow storing settings that are not guild specific. --- cogs/errors.py | 3 +++ cogs/music/decorators.py | 7 +++++-- cogs/nodemanager.py | 26 +++++++++++++++++++++++++- cogs/utils/settingsmanager.py | 32 ++++++++++++++++++++------------ localization/en_en/music.yaml | 1 + localization/nb_no/music.yaml | 1 + 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/cogs/errors.py b/cogs/errors.py index e81588d..d538c8e 100644 --- a/cogs/errors.py +++ b/cogs/errors.py @@ -64,6 +64,9 @@ class Errors(commands.Cog): elif (err.original == 'Not listening'): return await send_error_embed('{have_to_listen}') + elif (err.original == 'The channel is currently full'): + return await send_error_embed('{errors.full_channel}') + if isinstance(err, WrongVoiceChannelError): changed = False response = ctx.localizer.format_str('{settings_check.voicechannel}') diff --git a/cogs/music/decorators.py b/cogs/music/decorators.py index 99be11e..4ab36da 100644 --- a/cogs/music/decorators.py +++ b/cogs/music/decorators.py @@ -13,7 +13,7 @@ def require_voice_connection(should_connect=False): Checks if the bot is in a valid voice channel for the command should_connect indicates whether the bot should try to join a channel """ - def ensure_voice_proper(func): + def ensure_voice(func): @functools.wraps(func) async def ensure_voice_inner(self, ctx, *command_args, **kwargs): """ This check ensures that the bot and command author are in the same voicechannel. """ @@ -32,6 +32,9 @@ def require_voice_connection(should_connect=False): if not permissions.connect or not permissions.speak: # Check user limit too? raise commands.CommandInvokeError('I need the `CONNECT` and `SPEAK` permissions.') + if (len(user_voice_channel.members) == user_voice_channel.user_limit) and not permissions.administrator: + raise commands.CommandInvokeError('The channel is currently full') + # Check against music channel restrictions in bot settings if voice_channels := self.bot.settings.get(ctx.guild, 'channels.music', []): if user_voice_channel.id not in voice_channels: @@ -47,7 +50,7 @@ def require_voice_connection(should_connect=False): await func(self, ctx, *command_args, **kwargs) return ensure_voice_inner - return ensure_voice_proper + return ensure_voice def require_playing(require_user_listening=False): diff --git a/cogs/nodemanager.py b/cogs/nodemanager.py index fce427a..5adc86f 100644 --- a/cogs/nodemanager.py +++ b/cogs/nodemanager.py @@ -48,9 +48,10 @@ class NodeManager(commands.Cog): for node in self.bot.lavalink.node_manager.nodes: name_cache.append(node.name) - for node in conf['lavalink nodes']: + for node in conf['lavalink nodes'] + self.settings.get('lavalink', 'nodes', []): if node['name'] in name_cache: continue + name_cache.append(node['name']) self.bot.lavalink.add_node(**node) self.logger.debug("Adding Lavalink node: %s on %s with the port %s in %s" % ( @@ -99,6 +100,15 @@ class NodeManager(commands.Cog): return embed + def get_node_properties(self, node): + return { + 'name': node.name, + 'host': node.host, + 'port': node.port, + 'password': node.password, + 'region': node.region + } + @commands.group(name='node', hidden=True) @commands.is_owner() async def _node(self, ctx): @@ -119,11 +129,17 @@ class NodeManager(commands.Cog): @_node.command(name='add') @commands.is_owner() async def _add(self, ctx, host, port, password, region, name=None): + if name in [n.name for n in self.bot.lavalink.node_manager.nodes]: + return await ctx.send("A node with that name already exists") + self.bot.lavalink.add_node(host, port, password, region, name=name) self.logger.debug("Adding Lavalink node: %s on %s with the port %s in %s" % (host, port, region, name,)) embed = await self._node_presenter(ctx, {'host': host, 'port': port, 'password': password, 'region': region, 'name': name}) embed.title = 'Added new node!' + + self.settings.set('lavalink', 'nodes', [self.get_node_properties(n) for + n in self.bot.lavalink.node_manager.nodes]) await ctx.send(embed=embed) @_node.command(name='list') @@ -149,6 +165,10 @@ class NodeManager(commands.Cog): await ctx.send(embed=embed) self.bot.lavalink.node_manager.remove_node(_node) self.logger.info("Removed Lavalink node: %s" % _node.host) + + self.settings.set('lavalink', 'nodes', [self.get_node_properties(n) for + n in self.bot.lavalink.node_manager.nodes]) + if not sent_feedback: await ctx.send('No node found') @@ -157,6 +177,10 @@ class NodeManager(commands.Cog): async def _nodechange(self, ctx, node=None): player = self.bot.lavalink.player_manager.get(ctx.guild.id) + if not player: + return + + # "Switches" to the current node if node is None: await player.change_node(player.node) diff --git a/cogs/utils/settingsmanager.py b/cogs/utils/settingsmanager.py index f361446..f976efa 100644 --- a/cogs/utils/settingsmanager.py +++ b/cogs/utils/settingsmanager.py @@ -1,3 +1,6 @@ +# Discord Packages +import discord + import codecs import locale as localee import os @@ -60,34 +63,39 @@ class Settings: except KeyError: return None - def set(self, guild, setting, value): + def set(self, identifier, setting, value): """ Set value in settings, will overwrite any existing values. """ - guild_id = str(guild.id) + guild_name = None + if isinstance(identifier, discord.Guild): + guild_name = identifier.name + identifier = str(identifier.id) - if guild_id not in self.settings.keys(): - self.settings[guild_id] = {} + if identifier not in self.settings.keys(): + self.settings[identifier] = {} - self.settings[guild_id]["_servername"] = guild.name - self._set(self.settings[guild_id], setting.split('.'), value) + if guild_name: + self.settings[identifier]["_servername"] = guild_name + self._set(self.settings[identifier], setting.split('.'), value) with codecs.open(self._SETTINGS_PATH, 'w', encoding='utf8') as f: yaml.dump(self.settings, f, indent=2) - def get(self, guild, setting, default=''): + def get(self, identifier, setting, default=''): """ Gets a value from the settings if a default return value is specified - it will return the default if no setting is found. If that default is a - class attribute the value of the attribute will get returned.""" - guild_id = str(guild.id) + it will return the default if no setting is found. If that default is an + attribute of the settings class, the value of the attribute will get returned.""" + if isinstance(identifier, discord.Guild): + identifier = str(identifier.id) if default and isinstance(default, str) and hasattr(self, default): default = getattr(self, default) elif not default: default = None - if guild_id not in self.settings.keys(): + if identifier not in self.settings.keys(): return default - value = self._get(self.settings[guild_id], setting.split('.')) + value = self._get(self.settings[identifier], setting.split('.')) if value is not None: return value else: diff --git a/localization/en_en/music.yaml b/localization/en_en/music.yaml index f7c84f4..33ce782 100644 --- a/localization/en_en/music.yaml +++ b/localization/en_en/music.yaml @@ -81,3 +81,4 @@ errors: need_permission: I need the `CONNECT` and `SPEAK` permissions right_channel: You need to be in the right voice channel my_channel: You need to be in my voicechannel + full_channel: The channel is currently full diff --git a/localization/nb_no/music.yaml b/localization/nb_no/music.yaml index d91492f..2dce461 100644 --- a/localization/nb_no/music.yaml +++ b/localization/nb_no/music.yaml @@ -81,3 +81,4 @@ errors: need_permission: Jeg trenger tillgang til `CONNECT` og `SPEAK` right_channel: Du må være i riktig kanal my_channel: Du må være i samme stemmekanal som meg + full_channel: Den kanalen er full From d858bb4175d5d52a8c5736bb9564e305105a46ce Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Sat, 12 Sep 2020 21:20:51 +0200 Subject: [PATCH 12/15] bugfix fixed default arg for settingsmanager. --- cogs/utils/settingsmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utils/settingsmanager.py b/cogs/utils/settingsmanager.py index f976efa..2b6b9f5 100644 --- a/cogs/utils/settingsmanager.py +++ b/cogs/utils/settingsmanager.py @@ -89,7 +89,7 @@ class Settings: if default and isinstance(default, str) and hasattr(self, default): default = getattr(self, default) - elif not default: + elif default == '': default = None if identifier not in self.settings.keys(): From 7a32e048206a59167bf06e46377e5a034b462a7c Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Mon, 14 Sep 2020 14:20:46 +0200 Subject: [PATCH 13/15] Add queue duration, simplify enqueue and play Remove redundant code in enqueue and the play command. Add queue duration to queue command. --- cogs/music/__init__.py | 62 ++++++++++++++------------------ cogs/music/basic_commands.py | 67 ++++++++++------------------------- cogs/utils/mixplayer.py | 18 +++++++--- cogs/utils/paginator.py | 15 ++++---- localization/en_en/music.yaml | 4 +-- localization/nb_no/music.yaml | 4 +-- 6 files changed, 70 insertions(+), 100 deletions(-) diff --git a/cogs/music/__init__.py b/cogs/music/__init__.py index e72d0f3..6f961cd 100644 --- a/cogs/music/__init__.py +++ b/cogs/music/__init__.py @@ -35,36 +35,29 @@ class Music(commands.Cog): 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 + async def enqueue(self, ctx, track, embed, silent=False): 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 + # Only add tracks that don't exceed the max track length + if maxlength := self.max_track_length(ctx.guild, player): + if 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 embed, False - # 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) + # Add thumbnail, turn track into track class + 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) + # Add to player 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) + if player.current is not None and not silent: + until_play = player.queue_duration(include_current=True, end_pos=pos_global) 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.add_field(name="{enqueue.playing_in}", value=f"`{until_play} ({{enqueue.estimated}})`", + inline=True) embed.title = '{enqueue.enqueued}' thumbnail_url = track.extra["thumbnail_url"] @@ -75,22 +68,19 @@ class Music(commands.Cog): duration = timeformatter.format_ms(int(track.duration)) embed.description = f'[{track.title}]({track.uri})\n**{duration}**' - return embed + return embed, True 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: + if maxlength := self.bot.settings.get(guild, 'duration.max', None): + is_dynamic = self.bot.settings.get(guild, 'duration.is_dynamic', 'default_duration_type') + listeners = max(1, len(player.listeners)) # Avoid division by 0 + + if maxlength > 10 and is_dynamic: + return max(maxlength*60*1000/listeners, 60*10*1000) + else: + return maxlength*60*1000 + else: 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 ( diff --git a/cogs/music/basic_commands.py b/cogs/music/basic_commands.py index c3612d7..78272c2 100644 --- a/cogs/music/basic_commands.py +++ b/cogs/music/basic_commands.py @@ -4,14 +4,13 @@ Commands that interact with just discord and lavalink and only present the basic # Discord Packages import discord -import lavalink from discord.ext import commands import asyncio import math import re -from ..utils import checks, thumbnailer, timeformatter +from ..utils import checks, timeformatter from ..utils.paginator import QueuePaginator, Scroller from ..utils.selector import Selector from .decorators import require_playing, require_queue, require_voice_connection @@ -24,7 +23,6 @@ url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') @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('<>') @@ -39,34 +37,20 @@ async def _play(self, ctx, *, query: str): 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) + numtracks = 0 + for track in results['tracks']: + _, track_added = await self.enqueue(ctx, track, embed, silent=True) + if track_added: + numtracks += 1 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) + + embed = ctx.localizer.format_embed(embed) + await ctx.send(embed=embed) if not player.is_playing: await player.play() @@ -192,16 +176,7 @@ async def _now(self, ctx): 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) - + pagified_queue = QueuePaginator(ctx.localizer, player, color=ctx.me.color, member=member) scroller = Scroller(ctx, pagified_queue) await scroller.start_scrolling() @@ -210,12 +185,7 @@ async def _queue(self, ctx, *, member: discord.Member = None): @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() + await self._queue(ctx, member=ctx.author) @commands.command(name='pause') @@ -384,20 +354,19 @@ async def _search(self, ctx, *, query): 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}`') + track = track['info'] + duration = timeformatter.format_ms(int(track['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() + message, current_page, (embed, added) = await search_selector.start_scrolling() - result = ctx.localizer.format_embed(result) - await message.edit(embed=result) + embed = ctx.localizer.format_embed(embed) + await message.edit(embed=embed) if not player.is_playing: await player.play() @@ -439,7 +408,7 @@ async def _reconnect(self, ctx): if track: await player.play(track, start_time=start_time) else: - recon() + await recon() @commands.command(name='volume') diff --git a/cogs/utils/mixplayer.py b/cogs/utils/mixplayer.py index 116f0cd..514e9f7 100644 --- a/cogs/utils/mixplayer.py +++ b/cogs/utils/mixplayer.py @@ -1,4 +1,5 @@ # Discord Packages +import discord import lavalink from lavalink import AudioTrack, DefaultPlayer, Node from lavalink.events import QueueEndEvent, TrackEndEvent, TrackExceptionEvent, TrackStartEvent, TrackStuckEvent @@ -55,14 +56,21 @@ class MixPlayer(DefaultPlayer): def get_history(self): return self.queue.history - def queue_duration(self, include_current: bool = True): + def queue_duration(self, include_current: bool = False, member: discord.Member = None, end_pos: int = None): duration = 0 - for track in self.queue: - duration += track.duration + queue = self.user_queue(member.id) if member else self.queue + + for i, track in enumerate(queue): + if i == end_pos: + break + duration += int(track.duration) + remaining = self.current.duration - self.position if include_current: - return lavalink.Utils.format_time(duration + remaining) - return lavalink.Utils.format_time(duration) + if self.current: + remaining = self.current.duration - self.position + return lavalink.utils.format_time(duration + remaining) + return lavalink.utils.format_time(duration) async def play(self, track: AudioTrack = None, start_time: int = 0): diff --git a/cogs/utils/paginator.py b/cogs/utils/paginator.py index 70a5bb3..9872093 100644 --- a/cogs/utils/paginator.py +++ b/cogs/utils/paginator.py @@ -110,19 +110,22 @@ class FieldPaginator(BasePaginator): class QueuePaginator(TextPaginator): - def __init__(self, localizer, queue, color, user_name: str = None): + def __init__(self, localizer, player, color, member: discord.Member = None, include_current: bool = False): self.localizer = localizer - self.queue = queue - if user_name is None: - title = localizer.format_str("{queue.length}", _length=len(queue)) + queue = player.global_queue() if not member else player.user_queue(member.id, dual=True) + duration = player.queue_duration(member=member, include_current=include_current) + + if member: + title = localizer.format_str("{queue.userqueue}", _user=member.display_name, _length=len(queue), + _duration=duration) else: - title = localizer.format_str("{queue.userqueue}", _user=user_name, _length=len(queue)) + title = localizer.format_str("{queue.length}", _length=len(queue), _duration=duration) super().__init__(max_lines=10, **{"color": color, "title": title}) for index, temp in enumerate(queue): - if user_name is None: + if member is None: track = temp queued_track = localizer.format_str("{queue.globaltrack}", _index=index+1, _title=track.title, _uri=track.uri, _user_id=track.requester) diff --git a/localization/en_en/music.yaml b/localization/en_en/music.yaml index 33ce782..5c5433a 100644 --- a/localization/en_en/music.yaml +++ b/localization/en_en/music.yaml @@ -21,8 +21,8 @@ now: Now Playing queue: empty: The queue is empty user_empty: "{_user}'s queue is empty" - length: "**Queue** `{_length} songs`" - userqueue: "**{_user}\\'s queue** `{_length}`" + length: "**Queue** `{_length}` songs `{_duration}`" + userqueue: "**{_user}\\'s queue** `{_length}` songs `{_duration}`" globaltrack: "`{_index}.` **[{_title}]({_uri})** _by <@{_user_id}>_" usertrack: "`{_index}({_globalindex}).` **[{_title}]({_uri})**" pageindicator: Viewing page {_current}/{_total} diff --git a/localization/nb_no/music.yaml b/localization/nb_no/music.yaml index 2dce461..c888385 100644 --- a/localization/nb_no/music.yaml +++ b/localization/nb_no/music.yaml @@ -21,8 +21,8 @@ now: Spiller nå queue: empty: Køen er tom user_empty: "{_user} sin kø er tom" - length: "**Kø** `{_length} sanger`" - userqueue: "**{_user} sin kø** `{_length}` sanger" + length: "**Kø** `{_length}` sanger `{_duration}`" + userqueue: "**{_user} sin kø** `{_length}` sanger `{_duration}`" globaltrack: "`{_index}.` **[{_title}]({_uri})** _av <@{_user_id}>_" usertrack: "`{_index}({_globalindex}).` **[{_title}]({_uri})**" pageindicator: Viser side {_current}/{_total} From 28208da262a9f93822b3ebca5d421dbc7db8fe64 Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Mon, 14 Sep 2020 17:05:07 +0200 Subject: [PATCH 14/15] Added votable decorator, reduced code duplication Votable decorator added, makes it possible to make a command accessible through voting. Can be overwridden. --- cogs/music/__init__.py | 26 +++++++++++++ cogs/music/advanced_commands.py | 0 cogs/music/basic_commands.py | 69 +++++++-------------------------- cogs/music/decorators.py | 32 +++++++++++++++ cogs/utils/mixplayer.py | 28 ++++++++++--- 5 files changed, 95 insertions(+), 60 deletions(-) delete mode 100644 cogs/music/advanced_commands.py diff --git a/cogs/music/__init__.py b/cogs/music/__init__.py index 6f961cd..492ebc9 100644 --- a/cogs/music/__init__.py +++ b/cogs/music/__init__.py @@ -1,4 +1,5 @@ # Discord Packages +import discord import lavalink from discord.ext import commands @@ -70,6 +71,31 @@ class Music(commands.Cog): return embed, True + def get_current_song_embed(self, ctx, include_time=False): + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + + if include_time: + 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})' + else: + 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) + return embed + def max_track_length(self, guild, player): if maxlength := self.bot.settings.get(guild, 'duration.max', None): is_dynamic = self.bot.settings.get(guild, 'duration.is_dynamic', 'default_duration_type') diff --git a/cogs/music/advanced_commands.py b/cogs/music/advanced_commands.py deleted file mode 100644 index e69de29..0000000 diff --git a/cogs/music/basic_commands.py b/cogs/music/basic_commands.py index 78272c2..f433166 100644 --- a/cogs/music/basic_commands.py +++ b/cogs/music/basic_commands.py @@ -7,13 +7,12 @@ import discord from discord.ext import commands import asyncio -import math import re from ..utils import checks, timeformatter from ..utils.paginator import QueuePaginator, Scroller from ..utils.selector import Selector -from .decorators import require_playing, require_queue, require_voice_connection +from .decorators import require_playing, require_queue, require_voice_connection, voteable time_rx = re.compile('[0-9]+') url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') @@ -78,38 +77,17 @@ async def _seek(self, ctx, *, time: str): @commands.command(name='skip') @require_voice_connection() @require_playing() +@voteable(requester_override=True) 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}")) + await player.skip() + if player.current: + embed = self.get_current_song_embed(ctx) + await ctx.send(ctx.localizer.format_str("{skip.skipped}"), embed=embed) 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) + await ctx.send(ctx.localizer.format_str("{skip.skipped}")) @commands.command(name='skipto') @@ -132,9 +110,9 @@ async def _skip_to(self, ctx, pos: int = 1): @commands.command(name='stop') -@checks.dj_or(alone=True) @require_voice_connection() @require_playing(require_user_listening=True) +@voteable(DJ_override=True) async def _stop(self, ctx): """ Stops the player and clears its queue. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) @@ -148,25 +126,7 @@ async def _stop(self, ctx): @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 = self.get_current_song_embed(ctx, include_time=True) embed = ctx.localizer.format_embed(embed) await ctx.send(embed=embed) @@ -195,12 +155,11 @@ async def _myqueue(self, ctx): async def _pause(self, ctx): """ Pauses/Resumes the current track. """ player = self.bot.lavalink.player_manager.get(ctx.guild.id) + await player.set_pause(not player.paused) 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}")) + else: + await ctx.send(ctx.localizer.format_str("{resume.resumed}")) @commands.command(name='shuffle') @@ -373,8 +332,8 @@ async def _search(self, ctx, *, query): @commands.command(name='disconnect') -@checks.dj_or(alone=True) @require_voice_connection() +@voteable(DJ_override=True) 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) @@ -388,8 +347,8 @@ async def _disconnect(self, ctx): @commands.command(name='reconnect') -@checks.dj_or(alone=True) @require_voice_connection() +@voteable(DJ_override=True) 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) diff --git a/cogs/music/decorators.py b/cogs/music/decorators.py index 4ab36da..997ba4b 100644 --- a/cogs/music/decorators.py +++ b/cogs/music/decorators.py @@ -4,9 +4,12 @@ from discord.ext import commands import functools import inspect +import math from cogs.music.music_errors import WrongVoiceChannelError +from ..utils.checks import is_dj + def require_voice_connection(should_connect=False): """ @@ -122,3 +125,32 @@ def require_queue(require_member_queue=False, require_author_queue=False): await func(self, ctx, *command_args, **kwargs) return ensure_queue_inner return ensure_queue + + +def voteable(requester_override=False, DJ_override=False): + def make_voteable(func): + @functools.wraps(func) + async def voteable_inner(self, ctx, *command_args, **kwargs): + player = self.bot.lavalink.player_manager.get(ctx.guild.id) + player.add_vote(func.__name__, ctx.author) + + total = len(player.listeners) + votes = len(player.get_voters(func.__name__)) + threshold = self.bot.settings.get(ctx.guild, 'vote_threshold', 'default_threshold') + + enough_votes = votes/total >= threshold/100 + DJ = DJ_override and is_dj(ctx) + requester = player.current.requester == ctx.author.id and requester_override + + if enough_votes or requester or DJ: + await func(self, ctx, *command_args, **kwargs) + player.clear_votes() + else: + if votes != 0: + needed = math.ceil(total*threshold/100) + # TODO: redo this message to allow for other vote types + msg = ctx.localizer.format_str("{skip.require_vote}", _skips=votes, _total=needed) + await ctx.send(msg) + + return voteable_inner + return make_voteable diff --git a/cogs/utils/mixplayer.py b/cogs/utils/mixplayer.py index 514e9f7..108c795 100644 --- a/cogs/utils/mixplayer.py +++ b/cogs/utils/mixplayer.py @@ -17,6 +17,7 @@ class MixPlayer(DefaultPlayer): self.queue = MixQueue() self.listeners = set() + self.voteables = {} self.skip_voters = set() self.boosted = False @@ -98,7 +99,7 @@ class MixPlayer(DefaultPlayer): """ Plays the next track in the queue, if any. """ for i in range(pos): _ = self.queue.pop_first() - self.skip_voters.clear() + self.clear_votes() await self.play() def update_listeners(self, member, voice_state): @@ -106,26 +107,43 @@ class MixPlayer(DefaultPlayer): vc = int(self.channel_id) if voice_state.channel is None or voice_state.channel.id != vc: self.listeners.discard(member) - self.skip_voters.discard(member) + self.remove_member_votes(member) else: if voice_state.deaf or voice_state.self_deaf: self.listeners.discard(member) - self.skip_voters.discard(member) + self.remove_member_votes(member) else: self.listeners.add(member) def clear_listeners(self): self.listeners.clear() - def add_skipper(self, member): + def add_vote(self, category, member): if member in self.listeners: - self.skip_voters.add(member) + if category not in self.voteables: + self.voteables[category] = set() + self.voteables[category].add(member) + + def remove_member_votes(self, member): + for category, votes in self.voteables.items(): + votes.discard(member) + + def get_voters(self, category): + if category not in self.voteables: + self.voteables[category] = set() + return self.voteables[category] + + def clear_votes(self): + for category, votes in self.voteables.items(): + votes.clear() async def handle_event(self, event): """ Handles the given event as necessary. """ if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \ isinstance(event, TrackEndEvent) and event.reason == 'FINISHED': self.skip_voters.clear() + for category, votes in self.voteables.items(): + votes.clear() await self.play() async def bassboost(self, boost: bool = False): From 1a9e16d66ff2c18d8def295f94978f6f6070bf1c Mon Sep 17 00:00:00 2001 From: Ev-1 <33662061+Ev-1@users.noreply.github.com> Date: Thu, 17 Sep 2020 14:27:27 +0200 Subject: [PATCH 15/15] Quick bugfix and version fix. --- cogs/music/extra_commands.py | 2 +- cogs/utils/bot_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/music/extra_commands.py b/cogs/music/extra_commands.py index 779540c..5bef7be 100644 --- a/cogs/music/extra_commands.py +++ b/cogs/music/extra_commands.py @@ -40,7 +40,7 @@ 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: + if boost is not None: await player.bassboost(boost) embed = discord.Embed(color=ctx.me.color) diff --git a/cogs/utils/bot_version.py b/cogs/utils/bot_version.py index d3baae9..320cd57 100644 --- a/cogs/utils/bot_version.py +++ b/cogs/utils/bot_version.py @@ -1 +1 @@ -bot_version = "0.1.3" +bot_version = "0.1.5"