From 12f7c96c932fa288c78d4a7322adf8800a1d397e Mon Sep 17 00:00:00 2001 From: Lemon4ksan Date: Fri, 21 Feb 2025 17:03:39 +0300 Subject: [PATCH] impr: Enforce menu view, vote improvement and bug fixes. --- MusicBot/cogs/general.py | 29 +-- MusicBot/cogs/settings.py | 115 +++------ MusicBot/cogs/utils/voice_extension.py | 211 +++++++++++++---- MusicBot/cogs/voice.py | 310 ++++--------------------- MusicBot/database/base.py | 9 +- MusicBot/database/extensions.py | 17 +- MusicBot/database/guild.py | 22 +- MusicBot/ui/find.py | 68 +++--- MusicBot/ui/menu.py | 181 ++++++++++++--- 9 files changed, 457 insertions(+), 505 deletions(-) diff --git a/MusicBot/cogs/general.py b/MusicBot/cogs/general.py index c97c6fa..e27b1ab 100644 --- a/MusicBot/cogs/general.py +++ b/MusicBot/cogs/general.py @@ -41,13 +41,13 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]: logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {uid}") - if content_type == 'Трек' and search.tracks: + if content_type == 'Трек' and search.tracks is not None: res = [f"{item.title} {f"({item.version})" if item.version else ''} - {", ".join(item.artists_name())}" for item in search.tracks.results] - elif content_type == 'Альбом' and search.albums: + elif content_type == 'Альбом' and search.albums is not None: res = [f"{item.title} - {", ".join(item.artists_name())}" for item in search.albums.results] - elif content_type == 'Артист' and search.artists: + elif content_type == 'Артист' and search.artists is not None: res = [f"{item.name}" for item in search.artists.results] - elif content_type == 'Плейлист' and search.playlists: + elif content_type == 'Плейлист' and search.playlists is not None: res = [f"{item.title}" for item in search.playlists.results] else: logging.warning(f"[GENERAL] Invalid content type '{content_type}' for user {uid}") @@ -108,7 +108,7 @@ class General(Cog): "Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n" "Для получения помощи по конкретной команде, введите /help <команда>.\n" "Для изменения настроек необходимо иметь права управления каналами на сервере.\n\n" - "**Для дополнительной помощи, присоединяйтесь к [серверу сообщества](https://discord.gg/gkmFDaPMeC).**" + "**Присоединяйтесь к нашему [серверу сообщества](https://discord.gg/TgnW8nfbFn)!**" ) embed.add_field( name='__Основные команды__', @@ -117,7 +117,6 @@ class General(Cog): `help` `queue` `settings` - `track` `voice`""" ) embed.set_footer(text='©️ Bananchiki') @@ -149,30 +148,19 @@ class General(Cog): embed.description += ( "`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки.\n\n" "Получить текущие настройки.\n```/settings show```\n" - "Разрешить или запретить создание меню проигрывателя, когда в канале больше одного человека.\n```/settings menu```\n" - "Разрешить или запретить голосование.\n```/settings vote <тип>```\n" - "Разрешить или запретить отключение/подключение бота к каналу участникам без прав управления каналом.\n```/settings connect```\n" - ) - elif command == 'track': - embed.description += ( - "`Примечание`: Если вы один в голосовом канале или имеете разрешение управления каналом, голосование не начинается.\n\n" - "Переключиться на следующий трек в очереди. \n```/track next```\n" - "Приостановить текущий трек.\n```/track pause```\n" - "Возобновить текущий трек.\n```/track resume```\n" - "Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```\n" - "Добавить трек в плейлист «Мне нравится» или удалить его, если он уже там.\n```/track like```" - "Запустить Мою Волну по текущему треку.\n```/track vibe```" + "Переключить параметр настроек.\n```/settings toggle <параметр>```\n" ) elif command == 'voice': embed.description += ( "`Примечание`: Доступность меню и Моей Волны зависит от настроек сервера.\n\n" "Присоединить бота в голосовой канал.\n```/voice join```\n" "Заставить бота покинуть голосовой канал.\n ```/voice leave```\n" + "Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```\n" "Создать меню проигрывателя. \n```/voice menu```\n" "Запустить станцию. Без уточнения станции, запускает Мою Волну.\n```/voice vibe <название станции>```" ) else: - await ctx.respond('❌ Неизвестная команда.') + await ctx.respond('❌ Неизвестная команда.', delete_after=15, ephemeral=True) return await ctx.respond(embed=embed, ephemeral=True) @@ -236,6 +224,7 @@ class General(Cog): await ctx.respond('❌ У вас нет треков в плейлисте «Мне нравится».', delete_after=15, ephemeral=True) return + await ctx.defer() # Sometimes it takes a while to fetch all tracks, so we defer the response real_tracks = await gather(*[track_short.fetch_track_async() for track_short in likes.tracks], return_exceptions=True) tracks = [track for track in real_tracks if not isinstance(track, BaseException)] # Can't fetch user tracks diff --git a/MusicBot/cogs/settings.py b/MusicBot/cogs/settings.py index 3ef5ae5..a3cdf7a 100644 --- a/MusicBot/cogs/settings.py +++ b/MusicBot/cogs/settings.py @@ -19,100 +19,53 @@ class Settings(Cog): @settings.command(name="show", description="Показать текущие настройки бота.") async def show(self, ctx: discord.ApplicationContext) -> None: - guild = await self.db.get_guild(ctx.guild.id, projection={ - 'always_allow_menu': 1, 'allow_connect': 1, 'allow_disconnect': 1, - 'vote_next_track': 1, 'vote_add_track': 1, 'vote_add_album': 1, 'vote_add_artist': 1, 'vote_add_playlist': 1 - }) + guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1, 'vote_switch_track': 1, 'vote_add': 1}) + + vote = "✅ - Переключение" if guild['vote_switch_track'] else "❌ - Переключение" + vote += "\n✅ - Добавление в очередь" if guild['vote_add'] else "\n❌ - Добавление в очередь" + + connect = "\n✅ - Разрешено всем" if guild['allow_change_connect'] else "\n❌ - Только для участникам с правами управления каналом" + embed = discord.Embed(title="Настройки бота", color=0xfed42b) - - menu = "✅ - Всегда доступно" if guild['always_allow_menu'] else "❌ - Если в канале 1 человек." - - vote = "✅ - Переключение" if guild['vote_next_track'] else "❌ - Переключение" - vote += "\n✅ - Добавление треков" if guild['vote_add_track'] else "\n❌ - Добавление треков" - vote += "\n✅ - Добавление альбомов" if guild['vote_add_album'] else "\n❌ - Добавление альбомов" - vote += "\n✅ - Добавление артистов" if guild['vote_add_artist'] else "\n❌ - Добавление артистов" - vote += "\n✅ - Добавление плейлистов" if guild['vote_add_playlist'] else "\n❌ - Добавление плейлистов" - - connect = "\n✅ - Разрешено всем" if guild['allow_connect'] else "\n❌ - Только для участникам с правами управления каналом" - - embed.add_field(name="__Меню проигрывателя__", value=menu, inline=False) embed.add_field(name="__Голосование__", value=vote, inline=False) - embed.add_field(name="__Подключение/Отключение__", value=connect, inline=False) + embed.add_field(name="__Подключение/Отключение бота__", value=connect, inline=False) await ctx.respond(embed=embed, ephemeral=True) - - @settings.command(name="connect", description="Разрешить/запретить отключение/подключение бота к каналу участникам без прав управления каналом.") - async def connect(self, ctx: discord.ApplicationContext) -> None: - member = cast(discord.Member, ctx.author) - if not member.guild_permissions.manage_channels: - await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True) - return - guild = await self.db.get_guild(ctx.guild.id, projection={'allow_connect': 1}) - await self.db.update(ctx.guild.id, {'allow_connect': not guild['allow_connect']}) - await ctx.respond(f"Отключение/подключение бота к каналу теперь {'✅ разрешено' if not guild['allow_connect'] else '❌ запрещено'} участникам без прав управления каналом.", delete_after=15, ephemeral=True) - - @settings.command(name="menu", description="Разрешить или запретить использование меню проигрывателя, если в канале больше одного человека.") - async def menu(self, ctx: discord.ApplicationContext) -> None: - member = cast(discord.Member, ctx.author) - if not member.guild_permissions.manage_channels: - await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True) - return - - guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1}) - await self.db.update(ctx.guild.id, {'always_allow_menu': not guild['always_allow_menu']}) - await ctx.respond(f"Меню проигрывателя теперь {'✅ доступно' if not guild['always_allow_menu'] else '❌ недоступно'} в каналах с несколькими людьми.", delete_after=15, ephemeral=True) - - @settings.command(name="vote", description="Настроить голосование.") + @settings.command(name="toggle", description="Переключить параметр настроек.") @discord.option( - "vote_type", + "параметр", + parameter_name="vote_type", description="Тип голосования.", type=discord.SlashCommandOptionType.string, - choices=['+Всё', '-Всё', 'Переключение', 'Трек', 'Альбом', 'Плейлист'], - default='+Всё' + choices=['Переключение', 'Добавление в очередь', 'Добавление/Отключение бота'] ) - async def vote(self, ctx: discord.ApplicationContext, vote_type: Literal['+Всё', '-Всё', 'Переключение', 'Трек', 'Альбом', 'Плейлист']) -> None: + async def toggle( + self, + ctx: discord.ApplicationContext, + vote_type: Literal['Переключение', 'Добавление в очередь', 'Добавление/Отключение бота'] + ) -> None: member = cast(discord.Member, ctx.author) if not member.guild_permissions.manage_channels: await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True) return - guild = await self.db.get_guild(ctx.guild.id, projection={'vote_next_track': 1, 'vote_add_track': 1, 'vote_add_album': 1, 'vote_add_artist': 1, 'vote_add_playlist': 1}) - - if vote_type == '-Всё': - await self.db.update(ctx.guild.id, { - 'vote_next_track': False, - 'vote_add_track': False, - 'vote_add_album': False, - 'vote_add_artist': False, - 'vote_add_playlist': False - } - ) - response_message = "Голосование ❌ выключено." - elif vote_type == '+Всё': - await self.db.update(ctx.guild.id, { - 'vote_next_track': True, - 'vote_add_track': True, - 'vote_add_album': True, - 'vote_add_artist': True, - 'vote_add_playlist': True - } - ) - response_message = "Голосование ✅ включено." - elif vote_type == 'Переключение': - await self.db.update(ctx.guild.id, {'vote_next_track': not guild['vote_next_track']}) - response_message = "Голосование за переключение трека " + ("❌ выключено." if guild['vote_next_track'] else "✅ включено.") - elif vote_type == 'Трек': - await self.db.update(ctx.guild.id, {'vote_add_track': not guild['vote_add_track']}) - response_message = "Голосование за добавление трека " + ("❌ выключено." if guild['vote_add_track'] else "✅ включено.") - elif vote_type == 'Альбом': - await self.db.update(ctx.guild.id, {'vote_add_album': not guild['vote_add_album']}) - response_message = "Голосование за добавление альбома " + ("❌ выключено." if guild['vote_add_album'] else "✅ включено.") - elif vote_type == 'Артист': - await self.db.update(ctx.guild.id, {'vote_add_artist': not guild['vote_add_artist']}) - response_message = "Голосование за добавление артиста " + ("❌ выключено." if guild['vote_add_artist'] else "✅ включено.") - elif vote_type == 'Плейлист': - await self.db.update(ctx.guild.id, {'vote_add_playlist': not guild['vote_add_playlist']}) - response_message = "Голосование за добавление плейлиста " + ("❌ выключено." if guild['vote_add_playlist'] else "✅ включено.") + guild = await self.db.get_guild(ctx.guild.id, projection={ + 'vote_switch_track': 1, 'vote_add': 1, 'allow_change_connect': 1}) + + if vote_type == 'Переключение': + await self.db.update(ctx.guild.id, {'vote_switch_track': not guild['vote_switch_track']}) + response_message = "Голосование за переключение трека " + ("❌ выключено." if guild['vote_switch_track'] else "✅ включено.") + + elif vote_type == 'Добавление в очередь': + await self.db.update(ctx.guild.id, {'vote_add': not guild['vote_add']}) + response_message = "Голосование за добавление в очередь " + ("❌ выключено." if guild['vote_add'] else "✅ включено.") + + elif vote_type == 'Добавление/Отключение бота': + await self.db.update(ctx.guild.id, {'allow_change_connect': not guild['allow_change_connect']}) + response_message = f"Добавление/Отключение бота от канала теперь {'✅ разрешено' if not guild['allow_change_connect'] else '❌ запрещено'} участникам без прав управления каналом." + + else: + response_message = "❌ Неизвестный тип голосования." await ctx.respond(response_message, delete_after=15, ephemeral=True) diff --git a/MusicBot/cogs/utils/voice_extension.py b/MusicBot/cogs/utils/voice_extension.py index c799a2f..3f614e2 100644 --- a/MusicBot/cogs/utils/voice_extension.py +++ b/MusicBot/cogs/utils/voice_extension.py @@ -9,10 +9,10 @@ from yandex_music import Track, TrackShort, ClientAsync as YMClient import discord from discord.ui import View -from discord import Interaction, ApplicationContext, RawReactionActionEvent +from discord import Interaction, ApplicationContext, RawReactionActionEvent, VoiceChannel from MusicBot.cogs.utils import generate_item_embed -from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase, ExplicitGuild, ExplicitUser +from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase, ExplicitGuild, ExplicitUser, MessageVotes menu_views: dict[int, View] = {} # Store menu views and delete them when needed to prevent memory leaks for after callbacks. @@ -23,13 +23,16 @@ class VoiceExtension: self.db = VoiceGuildsDatabase() self.users_db = BaseUsersDatabase() - async def send_menu_message(self, ctx: ApplicationContext | Interaction, *, disable: bool = False) -> bool: + async def send_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, *, disable: bool = False) -> bool: """Send menu message to the channel and delete old menu message if exists. Return True if sent. Args: ctx (ApplicationContext | Interaction): Context. disable (bool, optional): Disable menu message. Defaults to False. - + + Raises: + ValueError: If bot instance is not set and ctx is RawReactionActionEvent. + Returns: bool: True if sent, False if not. """ @@ -65,7 +68,23 @@ class VoiceExtension: await message.delete() await self._update_menu_views_dict(ctx, disable=disable) - interaction = await ctx.respond(view=menu_views[ctx.guild_id], embed=embed) + + if isinstance(ctx, (ApplicationContext, Interaction)): + interaction = await ctx.respond(view=menu_views[ctx.guild_id], embed=embed) + else: + if not self.bot: + raise ValueError("Bot instance is not set.") + + channel = cast(VoiceChannel, self.bot.get_channel(ctx.channel_id)) + if not channel: + logging.warning(f"[VC_EXT] Channel {ctx.channel_id} not found in guild {ctx.guild_id}") + return False + + interaction = await channel.send( + view=menu_views[ctx.guild_id], + embed=embed # type: ignore # Wrong typehints. + ) + response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction await self.db.update(ctx.guild_id, {'current_menu': response.id}) @@ -105,18 +124,17 @@ class VoiceExtension: await self.db.update(ctx.guild_id, {'current_menu': None}) return None - if menu: - logging.debug(f"[VC_EXT] Menu message {menu_mid} successfully fetched") - else: + if not menu: logging.debug(f"[VC_EXT] Menu message {menu_mid} not found in guild {ctx.guild_id}") await self.db.update(ctx.guild_id, {'current_menu': None}) + return None + logging.debug(f"[VC_EXT] Menu message {menu_mid} successfully fetched") return menu async def update_menu_full( self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, - menu_mid: int | None = None, *, menu_message: discord.Message | None = None, button_callback: bool = False @@ -132,7 +150,7 @@ class VoiceExtension: Returns: bool: True if updated, False if not. """ - logging.debug( + logging.info( f"[VC_EXT] Updating menu embed using " + ( "interaction context" if isinstance(ctx, Interaction) else "application context" if isinstance(ctx, ApplicationContext) else @@ -147,14 +165,11 @@ class VoiceExtension: logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'update_menu_embed'") return False - guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_track': 1}) - - if not menu_message: - if not menu_mid: - logging.warning("[VC_EXT] No menu message or menu message id provided") - return False - menu_message = await self.get_menu_message(ctx, menu_mid) + guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_menu': 1, 'current_track': 1}) + if not guild['current_menu']: + return False + menu_message = await self.get_menu_message(ctx, guild['current_menu']) if not menu_message else menu_message if not menu_message: return False @@ -168,6 +183,16 @@ class VoiceExtension: )) embed = await generate_item_embed(track, guild['vibing']) + vc = await self.get_voice_client(ctx) + if not vc: + logging.warning("[VC_EXT] Voice client not found") + return False + + if vc.is_paused(): + embed.set_footer(text='Приостановлено') + else: + embed.remove_footer() + await self._update_menu_views_dict(ctx) try: if isinstance(ctx, Interaction) and button_callback: @@ -186,7 +211,6 @@ class VoiceExtension: async def update_menu_view( self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, - guild: ExplicitGuild, *, menu_message: discord.Message | None = None, button_callback: bool = False, @@ -206,6 +230,11 @@ class VoiceExtension: """ logging.debug("[VC_EXT] Updating menu view") + if not ctx.guild_id: + logging.warning("[VC_EXT] Guild ID not found in context inside 'update_menu_view'") + return False + + guild = await self.db.get_guild(ctx.guild_id, projection={'current_menu': 1}) if not guild['current_menu']: return False @@ -217,10 +246,10 @@ class VoiceExtension: try: if isinstance(ctx, Interaction) and button_callback: # If interaction from menu buttons - await ctx.edit(view=menu_views[guild['_id']]) + await ctx.edit(view=menu_views[ctx.guild_id]) else: # If interaction from other buttons or commands. They should have their own response. - await menu_message.edit(view=menu_views[guild['_id']]) + await menu_message.edit(view=menu_views[ctx.guild_id]) except discord.NotFound: logging.warning("[VC_EXT] Menu message not found") return False @@ -340,7 +369,7 @@ class VoiceExtension: if not isinstance(ctx.channel, discord.VoiceChannel): logging.debug("[VC_EXT] User is not in a voice channel") - await ctx.respond("❌ Вы должны отправить команду в голосовом канале.", delete_after=15, ephemeral=True) + await ctx.respond("❌ Вы должны отправить команду в чате голосового канала.", delete_after=15, ephemeral=True) return False if ctx.user.id not in ctx.channel.voice_states: @@ -408,7 +437,7 @@ class VoiceExtension: retry: bool = False ) -> str | None: """Download ``track`` by its id and play it in the voice channel. Return track title on success. - Send feedback for vibe track playing if vibing. Should be called if voice requirements are met. + Send vibe feedback for playing track if vibing. Should be called when voice requirements are met. Args: ctx (ApplicationContext | Interaction): Context. @@ -457,25 +486,31 @@ class VoiceExtension: if menu_message or guild['current_menu']: # Updating menu message before playing to prevent delay and avoid FFMPEG lags. - await self.update_menu_full(ctx, guild['current_menu'], menu_message=menu_message, button_callback=button_callback) + await self.update_menu_full(ctx, menu_message=menu_message, button_callback=button_callback) if not guild['vibing']: # Giving FFMPEG enough time to process the audio file await asyncio.sleep(1) loop = self._get_current_event_loop(ctx) - vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop)) + try: + vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop)) + except discord.errors.ClientException as e: + logging.error(f"[VC_EXT] Error while playing track '{track.title}': {e}") + if not isinstance(ctx, RawReactionActionEvent): + await ctx.respond(f"❌ Не удалось проиграть трек. Попробуйте сбросить меню.", delete_after=15, ephemeral=True) logging.info(f"[VC_EXT] Playing track '{track.title}'") await self.db.update(gid, {'is_stopped': False}) if guild['vibing']: - await self._my_vibe_send_start_feedback(ctx, track, uid) + await self._my_vibe_start_feedback(ctx, track, uid) return track.title async def stop_playing( - self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, + self, + ctx: ApplicationContext | Interaction | RawReactionActionEvent, *, vc: discord.VoiceClient | None = None, full: bool = False @@ -514,7 +549,7 @@ class VoiceExtension: return False if guild['vibing'] and guild['current_track']: - if not await self._my_vibe_send_stop_feedback(ctx, guild, user): + if not await self._my_vibe_stop_feedback(ctx, guild, user): return False return True @@ -571,10 +606,10 @@ class VoiceExtension: await self.db.modify_track(gid, guild['current_track'], 'previous', 'insert') if after and guild['current_menu']: - await self.update_menu_view(ctx, guild, menu_message=menu_message, disable=True) + await self.update_menu_view(ctx, menu_message=menu_message, disable=True) if guild['vibing'] and guild['current_track'] and not isinstance(ctx, discord.RawReactionActionEvent): - if not await self._send_next_vibe_feedback(ctx, guild, user, client, after=after): + if not await self._my_vibe_feedback(ctx, guild, user, client, after=after): await ctx.respond("❌ Что-то пошло не так. Попробуйте снова.", ephemeral=True) return None @@ -598,7 +633,7 @@ class VoiceExtension: next_track = await self.db.get_track(gid, 'next') if next_track: - title = await self._play_next_track(ctx, next_track, client=client, vc=vc, button_callback=button_callback) + title = await self._play_track(ctx, next_track, client=client, vc=vc, button_callback=button_callback) if after and not guild['current_menu']: if isinstance(ctx, discord.RawReactionActionEvent): @@ -618,7 +653,7 @@ class VoiceExtension: return None - async def prev_track(self, ctx: ApplicationContext | Interaction, button_callback: bool = False) -> str | None: + async def previous_track(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, button_callback: bool = False) -> str | None: """Switch to the previous track in the queue. Repeat current track if no previous one found. Return track title on success. @@ -629,12 +664,17 @@ class VoiceExtension: Returns: (str | None): Track title or None. """ - if not ctx.guild or not ctx.user: - logging.warning("Guild or User not found in context inside 'prev_track'") + logging.debug("[VC_EXT] Switching to previous track") + + gid = ctx.guild_id + uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None + + if not gid or not uid: + logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'next_track'") return None - current_track = await self.db.get_track(ctx.guild.id, 'current') - prev_track = await self.db.get_track(ctx.guild.id, 'previous') + current_track = await self.db.get_track(gid, 'current') + prev_track = await self.db.get_track(gid, 'previous') if prev_track: logging.debug("[VC_EXT] Previous track found") @@ -647,7 +687,7 @@ class VoiceExtension: track = None if track: - return await self._play_next_track(ctx, track, button_callback=button_callback) + return await self._play_track(ctx, track, button_callback=button_callback) return None @@ -721,7 +761,8 @@ class VoiceExtension: add_func = client.users_dislikes_tracks_add remove_func = client.users_dislikes_tracks_remove - if not tracks: + if tracks is None: + logging.debug(f"[VC_EXT] No {action}s found") return (False, None) if str(current_track['id']) not in [str(track.id) for track in tracks]: @@ -771,6 +812,82 @@ class VoiceExtension: self._ym_clients[token] = client return client + async def proccess_vote(self, ctx: RawReactionActionEvent, guild: ExplicitGuild, channel: VoiceChannel, vote_data: MessageVotes) -> bool: + """Proccess vote and perform action from `vote_data` and respond. Return True on success. + + Args: + ctx (RawReactionActionEvent): Context. + guild (ExplicitGuild): Guild data. + message (Message): Message. + vote_data (MessageVotes): Vote data. + + Returns: + bool: Success status. + """ + logging.info(f"[VOICE] Performing '{vote_data['action']}' action for message {ctx.message_id}") + + if not guild['current_menu']: + await self.send_menu_message(ctx) + + if vote_data['action'] in ('next', 'previous'): + if not guild.get(f'{vote_data['action']}_tracks'): + await channel.send(content=f"❌ Очередь пуста!", delete_after=15) + + elif not (await self.next_track(ctx) if vote_data['action'] == 'next' else await self.previous_track(ctx)): + await channel.send(content=f"❌ Ошибка при смене трека! Попробуйте ещё раз.", delete_after=15) + + elif vote_data['action'] == 'add_track': + track = vote_data['vote_content'] + if not track: + logging.info(f"[VOICE] Recieved empty vote context for message {ctx.message_id}") + return False + + await self.db.modify_track(guild['_id'], track, 'next', 'append') + + if guild['current_track']: + await channel.send(content=f"✅ Трек был добавлен в очередь!", delete_after=15) + else: + if not await self.next_track(ctx): + await channel.send(content=f"❌ Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15) + + elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'): + tracks = vote_data['vote_content'] + if not tracks: + logging.info(f"[VOICE] Recieved empty vote context for message {ctx.message_id}") + return False + + await self.db.update(guild['_id'], {'is_stopped': False}) + await self.db.modify_track(guild['_id'], tracks, 'next', 'extend') + + if guild['current_track']: + await channel.send(content=f"✅ Контент был добавлен в очередь!", delete_after=15) + else: + if not await self.next_track(ctx): + await channel.send(content=f"❌ Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15) + + elif vote_data['action'] == 'play/pause': + vc = await self.get_voice_client(ctx) + if not vc: + await channel.send(content=f"❌ Ошибка при изменении воспроизведения! Попробуйте ещё раз.", delete_after=15) + return False + + if vc.is_playing(): + vc.pause() + else: + vc.resume() + + await self.update_menu_full(ctx) + + elif vote_data['action'] in ('repeat', 'shuffle'): + await self.db.update(guild['_id'], {vote_data['action']: not guild[vote_data['action']]}) + await self.update_menu_view(ctx) + + else: + logging.warning(f"[VOICE] Unknown action '{vote_data['action']}' for message {ctx.message_id}") + return False + + return True + async def _update_menu_views_dict( self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, @@ -836,7 +953,7 @@ class VoiceExtension: }) return True - async def _my_vibe_send_start_feedback(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, track: Track, uid: int): + async def _my_vibe_start_feedback(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, track: Track, uid: int): """Send vibe start feedback to Yandex Music. Return True on success. Args: @@ -861,7 +978,7 @@ class VoiceExtension: logging.debug(f"[VIBE] Track started feedback: {feedback}") return True - async def _my_vibe_send_stop_feedback( + async def _my_vibe_stop_feedback( self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, guild: ExplicitGuild, @@ -904,7 +1021,7 @@ class VoiceExtension: logging.info(f"[VOICE] User {user['_id']} finished vibing with result: {res}") return True - async def _send_next_vibe_feedback( + async def _my_vibe_feedback( self, ctx: ApplicationContext | Interaction, guild: ExplicitGuild, @@ -926,6 +1043,7 @@ class VoiceExtension: Returns: bool: True on success, False otherwise. """ + # TODO: Should be refactored to prevent duplication with `_my_vibe_stop_feedback` and `_my_vibe_start_feedback` logging.debug(f"[VC_EXT] Sending vibe feedback, after: {after}") if not user['vibe_type'] or not user['vibe_id']: @@ -964,21 +1082,21 @@ class VoiceExtension: return feedback - async def _play_next_track( + async def _play_track( self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, - next_track: dict[str, Any], + track: dict[str, Any], *, client: YMClient | None = None, vc: discord.VoiceClient | None = None, menu_message: discord.Message | None = None, button_callback: bool = False, ) -> str | None: - """Play the `next_track` in the voice channel. Avoids additional button and vibe checks. + """Play `track` in the voice channel. Avoids additional vibe checks used in `next_track` and `previous_track`. Args: ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context. - next_track (dict[str, Any]): Next track to play. + track (dict[str, Any]): Track to play. vc (discord.VoiceClient | None, optional): Voice client. Defaults to None. menu_message (discord.Message | None, optional): Menu message to update. Defaults to None. button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False. @@ -986,6 +1104,7 @@ class VoiceExtension: Returns: str | None: Song title or None. """ + # TODO: This should be refactored to avoid code duplication with `next_track` and `previous_track`. client = await self.init_ym_client(ctx) if not client else client if not client: @@ -998,7 +1117,7 @@ class VoiceExtension: return None ym_track = cast(Track, Track.de_json( - next_track, + track, client=client # type: ignore # Async client can be used here. )) return await self.play_track( @@ -1010,7 +1129,7 @@ class VoiceExtension: ) def _get_current_event_loop(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> asyncio.AbstractEventLoop: - """Get the current event loop. If the context is a RawReactionActionEvent, get the loop from the bot. + """Get the current event loop. If the context is a RawReactionActionEvent, get the loop from the self.bot instance. Args: ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context. diff --git a/MusicBot/cogs/voice.py b/MusicBot/cogs/voice.py index 2c6da6f..65c8b56 100644 --- a/MusicBot/cogs/voice.py +++ b/MusicBot/cogs/voice.py @@ -39,7 +39,6 @@ class Voice(Cog, VoiceExtension): voice = discord.SlashCommandGroup("voice", "Команды, связанные с голосовым каналом.") queue = discord.SlashCommandGroup("queue", "Команды, связанные с очередью треков.") - track = discord.SlashCommandGroup("track", "Команды, связанные с треками в голосовом канале.") def __init__(self, bot: discord.Bot): VoiceExtension.__init__(self, bot) @@ -48,7 +47,7 @@ class Voice(Cog, VoiceExtension): @Cog.listener() async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState) -> None: gid = member.guild.id - guild = await self.db.get_guild(gid, projection={'current_menu': 1, 'always_allow_menu': 1}) + guild = await self.db.get_guild(gid, projection={'current_menu': 1}) channel = after.channel or before.channel if not channel: @@ -87,18 +86,7 @@ class Voice(Cog, VoiceExtension): 'repeat': False, 'shuffle': False, 'is_stopped': True }) vc.stop() - elif len(channel.members) > 2 and not guild['always_allow_menu']: - if guild['current_menu']: - logging.info(f"[VOICE] Disabling current menu for guild {gid} due to multiple members") - await self.db.update(gid, {'current_menu': None, 'repeat': False, 'shuffle': False, 'vibing': False}) - try: - message = await channel.fetch_message(guild['current_menu']) - await message.delete() - await channel.send("Меню отключено из-за большого количества участников.", delete_after=15) - except (discord.NotFound, discord.Forbidden): - pass - if member.guild.id in menu_views: menu_views[member.guild.id].stop() del menu_views[member.guild.id] @@ -138,7 +126,7 @@ class Voice(Cog, VoiceExtension): if not guild_id: return - guild = await self.db.get_guild(guild_id, projection={'votes': 1, 'current_track': 1}) + guild = await self.db.get_guild(guild_id) votes = guild['votes'] if str(payload.message_id) not in votes: @@ -158,54 +146,9 @@ class Voice(Cog, VoiceExtension): required_votes = 2 if total_members <= 5 else 4 if total_members <= 10 else 6 if total_members <= 15 else 9 if len(vote_data['positive_votes']) >= required_votes: logging.info(f"[VOICE] Enough positive votes for message {payload.message_id}") - - if vote_data['action'] == 'next': - logging.info(f"[VOICE] Skipping track for message {payload.message_id}") - - title = await self.next_track(payload) - await message.clear_reactions() - await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15) - del votes[str(payload.message_id)] - - elif vote_data['action'] == 'add_track': - logging.info(f"[VOICE] Adding track for message {payload.message_id}") - await message.clear_reactions() - - track = vote_data['vote_content'] - if not track: - logging.info(f"[VOICE] Recieved empty vote context for message {payload.message_id}") - return - - await self.db.modify_track(guild_id, track, 'next', 'append') - - if guild['current_track']: - await message.edit(content=f"Трек был добавлен в очередь!", delete_after=15) - else: - title = await self.next_track(payload) - await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15) - - del votes[str(payload.message_id)] - - elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'): - logging.info(f"[VOICE] Performing '{vote_data['action']}' action for message {payload.message_id}") - - await message.clear_reactions() - - tracks = vote_data['vote_content'] - if not tracks: - logging.info(f"[VOICE] Recieved empty vote context for message {payload.message_id}") - return - - await self.db.update(guild_id, {'is_stopped': False}) - await self.db.modify_track(guild_id, tracks, 'next', 'extend') - - if guild['current_track']: - await message.edit(content=f"Контент был добавлен в очередь!", delete_after=15) - else: - title = await self.next_track(payload) - await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15) - - del votes[str(payload.message_id)] + await message.delete() + await self.proccess_vote(payload, guild, channel, vote_data) + del votes[str(payload.message_id)] elif len(vote_data['negative_votes']) >= required_votes: logging.info(f"[VOICE] Enough negative votes for message {payload.message_id}") @@ -224,9 +167,10 @@ class Voice(Cog, VoiceExtension): guild_id = payload.guild_id if not guild_id: return + guild = await self.db.get_guild(guild_id, projection={'votes': 1}) votes = guild['votes'] - + if str(payload.message_id) not in votes: logging.info(f"[VOICE] Message {payload.message_id} not found in votes") return @@ -257,38 +201,33 @@ class Voice(Cog, VoiceExtension): await self.db.update(guild_id, {'votes': votes}) - @voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.") + @voice.command(name="menu", description="Создать или обновить меню проигрывателя.") async def menu(self, ctx: discord.ApplicationContext) -> None: logging.info(f"[VOICE] Menu command invoked by user {ctx.author.id} in guild {ctx.guild.id}") - if not await self.voice_check(ctx): - return - - guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1}) - channel = cast(discord.VoiceChannel, ctx.channel) - - if len(channel.members) > 2 and not guild['always_allow_menu']: - logging.info(f"[VOICE] Action declined: other members are present in the voice channel") - await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True) - return - - await self.send_menu_message(ctx) + if await self.voice_check(ctx): + await self.send_menu_message(ctx) @voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.") async def join(self, ctx: discord.ApplicationContext) -> None: logging.info(f"[VOICE] Join command invoked by user {ctx.author.id} in guild {ctx.guild.id}") member = cast(discord.Member, ctx.author) - guild = await self.db.get_guild(ctx.guild.id, projection={'allow_connect': 1}) + guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1}) + vc = await self.get_voice_client(ctx) - if not member.guild_permissions.manage_channels and not guild['allow_connect']: + if not member.guild_permissions.manage_channels and not guild['allow_change_connect']: response_message = "❌ У вас нет прав для выполнения этой команды." - elif (vc := await self.get_voice_client(ctx)) and vc.is_connected(): + elif vc and vc.is_connected(): response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave." elif isinstance(ctx.channel, discord.VoiceChannel): - await ctx.channel.connect(timeout=15) - response_message = "Подключение успешно!" + try: + await ctx.channel.connect() + except TimeoutError: + response_message = "❌ Не удалось подключиться к голосовому каналу." + else: + response_message = "✅ Подключение успешно!" else: - response_message = "❌ Вы должны отправить команду в голосовом канале." + response_message = "❌ Вы должны отправить команду в чате голосового канала." logging.info(f"[VOICE] Join command response: {response_message}") await ctx.respond(response_message, delete_after=15, ephemeral=True) @@ -298,22 +237,22 @@ class Voice(Cog, VoiceExtension): logging.info(f"[VOICE] Leave command invoked by user {ctx.author.id} in guild {ctx.guild.id}") member = cast(discord.Member, ctx.author) - guild = await self.db.get_guild(ctx.guild.id, projection={'allow_connect': 1}) - - if not member.guild_permissions.manage_channels and not guild['allow_connect']: + guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1}) + + if not member.guild_permissions.manage_channels and not guild['allow_change_connect']: logging.info(f"[VOICE] User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild.id}") await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True) return if (vc := await self.get_voice_client(ctx)) and await self.voice_check(ctx) and vc.is_connected: - res = await self.stop_playing(ctx, full=True) - if res: - await vc.disconnect(force=True) - await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True) - logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild.id}") - return - else: + res = await self.stop_playing(ctx, vc=vc, full=True) + if not res: await ctx.respond("❌ Не удалось отключиться.", delete_after=15, ephemeral=True) + return + + await vc.disconnect(force=True) + await ctx.respond("✅ Отключение успешно!", delete_after=15, ephemeral=True) + logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild.id}") else: await ctx.respond("❌ Бот не подключен к голосовому каналу.", delete_after=15, ephemeral=True) @@ -329,7 +268,7 @@ class Voice(Cog, VoiceExtension): await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True) elif await self.voice_check(ctx): await self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []}) - await ctx.respond("Очередь и история сброшены.", delete_after=15, ephemeral=True) + await ctx.respond("✅ Очередь и история сброшены.", delete_after=15, ephemeral=True) logging.info(f"[VOICE] Queue and history cleared in guild {ctx.guild.id}") @queue.command(description="Получить очередь треков.") @@ -346,55 +285,7 @@ class Voice(Cog, VoiceExtension): logging.info(f"[VOICE] Queue embed sent to user {ctx.author.id} in guild {ctx.guild.id}") - @track.command(description="Приостановить текущий трек.") - async def pause(self, ctx: discord.ApplicationContext) -> None: - logging.info(f"[VOICE] Pause command invoked by user {ctx.author.id} in guild {ctx.guild.id}") - - member = cast(discord.Member, ctx.author) - channel = cast(discord.VoiceChannel, ctx.channel) - - if len(channel.members) > 2 and not member.guild_permissions.manage_channels: - logging.info(f"[VOICE] User {ctx.author.id} does not have permissions to pause the track in guild {ctx.guild.id}") - await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True) - - elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)) is not None: - if not vc.is_paused(): - vc.pause() - - menu = await self.db.get_current_menu(ctx.guild.id) - if menu: - await self.update_menu_full(ctx, menu) - - logging.info(f"[VOICE] Track paused in guild {ctx.guild.id}") - await ctx.respond("Воспроизведение приостановлено.", delete_after=15, ephemeral=True) - else: - logging.info(f"[VOICE] Track already paused in guild {ctx.guild.id}") - await ctx.respond("Воспроизведение уже приостановлено.", delete_after=15, ephemeral=True) - - @track.command(description="Возобновить текущий трек.") - async def resume(self, ctx: discord.ApplicationContext) -> None: - logging.info(f"[VOICE] Resume command invoked by user {ctx.author.id} in guild {ctx.guild.id}") - - member = cast(discord.Member, ctx.author) - channel = cast(discord.VoiceChannel, ctx.channel) - - if len(channel.members) > 2 and not member.guild_permissions.manage_channels: - logging.info(f"[VOICE] User {ctx.author.id} does not have permissions to resume the track in guild {ctx.guild.id}") - await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True) - - elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)): - if vc.is_paused(): - vc.resume() - menu = await self.db.get_current_menu(ctx.guild.id) - if menu: - await self.update_menu_full(ctx, menu) - logging.info(f"[VOICE] Track resumed in guild {ctx.guild.id}") - await ctx.respond("Воспроизведение восстановлено.", delete_after=15, ephemeral=True) - else: - logging.info(f"[VOICE] Track is not paused in guild {ctx.guild.id}") - await ctx.respond("Воспроизведение не на паузе.", delete_after=15, ephemeral=True) - - @track.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.") + @voice.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.") async def stop(self, ctx: discord.ApplicationContext) -> None: logging.info(f"[VOICE] Stop command invoked by user {ctx.author.id} in guild {ctx.guild.id}") @@ -408,115 +299,10 @@ class Voice(Cog, VoiceExtension): elif await self.voice_check(ctx): res = await self.stop_playing(ctx, full=True) if res: - await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True) + await ctx.respond("✅ Воспроизведение остановлено.", delete_after=15, ephemeral=True) else: await ctx.respond("❌ Произошла ошибка при остановке воспроизведения.", delete_after=15, ephemeral=True) - @track.command(description="Переключиться на следующую песню в очереди.") - async def next(self, ctx: discord.ApplicationContext) -> None: - logging.info(f"[VOICE] Next command invoked by user {ctx.author.id} in guild {ctx.guild.id}") - if not await self.voice_check(ctx): - return - - gid = ctx.guild.id - guild = await self.db.get_guild(gid, projection={'next_tracks': 1, 'vote_next_track': 1}) - if not guild['next_tracks']: - logging.info(f"[VOICE] No tracks in queue in guild {ctx.guild.id}") - await ctx.respond("❌ Нет песенен в очереди.", delete_after=15, ephemeral=True) - return - - member = cast(discord.Member, ctx.author) - channel = cast(discord.VoiceChannel, ctx.channel) - - if guild['vote_next_track'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels: - logging.info(f"[VOICE] User {ctx.author.id} started vote to skip track in guild {ctx.guild.id}") - - message = cast(discord.Interaction, await ctx.respond(f"{member.mention} хочет пропустить текущий трек.\n\nВыполнить переход?", delete_after=30)) - response = await message.original_response() - - await response.add_reaction('✅') - await response.add_reaction('❌') - - await self.db.update_vote( - gid, - response.id, - { - 'positive_votes': list(), - 'negative_votes': list(), - 'total_members': len(channel.members), - 'action': 'next', - 'vote_content': None - } - ) - else: - logging.info(f"[VOICE] Skipping vote for user {ctx.author.id} in guild {ctx.guild.id}") - - await self.db.update(gid, {'is_stopped': False}) - title = await self.next_track(ctx) - await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15) - - @track.command(description="Добавить трек в избранное или убрать, если он уже там.") - async def like(self, ctx: discord.ApplicationContext) -> None: - logging.info(f"[VOICE] Like command invoked by user {ctx.author.id} in guild {ctx.guild.id}") - - if not await self.voice_check(ctx): - return - - vc = await self.get_voice_client(ctx) - if not vc or not vc.is_playing: - logging.info(f"[VOICE] No current track in {ctx.guild.id}") - await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True) - return - - result = await self.react_track(ctx, 'like') - if not result[0]: - logging.warning(f"Like command failed for user {ctx.author.id} in guild {ctx.guild.id}") - await ctx.respond("❌ Операция не удалась.", delete_after=15, ephemeral=True) - elif result[1] == 'removed': - logging.info(f"[VOICE] Track removed from favorites for user {ctx.author.id} in guild {ctx.guild.id}") - await ctx.respond("Трек был удалён из избранного.", delete_after=15, ephemeral=True) - elif result[1] == 'added': - logging.info(f"[VOICE] Track added to favorites for user {ctx.author.id} in guild {ctx.guild.id}") - await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True) - else: - raise ValueError(f"Unknown like command result: '{result}'") - - @track.command(name='vibe', description="Запустить Мою Волну по текущему треку.") - async def track_vibe(self, ctx: discord.ApplicationContext) -> None: - logging.info(f"[VOICE] Vibe (track) command invoked by user {ctx.author.id} in guild {ctx.guild.id}") - if not await self.voice_check(ctx): - return - - guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1, 'current_track': 1, 'current_menu': 1, 'vibing': 1}) - channel = cast(discord.VoiceChannel, ctx.channel) - - if len(channel.members) > 2 and not guild['always_allow_menu']: - logging.info(f"[VOICE] Action declined: other members are present in the voice channel") - await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True) - return - - if guild['vibing']: - logging.info(f"[VOICE] Action declined: vibing is already enabled in guild {ctx.guild.id}") - await ctx.respond("❌ Моя Волна уже включена. Используйте /track stop, чтобы остановить воспроизведение.", ephemeral=True) - return - - if not guild['current_track']: - logging.info(f"[VOICE] No current track in {ctx.guild.id}") - await ctx.respond("❌ Нет воспроизводимого трека.", ephemeral=True) - return - - feedback = await self.update_vibe(ctx, 'track', guild['current_track']['id']) - if not feedback: - await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True) - return - - if not guild['current_menu']: - await self.send_menu_message(ctx, disable=True) - - next_track = await self.db.get_track(ctx.guild_id, 'next') - if next_track: - await self._play_next_track(ctx, next_track) - @voice.command(name='vibe', description="Запустить Мою Волну.") @discord.option( "запрос", @@ -531,18 +317,14 @@ class Voice(Cog, VoiceExtension): if not await self.voice_check(ctx): return - guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1, 'current_menu': 1, 'vibing': 1}) - channel = cast(discord.VoiceChannel, ctx.channel) + guild = await self.db.get_guild(ctx.guild.id, projection={'current_menu': 1, 'vibing': 1}) - if len(channel.members) > 2 and not guild['always_allow_menu']: - logging.info(f"[VOICE] Action declined: other members are present in the voice channel") - await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True) - return if guild['vibing']: logging.info(f"[VOICE] Action declined: vibing is already enabled in guild {ctx.guild.id}") - await ctx.respond("❌ Моя Волна уже включена. Используйте /track stop, чтобы остановить воспроизведение.", ephemeral=True) + await ctx.respond("❌ Моя Волна уже включена. Используйте /track stop, чтобы остановить воспроизведение.", delete_after=15, ephemeral=True) return + await ctx.defer(invisible=False) if name: token = await users_db.get_ym_token(ctx.user.id) if not token: @@ -564,27 +346,29 @@ class Voice(Cog, VoiceExtension): if not content: logging.debug(f"[VOICE] Station {name} not found") - await ctx.respond("❌ Станция не найдена.", ephemeral=True) + await ctx.respond("❌ Станция не найдена.", delete_after=15, ephemeral=True) return _type, _id = content.ad_params.other_params.split(':') if content.ad_params else (None, None) if not _type or not _id: logging.debug(f"[VOICE] Station {name} has no ad params") - await ctx.respond("❌ Станция не найдена.", ephemeral=True) + await ctx.respond("❌ Станция не найдена.", delete_after=15, ephemeral=True) return - - feedback = await self.update_vibe(ctx, _type, _id) else: - feedback = await self.update_vibe(ctx, 'user', 'onyourwave') + _type, _id = 'user', 'onyourwave' + + feedback = await self.update_vibe(ctx, _type, _id) if not feedback: - await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True) + await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", delete_after=15, ephemeral=True) return - if not guild['current_menu']: + if guild['current_menu']: + await ctx.respond("✅ Моя Волна включена.", delete_after=15, ephemeral=True) + else: await self.send_menu_message(ctx, disable=True) next_track = await self.db.get_track(ctx.guild_id, 'next') if next_track: - await self._play_next_track(ctx, next_track) + await self._play_track(ctx, next_track) diff --git a/MusicBot/database/base.py b/MusicBot/database/base.py index 73f96ae..ea91efe 100644 --- a/MusicBot/database/base.py +++ b/MusicBot/database/base.py @@ -80,12 +80,9 @@ class BaseGuildsDatabase: current_menu=None, is_stopped=True, always_allow_menu=False, - allow_connect=True, - vote_next_track=True, - vote_add_track=True, - vote_add_album=True, - vote_add_artist=True, - vote_add_playlist=True, + allow_change_connect=True, + vote_switch_track=True, + vote_add=True, shuffle=False, repeat=False, votes={}, diff --git a/MusicBot/database/extensions.py b/MusicBot/database/extensions.py index 6e83df8..7352a16 100644 --- a/MusicBot/database/extensions.py +++ b/MusicBot/database/extensions.py @@ -19,28 +19,29 @@ class VoiceGuildsDatabase(BaseGuildsDatabase): if list_type not in ('next', 'previous', 'current'): raise ValueError("list_type must be either 'next' or 'previous'") - if list_type == 'current': - return (await self.get_guild(gid, projection={'current_track': 1}))['current_track'] - field = f'{list_type}_tracks' - update = {'$pop': {field: -1}} + guild = await self.get_guild(gid, projection={'current_track': 1, field: 1}) + + if list_type == 'current': + return guild['current_track'] + result = await guilds.find_one_and_update( {'_id': gid}, - update, + {'$pop': {field: -1}}, projection={field: 1}, return_document=ReturnDocument.BEFORE ) - res = result.get(field, [])[0] if result and result.get(field) else None + res = result.get(field, []) if result else None if field == 'previous_tracks' and res: await guilds.find_one_and_update( {'_id': gid}, - {'$push': {'next_tracks': {'$each': [res], '$position': 0}}}, + {'$push': {'next_tracks': {'$each': [guild['current_track']], '$position': 0}}}, projection={'next_tracks': 1} ) - return res + return res[0] if res else None async def modify_track( self, diff --git a/MusicBot/database/guild.py b/MusicBot/database/guild.py index 1b46395..b915b74 100644 --- a/MusicBot/database/guild.py +++ b/MusicBot/database/guild.py @@ -4,8 +4,8 @@ class MessageVotes(TypedDict): positive_votes: list[int] negative_votes: list[int] total_members: int - action: Literal['next', 'add_track', 'add_album', 'add_artist', 'add_playlist'] - vote_content: dict[str, Any] | list[dict[str, Any]] | None + action: Literal['next', 'play/pause', 'repeat', 'shuffle', 'previous', 'add_track', 'add_album', 'add_artist', 'add_playlist'] + vote_content: Any | None class Guild(TypedDict, total=False): next_tracks: list[dict[str, Any]] @@ -14,12 +14,9 @@ class Guild(TypedDict, total=False): current_menu: int | None is_stopped: bool always_allow_menu: bool - allow_connect: bool - vote_next_track: bool - vote_add_track: bool - vote_add_album: bool - vote_add_artist: bool - vote_add_playlist: bool + allow_change_connect: bool + vote_switch_track: bool + vote_add: bool shuffle: bool repeat: bool votes: dict[str, MessageVotes] @@ -34,12 +31,9 @@ class ExplicitGuild(TypedDict): current_menu: int | None is_stopped: bool # Prevents the `after` callback of play_track always_allow_menu: bool - allow_connect: bool - vote_next_track: bool - vote_add_track: bool - vote_add_album: bool - vote_add_artist: bool - vote_add_playlist: bool + allow_change_connect: bool + vote_switch_track: bool + vote_add: bool shuffle: bool repeat: bool votes: dict[str, MessageVotes] diff --git a/MusicBot/ui/find.py b/MusicBot/ui/find.py index bbdc01e..b682c96 100644 --- a/MusicBot/ui/find.py +++ b/MusicBot/ui/find.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Literal, cast +from typing import cast import discord from yandex_music import Track, Album, Artist, Playlist @@ -27,40 +27,39 @@ class PlayButton(Button, VoiceExtension): return gid = interaction.guild.id - guild = await self.db.get_guild(gid, projection={'current_track': 1, 'current_menu': 1, 'vote_add_track': 1, 'vote_add_album': 1, 'vote_add_artist': 1, 'vote_add_playlist': 1}) + guild = await self.db.get_guild(gid, projection={'current_track': 1, 'current_menu': 1, 'vote_add': 1}) channel = cast(discord.VoiceChannel, interaction.channel) member = cast(discord.Member, interaction.user) - action: Literal['add_track', 'add_album', 'add_artist', 'add_playlist'] if isinstance(self.item, Track): tracks = [self.item] action = 'add_track' vote_message = f"{member.mention} хочет добавить трек **{self.item.title}** в очередь.\n\n Голосуйте за добавление." - response_message = f"Трек **{self.item.title}** был добавлен в очередь." + response_message = f"✅ Трек **{self.item.title}** был добавлен в очередь." elif isinstance(self.item, Album): album = await self.item.with_tracks_async() if not album or not album.volumes: logging.debug("[FIND] Failed to fetch album tracks in PlayButton callback") - await interaction.respond("Не удалось получить треки альбома.", ephemeral=True) + await interaction.respond("❌ Не удалось получить треки альбома.", ephemeral=True) return tracks = [track for volume in album.volumes for track in volume] action = 'add_album' vote_message = f"{member.mention} хочет добавить альбом **{self.item.title}** в очередь.\n\n Голосуйте за добавление." - response_message = f"Альбом **{self.item.title}** был добавлен в очередь." + response_message = f"✅ Альбом **{self.item.title}** был добавлен в очередь." elif isinstance(self.item, Artist): artist_tracks = await self.item.get_tracks_async() if not artist_tracks: logging.debug("[FIND] Failed to fetch artist tracks in PlayButton callback") - await interaction.respond("Не удалось получить треки артиста.", ephemeral=True) + await interaction.respond("❌ Не удалось получить треки артиста.", ephemeral=True) return tracks = artist_tracks.tracks.copy() action = 'add_artist' vote_message = f"{member.mention} хочет добавить треки от **{self.item.name}** в очередь.\n\n Голосуйте за добавление." - response_message = f"Песни артиста **{self.item.name}** были добавлены в очередь." + response_message = f"✅ Песни артиста **{self.item.name}** были добавлены в очередь." elif isinstance(self.item, Playlist): short_tracks = await self.item.fetch_tracks_async() @@ -72,7 +71,7 @@ class PlayButton(Button, VoiceExtension): tracks = [cast(Track, short_track.track) for short_track in short_tracks] action = 'add_playlist' vote_message = f"{member.mention} хочет добавить плейлист **{self.item.title}** в очередь.\n\n Голосуйте за добавление." - response_message = f"Плейлист **{self.item.title}** был добавлен в очередь." + response_message = f"✅ Плейлист **{self.item.title}** был добавлен в очередь." elif isinstance(self.item, list): tracks = self.item.copy() @@ -83,12 +82,12 @@ class PlayButton(Button, VoiceExtension): action = 'add_playlist' vote_message = f"{member.mention} хочет добавить плейлист **Мне Нравится** в очередь.\n\n Голосуйте за добавление." - response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь." + response_message = f"✅ Плейлист **«Мне нравится»** был добавлен в очередь." else: raise ValueError(f"Unknown item type: '{type(self.item).__name__}'") - if guild.get(f'vote_{action}') and len(channel.members) > 2 and not member.guild_permissions.manage_channels: + if guild['vote_add'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels: logging.debug(f"Starting vote for '{action}' (from PlayButton callback)") message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30)) @@ -108,26 +107,28 @@ class PlayButton(Button, VoiceExtension): 'vote_content': [track.to_dict() for track in tracks] } ) + return + + logging.debug(f"[FIND] Skipping vote for '{action}'") + + if guild['current_menu']: + await interaction.respond(response_message, delete_after=15) else: - logging.debug(f"[FIND] Skipping vote for '{action}' (from PlayButton callback)") + await self.send_menu_message(interaction, disable=True) - current_menu = await self.get_menu_message(interaction, guild['current_menu']) if guild['current_menu'] else None + if guild['current_track'] is not None: + logging.debug(f"[FIND] Adding tracks to queue") + await self.db.modify_track(gid, tracks, 'next', 'extend') + else: + logging.debug(f"[FIND] Playing track") + track = tracks.pop(0) + await self.db.modify_track(gid, tracks, 'next', 'extend') + await self.play_track(interaction, track) - if guild['current_track'] is not None: - logging.debug(f"[FIND] Adding tracks to queue (from PlayButton callback)") - await self.db.modify_track(gid, tracks, 'next', 'extend') - else: - logging.debug(f"[FIND] Playing track (from PlayButton callback)") - track = tracks.pop(0) - await self.db.modify_track(gid, tracks, 'next', 'extend') - await self.play_track(interaction, track) - response_message = f"Сейчас играет: **{track.title}**!" - - if current_menu and interaction.message: - logging.debug(f"[FIND] Deleting interaction message {interaction.message.id}: current player {current_menu.id} found") - await interaction.message.delete() - else: - await interaction.respond(response_message, delete_after=15) + if interaction.message: + await interaction.message.delete() + else: + logging.warning(f"[FIND] Interaction message is None") class MyVibeButton(Button, VoiceExtension): def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *args, **kwargs): @@ -145,14 +146,6 @@ class MyVibeButton(Button, VoiceExtension): logging.warning(f"[VIBE] Guild ID is None in button callback") return - guild = await self.db.get_guild(gid) - channel = cast(discord.VoiceChannel, interaction.channel) - - if len(channel.members) > 2 and not guild['always_allow_menu']: - logging.info(f"[VIBE] Button callback declined: other members are present in the voice channel") - await interaction.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True) - return - track_type_map = { Track: 'track', Album: 'album', Artist: 'artist', Playlist: 'playlist', list: 'user' } @@ -178,7 +171,7 @@ class MyVibeButton(Button, VoiceExtension): next_track = await self.db.get_track(gid, 'next') if next_track: - await self._play_next_track(interaction, next_track) + await self._play_track(interaction, next_track) class ListenView(View): def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True): @@ -217,3 +210,4 @@ class ListenView(View): return await super().on_timeout() except discord.NotFound: pass + self.stop() diff --git a/MusicBot/ui/menu.py b/MusicBot/ui/menu.py index 4f45251..e617ad3 100644 --- a/MusicBot/ui/menu.py +++ b/MusicBot/ui/menu.py @@ -2,7 +2,7 @@ import logging from typing import Self, cast from discord.ui import View, Button, Item, Select -from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption +from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption, Member import yandex_music.exceptions from yandex_music import TrackLyrics, Playlist, ClientAsync as YMClient @@ -13,14 +13,14 @@ class ToggleButton(Button, VoiceExtension): super().__init__(*args, **kwargs) VoiceExtension.__init__(self, None) - async def callback(self, interaction: Interaction): + async def callback(self, interaction: Interaction) -> None: callback_type = interaction.custom_id if callback_type not in ('repeat', 'shuffle'): raise ValueError(f"Invalid callback type: '{callback_type}'") logging.info(f'[MENU] {callback_type.capitalize()} button callback') - if not (gid := interaction.guild_id): + if not (gid := interaction.guild_id) or not interaction.user: logging.warning('[MENU] Failed to get guild ID.') await interaction.respond("❌ Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True) return @@ -29,9 +29,36 @@ class ToggleButton(Button, VoiceExtension): return guild = await self.db.get_guild(gid) + member = cast(Member, interaction.user) + channel = cast(VoiceChannel, interaction.channel) + + if len(channel.members) > 2 and not member.guild_permissions.manage_channels: + logging.info(f"[MENU] User {interaction.user.id} started vote to pause/resume track in guild {gid}") + + action = "выключить" if guild[callback_type] else "включить" + task = "перемешивание треков" if callback_type == 'shuffle' else "повтор трека" + message = cast(Interaction, await interaction.respond(f"{member.mention} хочет {action} {task}.\n\nВыполнить действие?", delete_after=60)) + response = await message.original_response() + + await response.add_reaction('✅') + await response.add_reaction('❌') + + await self.db.update_vote( + gid, + response.id, + { + 'positive_votes': list(), + 'negative_votes': list(), + 'total_members': len(channel.members), + 'action': callback_type, + 'vote_content': None + } + ) + return + await self.db.update(gid, {callback_type: not guild[callback_type]}) - if not await self.update_menu_view(interaction, guild, button_callback=True): + if not await self.update_menu_view(interaction, button_callback=True): await interaction.respond("❌ Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True) class PlayPauseButton(Button, VoiceExtension): @@ -44,9 +71,39 @@ class PlayPauseButton(Button, VoiceExtension): if not await self.voice_check(interaction, check_vibe_privilage=True): return + if not (gid := interaction.guild_id) or not interaction.user: + logging.warning('[MENU] Failed to get guild ID or user.') + return + if not (vc := await self.get_voice_client(interaction)) or not interaction.message: return + member = cast(Member, interaction.user) + channel = cast(VoiceChannel, interaction.channel) + + if len(channel.members) > 2 and not member.guild_permissions.manage_channels: + logging.info(f"[MENU] User {interaction.user.id} started vote to pause/resume track in guild {gid}") + + task = "приостановить" if vc.is_playing() else "возобновить" + message = cast(Interaction, await interaction.respond(f"{member.mention} хочет {task} проигрывание.\n\nВыполнить действие?", delete_after=60)) + response = await message.original_response() + + await response.add_reaction('✅') + await response.add_reaction('❌') + + await self.db.update_vote( + gid, + response.id, + { + 'positive_votes': list(), + 'negative_votes': list(), + 'total_members': len(channel.members), + 'action': "play/pause", + 'vote_content': None + } + ) + return + try: embed = interaction.message.embeds[0] except IndexError: @@ -67,23 +124,61 @@ class SwitchTrackButton(Button, VoiceExtension): super().__init__(*args, **kwargs) VoiceExtension.__init__(self, None) - async def callback(self, interaction: Interaction): + async def callback(self, interaction: Interaction) -> None: callback_type = interaction.custom_id if callback_type not in ('next', 'previous'): raise ValueError(f"Invalid callback type: '{callback_type}'") - + + if not (gid := interaction.guild_id) or not interaction.user: + logging.warning(f"[MENU] {callback_type.capitalize()} track button callback without guild id or user") + return + logging.info(f'[MENU] {callback_type.capitalize()} track button callback') if not await self.voice_check(interaction, check_vibe_privilage=True): return + tracks_type = callback_type + '_tracks' + guild = await self.db.get_guild(gid, projection={tracks_type: 1, 'vote_switch_track': 1}) + + if not guild[tracks_type]: + logging.info(f"[MENU] No tracks in '{tracks_type}' list in guild {gid}") + await interaction.respond(f"❌ Нет треков в {'очереди' if callback_type == 'next' else 'истории'}.", delete_after=15, ephemeral=True) + return + + member = cast(Member, interaction.user) + channel = cast(VoiceChannel, interaction.channel) + + if guild['vote_switch_track'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels: + logging.info(f"[MENU] User {interaction.user.id} started vote to skip track in guild {gid}") + + task = "пропустить текущий трек" if callback_type == 'next' else "вернуться к предыдущему треку" + message = cast(Interaction, await interaction.respond(f"{member.mention} хочет {task}.\n\nВыполнить переход?", delete_after=60)) + response = await message.original_response() + + await response.add_reaction('✅') + await response.add_reaction('❌') + + await self.db.update_vote( + gid, + response.id, + { + 'positive_votes': list(), + 'negative_votes': list(), + 'total_members': len(channel.members), + 'action': callback_type, + 'vote_content': None + } + ) + return + if callback_type == 'next': title = await self.next_track(interaction, button_callback=True) else: - title = await self.prev_track(interaction, button_callback=True) + title = await self.previous_track(interaction, button_callback=True) if not title: - await interaction.respond(f"❌ Нет треков в очереди.", delete_after=15, ephemeral=True) + await interaction.respond(f"❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True) class ReactionButton(Button, VoiceExtension): def __init__(self, *args, **kwargs): @@ -103,16 +198,32 @@ class ReactionButton(Button, VoiceExtension): if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing: await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True) + channel = cast(VoiceChannel, interaction.channel) res = await self.react_track(interaction, callback_type) if callback_type == 'like' and res[0]: await self._update_menu_views_dict(interaction) await interaction.edit(view=menu_views[gid]) + await interaction.respond( + f"✅ Трек был {'добавлен в понравившиеся.' if res[1] == 'added' else 'удалён из понравившихся.'}", + delete_after=15, ephemeral=True + ) + elif callback_type == 'dislike' and res[0]: - await self.next_track(interaction, vc=vc, button_callback=True) + + if len(channel.members) == 2 and not await self.next_track(interaction, vc=vc, button_callback=True): + await interaction.respond("✅ Воспроизведение приостановлено. Нет треков в очереди.", delete_after=15) + + await self._update_menu_views_dict(interaction) + await interaction.edit(view=menu_views[gid]) + await interaction.respond( + f"✅ Трек был {'добавлен в дизлайки.' if res[1] == 'added' else 'удалён из дизлайков.'}", + delete_after=15, ephemeral=True + ) + else: - logging.debug(f"[VC_EXT] Failed to {callback_type} track") - await interaction.respond("❌ Операция не удалась. Попробуйте позже.") + logging.debug(f"[VC_EXT] Failed to get {callback_type} tracks") + await interaction.respond("❌ Операция не удалась. Попробуйте позже.", delete_after=15, ephemeral=True) class LyricsButton(Button, VoiceExtension): def __init__(self, **kwargs): @@ -192,7 +303,7 @@ class MyVibeButton(Button, VoiceExtension): if next_track: # Need to avoid additional feedback. # TODO: Make it more elegant - await self._play_next_track(interaction, next_track, button_callback=True) + await self._play_track(interaction, next_track, button_callback=True) class MyVibeSelect(Select, VoiceExtension): def __init__(self, *args, **kwargs): @@ -214,23 +325,23 @@ class MyVibeSelect(Select, VoiceExtension): logging.warning(f'[MENU] Unknown custom_id: {custom_id}') return - if not interaction.data or 'values' not in interaction.data: + if not interaction.data: logging.warning('[MENU] No data in select callback') return - data_value = interaction.data['values'][0] - if data_value not in ( + data_values = cast(list[str] | None, interaction.data.get('values')) + if not data_values or data_values[0] not in ( 'fun', 'active', 'calm', 'sad', 'all', 'favorite', 'popular', 'discover', 'default', 'not-russian', 'russian', 'without-words', 'any' ): - logging.warning(f'[MENU] Unknown data_value: {data_value}') + logging.warning(f'[MENU] Unknown data_value: {data_values}') return - logging.info(f"[MENU] Settings option '{custom_id}' updated to {data_value}") - await self.users_db.update(interaction.user.id, {f'vibe_settings.{custom_id}': data_value}) + logging.info(f"[MENU] Settings option '{custom_id}' updated to '{data_values[0]}'") + await self.users_db.update(interaction.user.id, {f'vibe_settings.{custom_id}': data_values[0]}) - view = MyVibeSettingsView(interaction) + view = await MyVibeSettingsView(interaction).init() view.disable_all_items() await interaction.edit(view=view) @@ -330,10 +441,15 @@ class AddToPlaylistSelect(Select, VoiceExtension): logging.warning('[MENU] No data in select callback') return - data = interaction.data['values'][0].split(';') - logging.debug(f"[MENU] Add to playlist select callback: {data}") + data_values = cast(list[str] | None, interaction.data.get('values')) + logging.debug(f"[MENU] Add to playlist select callback: {data_values}") - playlist = cast(Playlist, await self.ym_client.users_playlists(kind=data[0], user_id=data[1])) + if not data_values: + logging.warning('[MENU] No data in select callback') + return + + kind, user_id = data_values[0].split(';') + playlist = cast(Playlist, await self.ym_client.users_playlists(kind=kind, user_id=user_id)) current_track = await self.db.get_track(interaction.guild_id, 'current') if not current_track: @@ -362,14 +478,19 @@ class AddToPlaylistButton(Button, VoiceExtension): return client = await self.init_ym_client(interaction) - if not client or not client.me or not client.me.account or not client.me.account.uid: - await interaction.respond('❌ Что-то пошло не так. Попробуйте позже.', ephemeral=True) + if not client: + await interaction.respond('❌ Что-то пошло не так. Попробуйте позже.', delete_after=15, ephemeral=True) return if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing: await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True) return + playlists = await client.users_playlists_list() + if not playlists: + await interaction.respond('❌ У вас нет плейлистов.', delete_after=15, ephemeral=True) + return + view = View( AddToPlaylistSelect( client, @@ -379,7 +500,7 @@ class AddToPlaylistButton(Button, VoiceExtension): SelectOption( label=playlist.title or "Без названия", value=f"{playlist.kind or "-1"};{playlist.uid}" - ) for playlist in await client.users_playlists_list(client.me.account.uid) + ) for playlist in playlists ] ) ) @@ -427,10 +548,9 @@ class MenuView(View, VoiceExtension): self.add_item(self.next_button) self.add_item(self.shuffle_button) - if not isinstance(self.ctx, RawReactionActionEvent) and len(cast(VoiceChannel, self.ctx.channel).members) > 2: - self.dislike_button.disabled = True - elif likes and current_track and str(current_track['id']) in [str(like.id) for like in likes]: - self.like_button.style = ButtonStyle.success + if not isinstance(self.ctx, RawReactionActionEvent) and len(cast(VoiceChannel, self.ctx.channel).members) == 2: + if likes and current_track and str(current_track['id']) in [str(like.id) for like in likes]: + self.like_button.style = ButtonStyle.success if not current_track: self.lyrics_button.disabled = True @@ -444,7 +564,7 @@ class MenuView(View, VoiceExtension): self.add_item(self.dislike_button) self.add_item(self.lyrics_button) self.add_item(self.add_to_playlist_button) - + if self.guild['vibing']: self.add_item(self.vibe_settings_button) else: @@ -469,3 +589,4 @@ class MenuView(View, VoiceExtension): logging.debug('[MENU] Successfully deleted menu message') else: logging.debug('[MENU] No menu message found') + self.stop()