From b69bd5426915ff257d8d4bba07b68d2ccf94ee44 Mon Sep 17 00:00:00 2001 From: Evanroby <107794516+Evanroby@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:56:50 +0200 Subject: [PATCH 1/5] [Audio]: Add button support for `[p]now` --- redbot/cogs/audio/core/commands/controller.py | 29 +++-- .../audio/core/utilities/menus/nowmenu.py | 104 ++++++++++++++++++ .../audio/core/utilities/miscellaneous.py | 3 +- 3 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 redbot/cogs/audio/core/utilities/menus/nowmenu.py diff --git a/redbot/cogs/audio/core/commands/controller.py b/redbot/cogs/audio/core/commands/controller.py index 8cc9714697b..19bbdac7d7b 100644 --- a/redbot/cogs/audio/core/commands/controller.py +++ b/redbot/cogs/audio/core/commands/controller.py @@ -19,6 +19,7 @@ from ..abc import MixinMeta from ..cog_utils import CompositeMetaClass +from ..utilities.menus.nowmenu import NowPlayingView log = getLogger("red.cogs.Audio.cog.Commands.player_controller") _ = Translator("Audio", Path(__file__)) @@ -82,7 +83,6 @@ async def command_disconnect(self, ctx: commands.Context): @commands.command(name="now") @commands.guild_only() @commands.bot_has_permissions(embed_links=True) - @commands.bot_can_react() async def command_now(self, ctx: commands.Context): """Now playing.""" if not self._player_check(ctx): @@ -143,22 +143,29 @@ async def command_now(self, ctx: commands.Context): + ("\N{WHITE HEAVY CHECK MARK}" if repeat else "\N{CROSS MARK}") ) - message = await self.send_embed_msg(ctx, embed=embed, footer=text) - - player.store("np_message", message) - dj_enabled = self._dj_status_cache.setdefault( ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled() ) vote_enabled = await self.config.guild(ctx.guild).vote_enabled() - if ( - (dj_enabled or vote_enabled) - and not await self._can_instaskip(ctx, ctx.author) - and not await self.is_requester_alone(ctx) - ): + can_control = await self._can_instaskip(ctx, ctx.author) or await self.is_requester_alone( + ctx + ) + has_queue = bool(player.queue) or autoplay + if await ctx.bot._config.use_buttons(): + if (dj_enabled or vote_enabled) and not can_control: + message = await self.send_embed_msg(ctx, embed=embed, footer=text) + else: + view = NowPlayingView(ctx=ctx, cog=self, has_queue=has_queue) + message = await self.send_embed_msg(ctx, embed=embed, footer=text, view=view) + view.message = message + player.store("np_message", message) return + message = await self.send_embed_msg(ctx, embed=embed, footer=text) + player.store("np_message", message) - if not player.queue and not autoplay: + if (dj_enabled or vote_enabled) and not can_control: + return + if not has_queue: expected = (emoji["stop"], emoji["pause"], emoji["close"]) task: Optional[asyncio.Task] if player.current: diff --git a/redbot/cogs/audio/core/utilities/menus/nowmenu.py b/redbot/cogs/audio/core/utilities/menus/nowmenu.py new file mode 100644 index 00000000000..aedc27219a3 --- /dev/null +++ b/redbot/cogs/audio/core/utilities/menus/nowmenu.py @@ -0,0 +1,104 @@ +import contextlib +from pathlib import Path + +import discord + +from redbot.core import commands +from redbot.core.i18n import Translator + +_ = Translator("Audio", Path(__file__)) + + +class NowPlayingView(discord.ui.View): + """Button-based controls for the [p]now command. + + Only shown when [p]set usebuttons is enabled. + When there is no queue and autoplay is off, prev and next are hidden. + """ + + def __init__(self, ctx: commands.Context, cog, has_queue: bool): + super().__init__(timeout=30.0) + self.ctx = ctx + self.cog = cog + self.message: discord.Message = None + + if not has_queue: + self.remove_item(self.button_prev) + self.remove_item(self.button_next) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.ctx.author: + await interaction.response.send_message( + _("You are not the one who requested this player."), ephemeral=True + ) + return False + return True + + async def on_timeout(self) -> None: + if self.message: + with contextlib.suppress(discord.HTTPException): + await self.message.edit(view=None) + + @discord.ui.button( + emoji="\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", + style=discord.ButtonStyle.grey, + ) + async def button_prev( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + await interaction.response.defer() + self.stop() + with contextlib.suppress(discord.HTTPException): + await self.message.edit(view=None) + await self.ctx.invoke(self.cog.command_prev) + + @discord.ui.button( + emoji="\N{BLACK SQUARE FOR STOP}\N{VARIATION SELECTOR-16}", + style=discord.ButtonStyle.grey, + ) + async def button_stop( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + await interaction.response.defer() + self.stop() + with contextlib.suppress(discord.HTTPException): + await self.message.edit(view=None) + await self.ctx.invoke(self.cog.command_stop) + + @discord.ui.button( + emoji="\N{BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR}\N{VARIATION SELECTOR-16}", + style=discord.ButtonStyle.grey, + ) + async def button_pause( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + await interaction.response.defer() + self.stop() + with contextlib.suppress(discord.HTTPException): + await self.message.edit(view=None) + await self.ctx.invoke(self.cog.command_pause) + + @discord.ui.button( + emoji="\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", + style=discord.ButtonStyle.grey, + ) + async def button_next( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + await interaction.response.defer() + self.stop() + with contextlib.suppress(discord.HTTPException): + await self.message.edit(view=None) + await self.ctx.invoke(self.cog.command_skip) + + @discord.ui.button( + emoji="\N{CROSS MARK}", + style=discord.ButtonStyle.grey, + ) + async def button_close( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + await interaction.response.defer() + self.stop() + with contextlib.suppress(discord.HTTPException): + await self.message.delete() diff --git a/redbot/cogs/audio/core/utilities/miscellaneous.py b/redbot/cogs/audio/core/utilities/miscellaneous.py index 4e3edce3011..7378b563246 100644 --- a/redbot/cogs/audio/core/utilities/miscellaneous.py +++ b/redbot/cogs/audio/core/utilities/miscellaneous.py @@ -71,6 +71,7 @@ async def send_embed_msg( timestamp = kwargs.get("timestamp") footer = kwargs.get("footer") thumbnail = kwargs.get("thumbnail") + view = kwargs.get("view") contents = dict(title=title, type=_type, url=url, description=description) if hasattr(kwargs.get("embed"), "to_dict"): embed = kwargs.get("embed") @@ -97,7 +98,7 @@ async def send_embed_msg( embed.set_author(name=name, icon_url=url) elif name: embed.set_author(name=name) - return await ctx.send(embed=embed) + return await ctx.send(embed=embed, view=view) def _has_notify_perms( self, From 82096fd3de460db5d3ada3c0e15106c7920c7a0b Mon Sep 17 00:00:00 2001 From: Evanroby <107794516+Evanroby@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:02:09 +0200 Subject: [PATCH 2/5] black --- .../audio/core/utilities/menus/nowmenu.py | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/redbot/cogs/audio/core/utilities/menus/nowmenu.py b/redbot/cogs/audio/core/utilities/menus/nowmenu.py index aedc27219a3..ac84ecfe96a 100644 --- a/redbot/cogs/audio/core/utilities/menus/nowmenu.py +++ b/redbot/cogs/audio/core/utilities/menus/nowmenu.py @@ -43,9 +43,7 @@ async def on_timeout(self) -> None: emoji="\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", style=discord.ButtonStyle.grey, ) - async def button_prev( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def button_prev(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.defer() self.stop() with contextlib.suppress(discord.HTTPException): @@ -56,9 +54,7 @@ async def button_prev( emoji="\N{BLACK SQUARE FOR STOP}\N{VARIATION SELECTOR-16}", style=discord.ButtonStyle.grey, ) - async def button_stop( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def button_stop(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.defer() self.stop() with contextlib.suppress(discord.HTTPException): @@ -69,9 +65,7 @@ async def button_stop( emoji="\N{BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR}\N{VARIATION SELECTOR-16}", style=discord.ButtonStyle.grey, ) - async def button_pause( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def button_pause(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.defer() self.stop() with contextlib.suppress(discord.HTTPException): @@ -82,9 +76,7 @@ async def button_pause( emoji="\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", style=discord.ButtonStyle.grey, ) - async def button_next( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def button_next(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.defer() self.stop() with contextlib.suppress(discord.HTTPException): @@ -95,9 +87,7 @@ async def button_next( emoji="\N{CROSS MARK}", style=discord.ButtonStyle.grey, ) - async def button_close( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def button_close(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.defer() self.stop() with contextlib.suppress(discord.HTTPException): From 8b2a2d0396805d3a10acf11e753e25a5905ff4c0 Mon Sep 17 00:00:00 2001 From: Evanroby Date: Thu, 28 May 2026 18:17:27 +0200 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Jakub Kuczys --- redbot/cogs/audio/core/commands/controller.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/redbot/cogs/audio/core/commands/controller.py b/redbot/cogs/audio/core/commands/controller.py index 19bbdac7d7b..bdf39136fe9 100644 --- a/redbot/cogs/audio/core/commands/controller.py +++ b/redbot/cogs/audio/core/commands/controller.py @@ -147,11 +147,13 @@ async def command_now(self, ctx: commands.Context): ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled() ) vote_enabled = await self.config.guild(ctx.guild).vote_enabled() - can_control = await self._can_instaskip(ctx, ctx.author) or await self.is_requester_alone( - ctx + can_control = ( + not (dj_enabled or vote_enabled) + or await self._can_instaskip(ctx, ctx.author) + or await self.is_requester_alone(ctx) ) has_queue = bool(player.queue) or autoplay - if await ctx.bot._config.use_buttons(): + if await ctx.bot.use_buttons(): if (dj_enabled or vote_enabled) and not can_control: message = await self.send_embed_msg(ctx, embed=embed, footer=text) else: From 06a14053c9c706b463e5b994d309260bf8010fb9 Mon Sep 17 00:00:00 2001 From: Evanroby <107794516+Evanroby@users.noreply.github.com> Date: Thu, 28 May 2026 21:16:25 +0200 Subject: [PATCH 4/5] rename file --- redbot/cogs/audio/core/commands/controller.py | 2 +- .../audio/core/utilities/menus/{nowmenu.py => now_playing.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename redbot/cogs/audio/core/utilities/menus/{nowmenu.py => now_playing.py} (100%) diff --git a/redbot/cogs/audio/core/commands/controller.py b/redbot/cogs/audio/core/commands/controller.py index 19bbdac7d7b..29d8a282741 100644 --- a/redbot/cogs/audio/core/commands/controller.py +++ b/redbot/cogs/audio/core/commands/controller.py @@ -19,7 +19,7 @@ from ..abc import MixinMeta from ..cog_utils import CompositeMetaClass -from ..utilities.menus.nowmenu import NowPlayingView +from ..utilities.menus.now_playing import NowPlayingView log = getLogger("red.cogs.Audio.cog.Commands.player_controller") _ = Translator("Audio", Path(__file__)) diff --git a/redbot/cogs/audio/core/utilities/menus/nowmenu.py b/redbot/cogs/audio/core/utilities/menus/now_playing.py similarity index 100% rename from redbot/cogs/audio/core/utilities/menus/nowmenu.py rename to redbot/cogs/audio/core/utilities/menus/now_playing.py From 4ea9435b8bd86697f92306d1f313c440b3e41ea8 Mon Sep 17 00:00:00 2001 From: Evanroby <107794516+Evanroby@users.noreply.github.com> Date: Thu, 28 May 2026 21:23:53 +0200 Subject: [PATCH 5/5] add check for reactions --- redbot/cogs/audio/core/commands/controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/redbot/cogs/audio/core/commands/controller.py b/redbot/cogs/audio/core/commands/controller.py index 42b6c449d7f..bdc8434dd70 100644 --- a/redbot/cogs/audio/core/commands/controller.py +++ b/redbot/cogs/audio/core/commands/controller.py @@ -162,6 +162,8 @@ async def command_now(self, ctx: commands.Context): view.message = message player.store("np_message", message) return + if not ctx.channel.permissions_for(ctx.me).add_reactions: + raise commands.BotMissingPermissions(["add_reactions"]) message = await self.send_embed_msg(ctx, embed=embed, footer=text) player.store("np_message", message)