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] 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):