impr: Rework MenuView.

This commit is contained in:
Lemon4ksan
2025-03-19 23:23:40 +03:00
parent 76dd9d5bfc
commit e320e10547
3 changed files with 153 additions and 130 deletions

View File

@@ -6,14 +6,13 @@ import yandex_music.exceptions
from yandex_music import ClientAsync as YMClient from yandex_music import ClientAsync as YMClient
import discord import discord
from discord.ui import View
from discord import Interaction, ApplicationContext, RawReactionActionEvent, MISSING from discord import Interaction, ApplicationContext, RawReactionActionEvent, MISSING
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
class BaseBot: class BaseBot:
menu_views: dict[int, View] = {} # Store menu views and delete them when needed to prevent memory leaks for after callbacks. menu_views: dict[int, Any] = {} # Store menu views and delete them when needed to prevent memory leaks for after callbacks.
_ym_clients: dict[str, YMClient] = {} # Store YM clients to prevent creating new ones for each command. _ym_clients: dict[str, YMClient] = {} # Store YM clients to prevent creating new ones for each command.
def __init__(self, bot: discord.Bot | None) -> None: def __init__(self, bot: discord.Bot | None) -> None:
@@ -106,10 +105,10 @@ class BaseBot:
if not embed and response_type: if not embed and response_type:
if content: if content:
kwargs['description'] = content kwargs['description'] = content
embed = self.generate_response_embed(response_type, **kwargs) embed = self.generate_response_embed(ctx, response_type, **kwargs)
content = None content = None
if not isinstance(ctx, RawReactionActionEvent) and ctx.response.is_done(): if not isinstance(ctx, RawReactionActionEvent) and not view and ctx.response.is_done():
view = MISSING view = MISSING
if not isinstance(ctx, RawReactionActionEvent): if not isinstance(ctx, RawReactionActionEvent):
@@ -176,41 +175,37 @@ class BaseBot:
return guild['current_viber_id'] return guild['current_viber_id']
return ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None return ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
async def update_menu_views_dict( async def init_menu_view(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, gid: int, *, disable: bool = False) -> None:
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
*,
disable: bool = False
) -> None:
"""Genereate a new menu view and update the `menu_views` dict. This prevents creating multiple menu views for the same guild.
Use guild id as a key to access menu view.
Args:
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context
guild (ExplicitGuild): Guild.
disable (bool, optional): Disable menu. Defaults to False.
"""
logging.debug(f"[BASE_BOT] Updating menu views dict for guild {ctx.guild_id}")
from MusicBot.ui import MenuView from MusicBot.ui import MenuView
self.menu_views[gid] = await MenuView(ctx).init(disable=disable)
if not ctx.guild_id:
logging.warning("[BASE_BOT] Guild not found")
return
if ctx.guild_id in self.menu_views:
self.menu_views[ctx.guild_id].stop()
self.menu_views[ctx.guild_id] = await MenuView(ctx).init(disable=disable)
def generate_response_embed( def generate_response_embed(
self, self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
embed_type: Literal['info', 'success', 'error'] = 'info', embed_type: Literal['info', 'success', 'error'] = 'info',
**kwargs: Any **kwargs: Any
) -> discord.Embed: ) -> discord.Embed:
if isinstance(ctx, Interaction):
name = ctx.client.user.name if ctx.client.user else None
icon_url = ctx.client.user.avatar.url if ctx.client.user and ctx.client.user.avatar else None
elif isinstance(ctx, ApplicationContext):
name = ctx.bot.user.name if ctx.bot.user else None
icon_url = ctx.bot.user.avatar.url if ctx.bot.user and ctx.bot.user.avatar else None
elif self.bot:
name = self.bot.user.name if self.bot.user else None
icon_url = self.bot.user.avatar.url if self.bot.user and self.bot.user.avatar else None
else:
name = icon_url = None
if not name:
name = 'YandexMusic'
if not icon_url:
icon_url="https://github.com/Lemon4ksan/YandexMusicDiscordBot/blob/main/assets/Logo.png?raw=true"
embed = discord.Embed(**kwargs) embed = discord.Embed(**kwargs)
embed.set_author(name='YandexMusic', icon_url="https://github.com/Lemon4ksan/YandexMusicDiscordBot/blob/main/assets/Logo.png?raw=true") embed.set_author(name=name, icon_url=icon_url)
if embed_type == 'info': if embed_type == 'info':
embed.color = 0xfed42b embed.color = 0xfed42b

View File

@@ -8,7 +8,7 @@ import yandex_music.exceptions
from yandex_music import Track, TrackShort, ClientAsync as YMClient from yandex_music import Track, TrackShort, ClientAsync as YMClient
import discord import discord
from discord import Interaction, ApplicationContext, RawReactionActionEvent, VoiceChannel from discord import Interaction, ApplicationContext, RawReactionActionEvent
from MusicBot.cogs.utils.base_bot import BaseBot from MusicBot.cogs.utils.base_bot import BaseBot
from MusicBot.cogs.utils import generate_item_embed from MusicBot.cogs.utils import generate_item_embed
@@ -63,11 +63,9 @@ class VoiceExtension(BaseBot):
if guild['current_menu']: if guild['current_menu']:
logging.info(f"[VC_EXT] Deleting old menu message {guild['current_menu']} in guild {ctx.guild_id}") logging.info(f"[VC_EXT] Deleting old menu message {guild['current_menu']} in guild {ctx.guild_id}")
if (message := await self.get_menu_message(ctx, guild['current_menu'])): await self._delete_menu_message(ctx, guild['current_menu'], ctx.guild_id)
await message.delete()
await self.update_menu_views_dict(ctx, disable=disable)
await self.init_menu_view(ctx, ctx.guild_id, disable=disable)
interaction = await self.respond(ctx, embed=embed, view=self.menu_views[ctx.guild_id]) interaction = await self.respond(ctx, embed=embed, view=self.menu_views[ctx.guild_id])
response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction
@@ -120,7 +118,6 @@ class VoiceExtension(BaseBot):
Args: Args:
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context. ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
menu_mid (int): Id of the menu message to update. Defaults to None.
menu_message (discord.Message | None): Message to update. If None, fetches menu from channel using `menu_mid`. Defaults to None. menu_message (discord.Message | None): Message to update. If None, fetches menu from channel using `menu_mid`. Defaults to None.
button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False. button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False.
@@ -174,7 +171,7 @@ class VoiceExtension(BaseBot):
else: else:
embed.remove_footer() embed.remove_footer()
await self.update_menu_views_dict(ctx) await self.menu_views[ctx.guild_id].update()
try: try:
if isinstance(ctx, Interaction) and button_callback: if isinstance(ctx, Interaction) and button_callback:
# If interaction from menu buttons # If interaction from menu buttons
@@ -193,7 +190,6 @@ class VoiceExtension(BaseBot):
self, self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent, ctx: ApplicationContext | Interaction | RawReactionActionEvent,
*, *,
menu_message: discord.Message | None = None,
button_callback: bool = False, button_callback: bool = False,
disable: bool = False disable: bool = False
) -> bool: ) -> bool:
@@ -201,8 +197,6 @@ class VoiceExtension(BaseBot):
Args: Args:
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context. ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
guild (ExplicitGuild): Guild data.
menu_message (discord.Message | None, optional): Menu message to update. Defaults to None.
button_callback (bool, optional): If True, the interaction is from a button callback. Defaults to False. button_callback (bool, optional): If True, the interaction is from a button callback. Defaults to False.
disable (bool, optional): Disable the view if True. Defaults to False. disable (bool, optional): Disable the view if True. Defaults to False.
@@ -210,29 +204,33 @@ class VoiceExtension(BaseBot):
bool: True if the view was updated, False otherwise. bool: True if the view was updated, False otherwise.
""" """
logging.debug("[VC_EXT] Updating menu view") logging.debug("[VC_EXT] Updating menu view")
if not ctx.guild_id: if not ctx.guild_id:
logging.warning("[VC_EXT] Guild ID not found in context inside 'update_menu_view'") logging.warning("[VC_EXT] Guild ID not found in context")
return False return False
if not menu_message: guild = await self.db.get_guild(ctx.guild_id, projection={'current_menu': 1})
guild = await self.db.get_guild(ctx.guild_id, projection={'current_menu': 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 guild['current_menu']:
logging.warning("[VC_EXT] Current menu not found in guild data")
if not menu_message:
return False return False
await self.update_menu_views_dict(ctx, disable=disable) if ctx.guild_id not in self.menu_views:
logging.debug("[VC_EXT] Creating new menu view")
await self.init_menu_view(ctx, ctx.guild_id, disable=disable)
view = self.menu_views[ctx.guild_id]
await view.update(disable=disable)
try: try:
if isinstance(ctx, Interaction) and button_callback: if isinstance(ctx, Interaction) and button_callback:
# If interaction from menu buttons # If interaction from menu buttons
await ctx.edit(view=self.menu_views[ctx.guild_id]) await ctx.edit(view=view)
else: else:
# If interaction from other buttons or commands. They should have their own response. # If interaction from other buttons or commands. They should have their own response.
await menu_message.edit(view=self.menu_views[ctx.guild_id]) if (menu_message := await self.get_menu_message(ctx, guild['current_menu'])):
await menu_message.edit(view=view)
except discord.DiscordException as e: except discord.DiscordException as e:
logging.warning(f"[VC_EXT] Error while updating menu view: {e}") logging.warning(f"[VC_EXT] Error while updating menu view: {e}")
return False return False
@@ -411,7 +409,6 @@ class VoiceExtension(BaseBot):
track: Track | dict[str, Any], track: Track | dict[str, Any],
*, *,
vc: discord.VoiceClient | None = None, vc: discord.VoiceClient | None = None,
menu_message: discord.Message | None = None,
button_callback: bool = False, button_callback: bool = False,
) -> str | None: ) -> str | None:
"""Play `track` in the voice channel. Avoids additional vibe feedback used in `next_track` and `previous_track`. """Play `track` in the voice channel. Avoids additional vibe feedback used in `next_track` and `previous_track`.
@@ -421,7 +418,6 @@ class VoiceExtension(BaseBot):
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context. ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
track (dict[str, Any]): Track to play. track (dict[str, Any]): Track to play.
vc (discord.VoiceClient | None, optional): Voice client. Defaults to None. 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. button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False.
Returns: Returns:
@@ -444,7 +440,6 @@ class VoiceExtension(BaseBot):
ctx, ctx,
track, track,
vc=vc, vc=vc,
menu_message=menu_message,
button_callback=button_callback button_callback=button_callback
) )
@@ -501,7 +496,6 @@ class VoiceExtension(BaseBot):
vc: discord.VoiceClient | None = None, vc: discord.VoiceClient | None = None,
*, *,
after: bool = False, after: bool = False,
menu_message: discord.Message | None = None,
button_callback: bool = False button_callback: bool = False
) -> str | None: ) -> str | None:
"""Switch to the next track in the queue. Return track title on success. """Switch to the next track in the queue. Return track title on success.
@@ -524,9 +518,12 @@ class VoiceExtension(BaseBot):
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'next_track'") logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'next_track'")
return None return None
guild = await self.db.get_guild(ctx.guild_id, projection={'shuffle': 1, 'repeat': 1, 'is_stopped': 1, 'current_menu': 1, 'vibing': 1, 'current_track': 1}) guild = await self.db.get_guild(ctx.guild_id, projection={
'shuffle': 1, 'repeat': 1, 'is_stopped': 1,
'current_menu': 1, 'vibing': 1, 'current_track': 1
})
if guild['is_stopped'] and after: if after and guild['is_stopped']:
logging.debug("[VC_EXT] Playback is stopped, skipping after callback.") logging.debug("[VC_EXT] Playback is stopped, skipping after callback.")
return None return None
@@ -534,8 +531,9 @@ class VoiceExtension(BaseBot):
logging.debug("[VC_EXT] Adding current track to history") logging.debug("[VC_EXT] Adding current track to history")
await self.db.modify_track(ctx.guild_id, guild['current_track'], 'previous', 'insert') await self.db.modify_track(ctx.guild_id, guild['current_track'], 'previous', 'insert')
if after and not await self.update_menu_view(ctx, menu_message=menu_message, disable=True): if after and guild['current_menu']:
await self.respond(ctx, "error", "Не удалось обновить меню.", ephemeral=True, delete_after=15) if not await self.update_menu_view(ctx, button_callback=button_callback, disable=True):
await self.respond(ctx, "error", "Не удалось обновить меню.", ephemeral=True, delete_after=15)
if guild['vibing'] and guild['current_track']: if guild['vibing'] and guild['current_track']:
await self.send_vibe_feedback(ctx, 'trackFinished' if after else 'skip', guild['current_track']) await self.send_vibe_feedback(ctx, 'trackFinished' if after else 'skip', guild['current_track'])
@@ -570,6 +568,9 @@ class VoiceExtension(BaseBot):
logging.info("[VC_EXT] No next track found") logging.info("[VC_EXT] No next track found")
if after: if after:
await self.db.update(ctx.guild_id, {'is_stopped': True, 'current_track': None}) await self.db.update(ctx.guild_id, {'is_stopped': True, 'current_track': None})
if guild['current_menu']:
await self.update_menu_view(ctx, button_callback=button_callback)
return None return None
@@ -854,7 +855,6 @@ class VoiceExtension(BaseBot):
track: Track, track: Track,
*, *,
vc: discord.VoiceClient | None = None, vc: discord.VoiceClient | None = None,
menu_message: discord.Message | None = None,
button_callback: bool = False, button_callback: bool = False,
retry: bool = False retry: bool = False
) -> str | None: ) -> str | None:
@@ -865,7 +865,6 @@ class VoiceExtension(BaseBot):
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context. ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
track (Track): Track to play. track (Track): Track to play.
vc (discord.VoiceClient | None): Voice client. vc (discord.VoiceClient | None): Voice client.
menu_message (discord.Message | None): Menu message. If None, fetches menu from channel using message id from database. Defaults to None.
button_callback (bool): Should be True if the function is being called from button callback. Defaults to False. button_callback (bool): Should be True if the function is being called from button callback. Defaults to False.
retry (bool): Whether the function is called again. retry (bool): Whether the function is called again.
@@ -887,7 +886,7 @@ class VoiceExtension(BaseBot):
await self._download_track(ctx.guild_id, track) await self._download_track(ctx.guild_id, track)
except yandex_music.exceptions.TimedOutError: except yandex_music.exceptions.TimedOutError:
if not retry: if not retry:
return await self._play_track(ctx, track, vc=vc, menu_message=menu_message, button_callback=button_callback, retry=True) return await self._play_track(ctx, track, vc=vc, button_callback=button_callback, retry=True)
await self.respond(ctx, "error", "Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15) await self.respond(ctx, "error", "Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15)
logging.error(f"[VC_EXT] Failed to download track '{track.title}'") logging.error(f"[VC_EXT] Failed to download track '{track.title}'")
@@ -904,9 +903,8 @@ class VoiceExtension(BaseBot):
await self.db.set_current_track(ctx.guild_id, track) await self.db.set_current_track(ctx.guild_id, track)
if menu_message or guild['current_menu']: if guild['current_menu']:
# Updating menu message before playing to prevent delay and avoid FFMPEG lags. await self.update_menu_embed_and_view(ctx, button_callback=button_callback)
await self.update_menu_embed_and_view(ctx, menu_message=menu_message, button_callback=button_callback)
if not guild['vibing']: if not guild['vibing']:
# Giving FFMPEG enough time to process the audio file # Giving FFMPEG enough time to process the audio file

View File

@@ -13,13 +13,14 @@ from yandex_music import TrackLyrics, Playlist, ClientAsync as YMClient
from MusicBot.cogs.utils import VoiceExtension from MusicBot.cogs.utils import VoiceExtension
class ToggleButton(Button, VoiceExtension): class ToggleButton(Button, VoiceExtension):
def __init__(self, *args, **kwargs): def __init__(self, root: 'MenuView', *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
self.root = root
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
callback_type = interaction.custom_id
if callback_type not in ('repeat', 'shuffle'): if (callback_type := interaction.custom_id) not in ('repeat', 'shuffle'):
raise ValueError(f"Invalid callback type: '{callback_type}'") raise ValueError(f"Invalid callback type: '{callback_type}'")
logging.info(f'[MENU] {callback_type.capitalize()} button callback') logging.info(f'[MENU] {callback_type.capitalize()} button callback')
@@ -62,8 +63,10 @@ class ToggleButton(Button, VoiceExtension):
await self.db.update(gid, {callback_type: not guild[callback_type]}) await self.db.update(gid, {callback_type: not guild[callback_type]})
if not await self.update_menu_view(interaction, button_callback=True): button = self.root.repeat_button if callback_type == 'repeat' else self.root.shuffle_button
await self.respond(interaction, "error", "Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True) button.style = ButtonStyle.secondary if guild[callback_type] else ButtonStyle.success
await interaction.edit(view=await self.root.update())
class PlayPauseButton(Button, VoiceExtension): class PlayPauseButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -109,6 +112,11 @@ class PlayPauseButton(Button, VoiceExtension):
) )
return return
if vc.is_paused():
vc.resume()
else:
vc.pause()
try: try:
embed = interaction.message.embeds[0] embed = interaction.message.embeds[0]
except IndexError: except IndexError:
@@ -116,16 +124,19 @@ class PlayPauseButton(Button, VoiceExtension):
return return
guild = await self.db.get_guild(interaction.guild_id, projection={'single_token_uid': 1}) guild = await self.db.get_guild(interaction.guild_id, projection={'single_token_uid': 1})
if vc.is_paused(): if not vc.is_paused() and guild['single_token_uid']:
vc.resume() user = await self.get_discord_user_by_id(interaction, guild['single_token_uid'])
if guild['single_token_uid'] and (user := await self.get_discord_user_by_id(interaction, guild['single_token_uid'])):
if guild['single_token_uid'] and user:
embed.set_footer(text=f"Используется токен {user.display_name}", icon_url=user.display_avatar.url) embed.set_footer(text=f"Используется токен {user.display_name}", icon_url=user.display_avatar.url)
else: else:
embed.remove_footer() embed.set_footer(text='Используется токен (неизвестный пользователь)')
else:
vc.pause() elif vc.is_paused():
embed.set_footer(text='Приостановлено') embed.set_footer(text='Приостановлено')
else:
embed.remove_footer()
await interaction.edit(embed=embed) await interaction.edit(embed=embed)
@@ -135,8 +146,8 @@ class SwitchTrackButton(Button, VoiceExtension):
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
callback_type = interaction.custom_id
if callback_type not in ('next', 'previous'): if (callback_type := interaction.custom_id) not in ('next', 'previous'):
raise ValueError(f"Invalid callback type: '{callback_type}'") raise ValueError(f"Invalid callback type: '{callback_type}'")
if not (gid := interaction.guild_id) or not interaction.user: if not (gid := interaction.guild_id) or not interaction.user:
@@ -191,9 +202,10 @@ class SwitchTrackButton(Button, VoiceExtension):
await self.respond(interaction, "error", "Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
class ReactionButton(Button, VoiceExtension): class ReactionButton(Button, VoiceExtension):
def __init__(self, *args, **kwargs): def __init__(self, root: 'MenuView', *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
self.root = root
async def callback(self, interaction: Interaction): async def callback(self, interaction: Interaction):
callback_type = interaction.custom_id callback_type = interaction.custom_id
@@ -212,30 +224,28 @@ class ReactionButton(Button, VoiceExtension):
res = await self.react_track(interaction, callback_type) res = await self.react_track(interaction, callback_type)
if callback_type == 'like' and res[0]: if callback_type == 'like' and res[0]:
await self.update_menu_views_dict(interaction) button = self.root.like_button
await interaction.edit(view=self.menu_views[gid]) response_message = f"Трек был {'добавлен в понравившиеся.' if res[1] == 'added' else 'удалён из понравившихся.'}"
await self.respond(
interaction, "success",
f"Трек был {'добавлен в понравившиеся.' if res[1] == 'added' else 'удалён из понравившихся.'}",
delete_after=15, ephemeral=True
)
elif callback_type == 'dislike' and res[0]: elif callback_type == 'dislike' and res[0]:
if len(channel.members) == 2 and not await self.play_next_track(interaction, vc=vc, button_callback=True): if len(channel.members) == 2:
await self.respond(interaction, "info", "Воспроизведение приостановлено. Нет треков в очереди.", delete_after=15) await self.play_next_track(interaction, vc=vc, button_callback=True)
return
await self.update_menu_views_dict(interaction) button = self.root.dislike_button
await interaction.edit(view=self.menu_views[gid]) response_message =f"Трек был {'добавлен в дизлайки.' if res[1] == 'added' else 'удалён из дизлайков.'}"
await self.respond(
interaction, "success",
f"Трек был {'добавлен в дизлайки.' if res[1] == 'added' else 'удалён из дизлайков.'}",
delete_after=15, ephemeral=True
)
else: else:
logging.debug(f"[VC_EXT] Failed to get {callback_type} tracks") logging.debug(f"[VC_EXT] Failed to get {callback_type} tracks")
await self.respond(interaction, "error", "Операция не удалась. Попробуйте позже.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Операция не удалась. Попробуйте позже.", delete_after=15, ephemeral=True)
return
if len(channel.members) == 2:
button.style = ButtonStyle.success if res[1] == 'added' else ButtonStyle.secondary
await interaction.edit(view=await self.root.update())
else:
await self.respond(interaction, "success", response_message, delete_after=15, ephemeral=True)
async def react_track( async def react_track(
self, self,
@@ -586,8 +596,7 @@ class AddToPlaylistButton(Button, VoiceExtension):
if not await self.voice_check(interaction) or not interaction.guild_id: if not await self.voice_check(interaction) or not interaction.guild_id:
return return
current_track = await self.db.get_track(interaction.guild_id, 'current') if not await self.db.get_track(interaction.guild_id, 'current'):
if not current_track:
await self.respond(interaction, "error", "Нет воспроизводимого трека.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
return return
@@ -599,8 +608,7 @@ class AddToPlaylistButton(Button, VoiceExtension):
await self.respond(interaction, "error", "Нет воспроизводимого трека.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
return return
playlists = await client.users_playlists_list() if not (playlists := await client.users_playlists_list()):
if not playlists:
await self.respond(interaction, "error", "У вас нет плейлистов.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "У вас нет плейлистов.", delete_after=15, ephemeral=True)
return return
@@ -628,39 +636,58 @@ class MenuView(View, VoiceExtension):
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
self.ctx = ctx self.ctx = ctx
self.repeat_button = ToggleButton(style=ButtonStyle.secondary, emoji='🔂', row=0, custom_id='repeat') self.repeat_button = ToggleButton(self, style=ButtonStyle.secondary, emoji='🔂', row=0, custom_id='repeat')
self.shuffle_button = ToggleButton(style=ButtonStyle.secondary, emoji='🔀', row=0, custom_id='shuffle') self.shuffle_button = ToggleButton(self, style=ButtonStyle.secondary, emoji='🔀', row=0, custom_id='shuffle')
self.play_pause_button = PlayPauseButton(style=ButtonStyle.primary, emoji='', row=0) self.play_pause_button = PlayPauseButton(style=ButtonStyle.primary, emoji='', row=0)
self.next_button = SwitchTrackButton(style=ButtonStyle.primary, emoji='', row=0, custom_id='next') self.next_button = SwitchTrackButton(style=ButtonStyle.primary, emoji='', row=0, custom_id='next')
self.prev_button = SwitchTrackButton(style=ButtonStyle.primary, emoji='', row=0, custom_id='previous') self.prev_button = SwitchTrackButton(style=ButtonStyle.primary, emoji='', row=0, custom_id='previous')
self.like_button = ReactionButton(style=ButtonStyle.secondary, emoji='❤️', row=1, custom_id='like') self.like_button = ReactionButton(self, style=ButtonStyle.secondary, emoji='❤️', row=1, custom_id='like')
self.dislike_button = ReactionButton(style=ButtonStyle.secondary, emoji='💔', row=1, custom_id='dislike') self.dislike_button = ReactionButton(self, style=ButtonStyle.secondary, emoji='💔', row=1, custom_id='dislike')
self.lyrics_button = LyricsButton(style=ButtonStyle.secondary, emoji='📋', row=1) self.lyrics_button = LyricsButton(style=ButtonStyle.secondary, emoji='📋', row=1)
self.add_to_playlist_button = AddToPlaylistButton(style=ButtonStyle.secondary, emoji='📁', row=1) self.add_to_playlist_button = AddToPlaylistButton(style=ButtonStyle.secondary, emoji='📁', row=1)
self.vibe_button = MyVibeButton(style=ButtonStyle.secondary, emoji='🌊', row=1) self.vibe_button = MyVibeButton(style=ButtonStyle.secondary, emoji='🌊', row=1)
self.vibe_settings_button = MyVibeSettingsButton(style=ButtonStyle.success, emoji='🛠', row=1) self.vibe_settings_button = MyVibeSettingsButton(style=ButtonStyle.success, emoji='🛠', row=1)
self.current_vibe_button: MyVibeButton | MyVibeSettingsButton = self.vibe_button
async def init(self, *, disable: bool = False) -> Self: async def init(self, *, disable: bool = False) -> Self:
if not self.ctx.guild_id: await self.update(disable=disable)
return self
self.guild = await self.db.get_guild(self.ctx.guild_id, projection={
'repeat': 1, 'shuffle': 1, 'current_track': 1, 'current_menu': 1, 'vibing': 1, 'single_token_uid': 1
})
if self.guild['repeat']:
self.repeat_button.style = ButtonStyle.success
if self.guild['shuffle']:
self.shuffle_button.style = ButtonStyle.success
current_track = self.guild['current_track']
self.add_item(self.repeat_button) self.add_item(self.repeat_button)
self.add_item(self.prev_button) self.add_item(self.prev_button)
self.add_item(self.play_pause_button) self.add_item(self.play_pause_button)
self.add_item(self.next_button) self.add_item(self.next_button)
self.add_item(self.shuffle_button) self.add_item(self.shuffle_button)
self.add_item(self.like_button)
self.add_item(self.dislike_button)
self.add_item(self.lyrics_button)
self.add_item(self.add_to_playlist_button)
self.add_item(self.current_vibe_button)
return self
async def update(self, *, disable: bool = False) -> Self:
if not self.ctx.guild_id:
return self
self.enable_all_items()
self.guild = await self.db.get_guild(self.ctx.guild_id, projection={
'repeat': 1, 'shuffle': 1, 'current_track': 1, 'current_viber_id': 1, 'vibing': 1, 'single_token_uid': 1
})
if self.guild['repeat']:
self.repeat_button.style = ButtonStyle.success
else:
self.repeat_button.style = ButtonStyle.secondary
if self.guild['shuffle']:
self.shuffle_button.style = ButtonStyle.success
else:
self.shuffle_button.style = ButtonStyle.secondary
current_track = self.guild['current_track']
if not isinstance(self.ctx, RawReactionActionEvent) \ if not isinstance(self.ctx, RawReactionActionEvent) \
and len(cast(VoiceChannel, self.ctx.channel).members) == 2 \ and len(cast(VoiceChannel, self.ctx.channel).members) == 2 \
@@ -668,9 +695,17 @@ class MenuView(View, VoiceExtension):
if current_track and str(current_track['id']) in [str(like.id) for like in await self.get_reacted_tracks(self.ctx, 'like')]: if current_track and str(current_track['id']) in [str(like.id) for like in await self.get_reacted_tracks(self.ctx, 'like')]:
self.like_button.style = ButtonStyle.success self.like_button.style = ButtonStyle.success
else:
self.like_button.style = ButtonStyle.secondary
if current_track and str(current_track['id']) in [str(dislike.id) for dislike in await self.get_reacted_tracks(self.ctx, 'dislike')]: if current_track and str(current_track['id']) in [str(dislike.id) for dislike in await self.get_reacted_tracks(self.ctx, 'dislike')]:
self.dislike_button.style = ButtonStyle.success self.dislike_button.style = ButtonStyle.success
else:
self.dislike_button.style = ButtonStyle.secondary
else:
self.like_button.style = ButtonStyle.secondary
self.dislike_button.style = ButtonStyle.secondary
if not current_track: if not current_track:
self.lyrics_button.disabled = True self.lyrics_button.disabled = True
@@ -679,32 +714,27 @@ class MenuView(View, VoiceExtension):
self.add_to_playlist_button.disabled = True self.add_to_playlist_button.disabled = True
elif not current_track['lyrics_available']: elif not current_track['lyrics_available']:
self.lyrics_button.disabled = True self.lyrics_button.disabled = True
if self.guild['single_token_uid']: if self.guild['single_token_uid']:
self.like_button.disabled = True self.like_button.disabled = True
self.dislike_button.disabled = True self.dislike_button.disabled = True
self.add_to_playlist_button.disabled = True self.add_to_playlist_button.disabled = True
self.add_item(self.like_button)
self.add_item(self.dislike_button)
self.add_item(self.lyrics_button)
self.add_item(self.add_to_playlist_button)
if self.guild['vibing']: if self.guild['vibing']:
self.add_item(self.vibe_settings_button) self.current_vibe_button = self.vibe_settings_button
else: else:
self.add_item(self.vibe_button) self.current_vibe_button = self.vibe_button
if disable: if disable:
self.disable_all_items() self.disable_all_items()
return self return self
async def on_timeout(self) -> None: async def on_timeout(self) -> None:
logging.debug('[MENU] Menu timed out. Deleting menu message') logging.debug('[MENU] Menu timed out. Deleting menu message')
if not self.ctx.guild_id: if not self.ctx.guild_id:
return return
if self.guild['current_menu']: if self.guild['current_menu']:
await self.db.update(self.ctx.guild_id, { await self.db.update(self.ctx.guild_id, {
'current_menu': None, 'repeat': False, 'shuffle': False, 'current_menu': None, 'repeat': False, 'shuffle': False,
@@ -718,4 +748,4 @@ class MenuView(View, VoiceExtension):
else: else:
logging.debug('[MENU] No menu message found') logging.debug('[MENU] No menu message found')
self.stop() self.stop()