feat: Add "My Vibe" button to menu and enhance interactions.

This commit is contained in:
Lemon4ksan
2025-01-27 23:32:08 +03:00
parent c49ff949cf
commit c353de429f
8 changed files with 332 additions and 153 deletions

View File

@@ -245,7 +245,6 @@ class General(Cog):
logging.info(f"Successfully fetched playlists for user {ctx.user.id}") logging.info(f"Successfully fetched playlists for user {ctx.user.id}")
await ctx.respond(embed=embed, view=MyPlaylists(ctx), ephemeral=True) await ctx.respond(embed=embed, view=MyPlaylists(ctx), ephemeral=True)
discord.Option
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.") @discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
@discord.option( @discord.option(
"тип", "тип",

View File

@@ -58,6 +58,10 @@ def _generate_likes_embed(tracks: list[Track]) -> Embed:
duration_m = duration // 60000 duration_m = duration // 60000
duration_s = ceil(duration / 1000) - duration_m * 60 duration_s = ceil(duration / 1000) - duration_m * 60
if duration_s == 60:
duration_m += 1
duration_s = 0
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}") embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
if track_count is not None: if track_count is not None:
@@ -89,6 +93,9 @@ async def _generate_track_embed(track: Track) -> Embed:
duration_m = duration // 60000 duration_m = duration // 60000
duration_s = ceil(duration / 1000) - duration_m * 60 duration_s = ceil(duration / 1000) - duration_m * 60
if duration_s == 60:
duration_m += 1
duration_s = 0
artist_url = f"https://music.yandex.ru/artist/{artist.id}" artist_url = f"https://music.yandex.ru/artist/{artist.id}"
artist_cover = artist.cover artist_cover = artist.cover
@@ -184,6 +191,9 @@ async def _generate_album_embed(album: Album) -> Embed:
if duration: if duration:
duration_m = duration // 60000 duration_m = duration // 60000
duration_s = ceil(duration / 1000) - duration_m * 60 duration_s = ceil(duration / 1000) - duration_m * 60
if duration_s == 60:
duration_m += 1
duration_s = 0
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}") embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
if track_count is not None: if track_count is not None:
@@ -291,6 +301,9 @@ async def _generate_playlist_embed(playlist: Playlist) -> Embed:
if duration: if duration:
duration_m = duration // 60000 duration_m = duration // 60000
duration_s = ceil(duration / 1000) - duration_m * 60 duration_s = ceil(duration / 1000) - duration_m * 60
if duration_s == 60:
duration_m += 1
duration_s = 0
embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}") embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}")
if track_count is not None: if track_count is not None:

View File

@@ -12,6 +12,8 @@ from discord import Interaction, ApplicationContext, RawReactionActionEvent
from MusicBot.cogs.utils import generate_item_embed from MusicBot.cogs.utils import generate_item_embed
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
# TODO: RawReactionActionEvent is poorly supported.
class VoiceExtension: class VoiceExtension:
def __init__(self, bot: discord.Bot | None) -> None: def __init__(self, bot: discord.Bot | None) -> None:
@@ -21,23 +23,21 @@ class VoiceExtension:
async def send_menu_message(self, ctx: ApplicationContext | Interaction) -> None: async def send_menu_message(self, ctx: ApplicationContext | Interaction) -> None:
from MusicBot.ui import MenuView from MusicBot.ui import MenuView
logging.info(f"Sending player menu") logging.info("[VC] Sending player menu")
if not ctx.guild: if not ctx.guild:
logging.warning("Guild not found in context inside 'create_menu'") logging.warning("[VC] Guild not found in context inside 'create_menu'")
return return
guild = self.db.get_guild(ctx.guild.id) guild = self.db.get_guild(ctx.guild.id)
embed = None embed = None
if guild['current_track']: if guild['current_track']:
embed = await generate_item_embed( track = cast(Track, Track.de_json(
Track.de_json( guild['current_track'],
guild['current_track'], client=YMClient() # type: ignore # Async client can be used here.
client=YMClient() # type: ignore # Async client can be used here. ))
), embed = await generate_item_embed(track, guild['vibing'])
guild['vibing']
)
vc = await self.get_voice_client(ctx) vc = await self.get_voice_client(ctx)
if vc and vc.is_paused(): if vc and vc.is_paused():
embed.set_footer(text='Приостановлено') embed.set_footer(text='Приостановлено')
@@ -45,7 +45,7 @@ class VoiceExtension:
embed.remove_footer() embed.remove_footer()
if guild['current_menu']: if guild['current_menu']:
logging.info(f"Deleteing old player menu {guild['current_menu']} in guild {ctx.guild.id}") logging.info(f"[VC] Deleting old player menu {guild['current_menu']} in guild {ctx.guild.id}")
message = await self.get_menu_message(ctx, guild['current_menu']) message = await self.get_menu_message(ctx, guild['current_menu'])
if message: if message:
await message.delete() await message.delete()
@@ -54,19 +54,60 @@ class VoiceExtension:
response = await interaction.original_response() response = await interaction.original_response()
self.db.update(ctx.guild.id, {'current_menu': response.id}) self.db.update(ctx.guild.id, {'current_menu': response.id})
logging.info(f"New player menu {response.id} created in guild {ctx.guild.id}") logging.info(f"[VC] New player menu {response.id} created in guild {ctx.guild.id}")
async def get_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, player_mid: int) -> discord.Message | None:
"""Fetch the player message by its id. Return the message if found, None if not.
Reset `current_menu` field in the database if not found.
Args:
ctx (ApplicationContext | Interaction): Context.
player_mid (int): Id of the player message.
Returns:
discord.Message | None: Player message or None.
"""
logging.debug(f"[VC] Fetching player message {player_mid}...")
if not ctx.guild_id:
logging.warning("[VC] Guild ID not found in context")
return None
try:
if isinstance(ctx, Interaction):
player = ctx.client.get_message(player_mid)
elif isinstance(ctx, RawReactionActionEvent):
if not self.bot:
raise ValueError("Bot instance is not set.")
player = self.bot.get_message(player_mid)
elif isinstance(ctx, ApplicationContext):
player = await ctx.fetch_message(player_mid)
else:
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
except discord.DiscordException as e:
logging.debug(f"[VC] Failed to get player message: {e}")
self.db.update(ctx.guild_id, {'current_menu': None})
return None
if player:
logging.debug("[VC] Player message found")
else:
logging.debug("[VC] Player message not found. Resetting current_menu field.")
self.db.update(ctx.guild_id, {'current_menu': None})
return player
async def update_menu_embed( async def update_menu_embed(
self, self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent, ctx: ApplicationContext | Interaction | RawReactionActionEvent,
player_mid: int, menu_mid: int,
button_callback: bool = False button_callback: bool = False
) -> bool: ) -> bool:
"""Update current player message by its id. Return True if updated, False if not. """Update current player message by its id. Return True if updated, False if not.
Args: Args:
ctx (ApplicationContext | Interaction): Context. ctx (ApplicationContext | Interaction): Context.
player_mid (int): Id of the player message. There can only be only one player in the guild. menu_mid (int): Id of the player message. There can only be only one player in the guild.
button_callback (bool, optional): If True, the interaction is a button interaction. Defaults to False. button_callback (bool, optional): If True, the interaction is a button interaction. Defaults to False.
Returns: Returns:
@@ -74,32 +115,33 @@ class VoiceExtension:
""" """
from MusicBot.ui import MenuView from MusicBot.ui import MenuView
logging.debug( logging.debug(
f"Updating player embed using " + ( f"[VC] Updating player embed using " + (
"interaction context" if isinstance(ctx, Interaction) else "interaction context" if isinstance(ctx, Interaction) else
"application context" if isinstance(ctx, ApplicationContext) else "application context" if isinstance(ctx, ApplicationContext) else
"raw reaction context") "raw reaction context"
)
) )
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid: if not gid or not uid:
logging.warning("Guild ID or User ID not found in context inside 'update_player_embed'") logging.warning("[VC] Guild ID or User ID not found in context inside 'update_player_embed'")
return False return False
player = await self.get_menu_message(ctx, player_mid) player = await self.get_menu_message(ctx, menu_mid)
if not player: if not player:
return False return False
token = self.users_db.get_ym_token(uid) token = self.users_db.get_ym_token(uid)
if not token: if not token:
logging.debug(f"No token found for user {uid}") logging.debug(f"[VC] No token found for user {uid}")
return False return False
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
current_track = guild['current_track'] current_track = guild['current_track']
if not current_track: if not current_track:
logging.debug("No current track found") logging.debug("[VC] No current track found")
return False return False
track = cast(Track, Track.de_json( track = cast(Track, Track.de_json(
@@ -117,56 +159,54 @@ class VoiceExtension:
# 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 player.edit(embed=embed, view=await MenuView(ctx).init()) await player.edit(embed=embed, view=await MenuView(ctx).init())
except discord.NotFound: except discord.NotFound:
logging.warning("[VC] Player message not found")
return False return False
return True return True
async def update_vibe( async def update_vibe(
self, ctx: ApplicationContext | Interaction, self,
type: Literal['track', 'album', 'artist', 'playlist', 'user'] | None = None, ctx: ApplicationContext | Interaction,
id: str | int | None = None, type: Literal['track', 'album', 'artist', 'playlist', 'user'],
id: str | int,
*,
button_callback: bool = False button_callback: bool = False
) -> str | None: ) -> str | None:
"""Get next vibe track. Return track title on success. If type or id is None, user's vibe will be used. """Update vibe state. Return track title on success.
Args: Args:
ctx (ApplicationContext | Interaction): Context. ctx (ApplicationContext | Interaction): Context.
type (Literal['track', 'album', 'artist', 'playlist', 'user'] | None, optional): Type of the item. Defaults to None. type (Literal['track', 'album', 'artist', 'playlist', 'user']): Type of the item.
id (str | int | Literal['onyourwave'] | None, optional): ID of the item. Defaults to None. id (str | int): ID of the item.
button_callback (bool, optional): If the function is called from button callback. Defaults to False. button_callback (bool, optional): If the function is called from button callback. Defaults to False.
Returns: Returns:
str | None: Track title or None. str | None: Track title or None.
""" """
logging.info(f"Updating vibe for guild {ctx.guild_id} with type '{type}' and id '{id}'") logging.info(f"[VC] Updating vibe for guild {ctx.guild_id} with type '{type}' and id '{id}'")
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not uid or not gid: if not uid or not gid:
logging.warning("Guild ID or User ID not found in context inside 'vibe_update'") logging.warning("[VC] Guild ID or User ID not found in context inside 'vibe_update'")
return None return None
token = self.users_db.get_ym_token(uid) token = self.users_db.get_ym_token(uid)
if not token: if not token:
logging.info(f"User {uid} has no YM token") logging.info(f"[VC] User {uid} has no YM token")
await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True) await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True)
return return
try: try:
client = await YMClient(token).init() client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError: except yandex_music.exceptions.UnauthorizedError:
logging.info(f"User {uid} provided invalid token") logging.info(f"[VC] User {uid} provided invalid token")
await ctx.respond('❌ Недействительный токен.') await ctx.respond('❌ Недействительный токен.')
return return
if type and id: self.users_db.update(uid, {'vibe_type': type, 'vibe_id': id})
self.users_db.update(uid, {'vibe_type': type, 'vibe_id': id})
else:
logging.info(f"[VIBE] Using user's vibe for guild {gid}")
type = 'user'
id = 'onyourwave'
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
if not guild['vibing']: if not guild['vibing']:
feedback = await client.rotor_station_feedback_radio_started( feedback = await client.rotor_station_feedback_radio_started(
f"{type}:{id}", f"{type}:{id}",
@@ -175,9 +215,7 @@ class VoiceExtension:
) )
logging.debug(f"[VIBE] Radio started feedback: {feedback}") logging.debug(f"[VIBE] Radio started feedback: {feedback}")
tracks = await client.rotor_station_tracks( tracks = await client.rotor_station_tracks(f"{type}:{id}")
f"{type}:{id}"
)
self.db.update(gid, {'vibing': True}) self.db.update(gid, {'vibing': True})
elif guild['current_track']: elif guild['current_track']:
tracks = await client.rotor_station_tracks( tracks = await client.rotor_station_tracks(
@@ -197,50 +235,12 @@ class VoiceExtension:
next_tracks = [cast(Track, track.track) for track in tracks.sequence] next_tracks = [cast(Track, track.track) for track in tracks.sequence]
self.db.update(gid, {'next_tracks': [track.to_dict() for track in next_tracks[1:]]}) self.db.update(gid, {
'next_tracks': [track.to_dict() for track in next_tracks[1:]],
'current_viber_id': uid
})
await self.stop_playing(ctx) await self.stop_playing(ctx)
return await self.play_track(ctx, next_tracks[0], button_callback=button_callback) return await self.play_track(ctx, next_tracks[0], button_callback=button_callback)
async def get_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, player_mid: int) -> discord.Message | None:
"""Fetch the player message by its id. Return the message if found, None if not.
Reset `current_menu` field in the database if not found.
Args:
ctx (ApplicationContext | Interaction): Context.
player_mid (int): Id of the player message.
Returns:
discord.Message | None: Player message or None.
"""
logging.debug(f"Fetching player message {player_mid}...")
if not ctx.guild_id:
logging.warning("Guild ID not found in context")
return None
try:
if isinstance(ctx, Interaction):
player = ctx.client.get_message(player_mid)
elif isinstance(ctx, RawReactionActionEvent):
if not self.bot:
raise ValueError("Bot instance is not set.")
player = self.bot.get_message(player_mid)
elif isinstance(ctx, ApplicationContext):
player = await ctx.fetch_message(player_mid)
else:
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
except discord.DiscordException as e:
logging.debug(f"Failed to get player message: {e}")
self.db.update(ctx.guild_id, {'current_menu': None})
return None
if player:
logging.debug(f"Player message found")
else:
logging.debug("Player message not found. Resetting current_menu field.")
self.db.update(ctx.guild_id, {'current_menu': None})
return player
async def voice_check(self, ctx: ApplicationContext | Interaction) -> bool: async def voice_check(self, ctx: ApplicationContext | Interaction) -> bool:
"""Check if bot can perform voice tasks and respond if failed. """Check if bot can perform voice tasks and respond if failed.
@@ -251,33 +251,36 @@ class VoiceExtension:
Returns: Returns:
bool: Check result. bool: Check result.
""" """
if not ctx.user: if not ctx.user or not ctx.guild:
logging.warning("User not found in context inside 'voice_check'") logging.warning("[VC] User or guild not found in context inside 'voice_check'")
return False return False
token = self.users_db.get_ym_token(ctx.user.id) token = self.users_db.get_ym_token(ctx.user.id)
if not token: if not token:
logging.debug(f"No token found for user {ctx.user.id}") logging.debug(f"[VC] No token found for user {ctx.user.id}")
await ctx.respond("❌ Необходимо указать свой токен доступа с помощью команды /login.", delete_after=15, ephemeral=True) await ctx.respond("❌ Необходимо указать свой токен доступа с помощью команды /login.", delete_after=15, ephemeral=True)
return False return False
channel = ctx.channel if not isinstance(ctx.channel, discord.VoiceChannel):
if not isinstance(channel, discord.VoiceChannel): logging.debug("[VC] User is not in a voice channel")
logging.debug("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 return False
if isinstance(ctx, Interaction): voice_clients = ctx.client.voice_clients if isinstance(ctx, Interaction) else ctx.bot.voice_clients
channels = ctx.client.voice_clients voice_chat = discord.utils.get(voice_clients, guild=ctx.guild)
else:
channels = ctx.bot.voice_clients
voice_chat = discord.utils.get(channels, guild=ctx.guild)
if not voice_chat: if not voice_chat:
logging.debug("Voice client not found") logging.debug("[VC] Voice client not found")
await ctx.respond("❌ Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True) await ctx.respond("❌ Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True)
return False return False
logging.debug("Voice requirements met") guild = self.db.get_guild(ctx.guild.id)
member = cast(discord.Member, ctx.user)
if guild['vibing'] and ctx.user.id != guild['current_viber_id'] and not member.guild_permissions.manage_channels:
logging.debug("[VIBE] Context user is not the current viber")
await ctx.respond("❌ Вы не можете взаимодействовать с чужой волной!", delete_after=15, ephemeral=True)
return False
logging.debug("[VC] Voice requirements met")
return True return True
async def get_voice_client(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> discord.VoiceClient | None: async def get_voice_client(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> discord.VoiceClient | None:
@@ -290,25 +293,30 @@ class VoiceExtension:
discord.VoiceClient | None: Voice client or None. discord.VoiceClient | None: Voice client or None.
""" """
if isinstance(ctx, Interaction): if isinstance(ctx, Interaction):
voice_chat = discord.utils.get(ctx.client.voice_clients, guild=ctx.guild) voice_clients = ctx.client.voice_clients
guild = ctx.guild
elif isinstance(ctx, RawReactionActionEvent): elif isinstance(ctx, RawReactionActionEvent):
if not self.bot: if not self.bot:
raise ValueError("Bot instance is not set.") raise ValueError("Bot instance is not set.")
if not ctx.guild_id: if not ctx.guild_id:
logging.warning("Guild ID not found in context inside get_voice_client") logging.warning("[VC] Guild ID not found in context inside get_voice_client")
return None return None
voice_chat = discord.utils.get(self.bot.voice_clients, guild=await self.bot.fetch_guild(ctx.guild_id)) voice_clients = self.bot.voice_clients
guild = await self.bot.fetch_guild(ctx.guild_id)
elif isinstance(ctx, ApplicationContext): elif isinstance(ctx, ApplicationContext):
voice_chat = discord.utils.get(ctx.bot.voice_clients, guild=ctx.guild) voice_clients = ctx.bot.voice_clients
guild = ctx.guild
else: else:
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.") raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
if voice_chat: voice_chat = discord.utils.get(voice_clients, guild=guild)
logging.debug("Voice client found")
else:
logging.debug("Voice client not found")
return cast((discord.VoiceClient | None), voice_chat) if voice_chat:
logging.debug("[VC] Voice client found")
else:
logging.debug("[VC] Voice client not found")
return cast(discord.VoiceClient | None, voice_chat)
async def play_track( async def play_track(
self, self,
@@ -316,7 +324,9 @@ class VoiceExtension:
track: Track, track: Track,
*, *,
vc: discord.VoiceClient | None = None, vc: discord.VoiceClient | None = None,
button_callback: bool = False menu_message: discord.Message | None = None,
button_callback: bool = False,
retry: bool = False
) -> str | None: ) -> str | None:
"""Download ``track`` by its id and play it in the voice channel. Return track title on success. """Download ``track`` by its id and play it in the voice channel. Return track title on success.
If sound is already playing, add track id to the queue. There's no response to the context. If sound is already playing, add track id to the queue. There's no response to the context.
@@ -325,15 +335,19 @@ class VoiceExtension:
ctx (ApplicationContext | Interaction): Context ctx (ApplicationContext | Interaction): 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.
button_callback (bool): Whether the interaction is a button callback. button_callback (bool): Whether the interaction is a button callback.
retry (bool): Whether the function is called again.
Returns: Returns:
str | None: Song title or None. str | None: Song title or None.
""" """
from MusicBot.ui import MenuView
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid: if not gid or not uid:
logging.warning("Guild ID or User ID not found in context inside 'play_track'") logging.warning("[VC] Guild ID or User ID not found in context inside 'play_track'")
return None return None
if not vc: if not vc:
@@ -352,25 +366,36 @@ class VoiceExtension:
else: else:
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.") raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
self.db.set_current_track(gid, track)
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
if guild['current_menu'] and not isinstance(ctx, RawReactionActionEvent):
if menu_message:
try:
await menu_message.edit(embed=await generate_item_embed(track, guild['vibing']), view=await MenuView(ctx).init())
except discord.errors.NotFound:
logging.warning("[VC] Menu message not found. Using 'update_menu_embed' instead.")
await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback)
else:
await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback)
try: try:
await track.download_async(f'music/{gid}.mp3') await track.download_async(f'music/{gid}.mp3')
song = discord.FFmpegPCMAudio(f'music/{gid}.mp3', options='-vn -filter:a "volume=0.15"') song = discord.FFmpegPCMAudio(f'music/{gid}.mp3', options='-vn -filter:a "volume=0.15"')
except yandex_music.exceptions.TimedOutError: # Not sure why that happens. Probably should add timeout for buttons. except yandex_music.exceptions.TimedOutError: # sometimes track takes too long to download.
logging.warning(f"[VC] Timed out while downloading track '{track.title}'")
if not isinstance(ctx, RawReactionActionEvent) and ctx.user and ctx.channel: if not isinstance(ctx, RawReactionActionEvent) and ctx.user and ctx.channel:
channel = cast(discord.VoiceChannel, ctx.channel) channel = cast(discord.VoiceChannel, ctx.channel)
await channel.send(f"😔 {ctx.user.mention}, не удалось загрузить трек. Яндекс Музыка не отвечает или блокирует запросы.") if not retry:
channel = cast(discord.VoiceChannel, ctx.channel)
await channel.send(f"Не удалось загрузить трек. Пробуем заного...", delete_after=5)
return await self.play_track(ctx, track, vc=vc, button_callback=button_callback, retry=True)
await channel.send(f"😔 Снова не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15)
return None return None
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop)) vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop))
logging.info(f"Playing track '{track.title}'") logging.info(f"[VC] Playing track '{track.title}'")
self.db.set_current_track(gid, track)
self.db.update(gid, {'is_stopped': False}) self.db.update(gid, {'is_stopped': False})
player = guild['current_menu']
if player is not None:
await self.update_menu_embed(ctx, player, button_callback)
if guild['vibing']: if guild['vibing']:
user = self.users_db.get_user(uid) user = self.users_db.get_user(uid)
@@ -388,13 +413,13 @@ class VoiceExtension:
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
if not gid: if not gid:
logging.warning("Guild ID not found in context") logging.warning("[VC] Guild ID not found in context")
return return
if not vc: if not vc:
vc = await self.get_voice_client(ctx) vc = await self.get_voice_client(ctx)
if vc: if vc:
logging.debug("Stopping playback") logging.debug("[VC] Stopping playback")
self.db.update(gid, {'current_track': None, 'is_stopped': True}) self.db.update(gid, {'current_track': None, 'is_stopped': True})
vc.stop() vc.stop()
@@ -418,8 +443,12 @@ class VoiceExtension:
Returns: Returns:
str | None: Track title or None. str | None: Track title or None.
""" """
from MusicBot.ui import MenuView
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
menu_message = None
if not gid or not uid: if not gid or not uid:
logging.warning("Guild ID or User ID not found in context inside 'next_track'") logging.warning("Guild ID or User ID not found in context inside 'next_track'")
return None return None
@@ -430,7 +459,7 @@ class VoiceExtension:
if not token: if not token:
logging.debug(f"No token found for user {uid}") logging.debug(f"No token found for user {uid}")
return None return None
client = await YMClient(token).init() client = await YMClient(token).init()
if guild['is_stopped'] and after: if guild['is_stopped'] and after:
@@ -442,7 +471,16 @@ class VoiceExtension:
if not vc: # Silently return if bot got kicked if not vc: # Silently return if bot got kicked
return None return None
if after and guild['current_menu']:
menu_message = await self.get_menu_message(ctx, guild['current_menu'])
if menu_message:
await menu_message.edit(view=await MenuView(ctx).init(disable=True))
if guild['vibing'] and not isinstance(ctx, RawReactionActionEvent): if guild['vibing'] and not isinstance(ctx, RawReactionActionEvent):
if not user['vibe_type'] or not user['vibe_id']:
logging.warning("[VIBE] No vibe type or id found")
return None
if guild['current_track']: if guild['current_track']:
if after: if after:
res = await client.rotor_station_feedback_track_finished( res = await client.rotor_station_feedback_track_finished(
@@ -462,8 +500,13 @@ class VoiceExtension:
time() time()
) )
logging.debug(f"[VIBE] Skipped track: {res}") logging.debug(f"[VIBE] Skipped track: {res}")
return await self.update_vibe(ctx, user['vibe_type'], user['vibe_id'], button_callback) return await self.update_vibe(
ctx,
user['vibe_type'],
user['vibe_id'],
button_callback=button_callback
)
if guild['repeat'] and after: if guild['repeat'] and after:
logging.debug("Repeating current track") logging.debug("Repeating current track")
next_track = guild['current_track'] next_track = guild['current_track']
@@ -488,6 +531,7 @@ class VoiceExtension:
ctx, ctx,
ym_track, # type: ignore # de_json should always work here. ym_track, # type: ignore # de_json should always work here.
vc=vc, vc=vc,
menu_message=menu_message,
button_callback=button_callback button_callback=button_callback
) )
@@ -495,9 +539,19 @@ class VoiceExtension:
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15) await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
return title return title
elif guild['vibing'] and not isinstance(ctx, RawReactionActionEvent): elif guild['vibing'] and not isinstance(ctx, RawReactionActionEvent):
logging.debug("[VIBE] No next track found, updating vibe") logging.debug("[VIBE] No next track found, updating vibe")
return await self.update_vibe(ctx, user['vibe_type'], user['vibe_id'], button_callback) if not user['vibe_type'] or not user['vibe_id']:
logging.warning("[VIBE] No vibe type or id found")
return None
return await self.update_vibe(
ctx,
user['vibe_type'],
user['vibe_id'],
button_callback=button_callback
)
logging.info("No next track found") logging.info("No next track found")
self.db.update(gid, {'is_stopped': True, 'current_track': None}) self.db.update(gid, {'is_stopped': True, 'current_track': None})
@@ -553,14 +607,14 @@ class VoiceExtension:
async def get_likes(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> list[TrackShort] | None: async def get_likes(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> list[TrackShort] | None:
"""Get liked tracks. Return list of tracks on success. Return None if no token found. """Get liked tracks. Return list of tracks on success. Return None if no token found.
Args: Args:
ctx (ApplicationContext | Interaction): Context. ctx (ApplicationContext | Interaction): Context.
Returns: Returns:
list[Track] | None: List of tracks or None. list[Track] | None: List of tracks or None.
""" """
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid: if not gid or not uid:
@@ -583,7 +637,7 @@ class VoiceExtension:
return None return None
return likes.tracks return likes.tracks
async def like_track(self, ctx: ApplicationContext | Interaction) -> str | Literal['TRACK REMOVED'] | None: async def like_track(self, ctx: ApplicationContext | Interaction) -> str | Literal['TRACK REMOVED'] | None:
"""Like current track. Return track title on success. """Like current track. Return track title on success.
@@ -623,4 +677,16 @@ class VoiceExtension:
logging.debug("Client account not found") logging.debug("Client account not found")
return None return None
await client.users_likes_tracks_remove(ym_track.id, client.me.account.uid) await client.users_likes_tracks_remove(ym_track.id, client.me.account.uid)
return 'TRACK REMOVED' return 'TRACK REMOVED'
async def _retry_update_menu_embed(
self,
ctx: ApplicationContext | Interaction,
menu_mid: int,
button_callback: bool
) -> None:
update = await self.update_menu_embed(ctx, menu_mid, button_callback)
for _ in range(10):
if update:
break
update = await self.update_menu_embed(ctx, menu_mid, button_callback)

View File

@@ -157,7 +157,7 @@ class Voice(Cog, VoiceExtension):
await message.clear_reactions() await message.clear_reactions()
await message.edit(content='Запрос был отклонён.', delete_after=15) await message.edit(content='Запрос был отклонён.', delete_after=15)
del votes[str(payload.message_id)] del votes[str(payload.message_id)]
self.db.update(guild_id, {'votes': votes}) self.db.update(guild_id, {'votes': votes})
@Cog.listener() @Cog.listener()
@@ -165,7 +165,7 @@ class Voice(Cog, VoiceExtension):
logging.info(f"Reaction removed by user {payload.user_id} in channel {payload.channel_id}") logging.info(f"Reaction removed by user {payload.user_id} in channel {payload.channel_id}")
if not self.typed_bot.user: if not self.typed_bot.user:
return return
guild_id = payload.guild_id guild_id = payload.guild_id
if not guild_id: if not guild_id:
return return
@@ -187,7 +187,7 @@ class Voice(Cog, VoiceExtension):
elif payload.emoji.name == '': elif payload.emoji.name == '':
logging.info(f"User {payload.user_id} removed negative vote for message {payload.message_id}") logging.info(f"User {payload.user_id} removed negative vote for message {payload.message_id}")
del vote_data['negative_votes'][payload.user_id] del vote_data['negative_votes'][payload.user_id]
self.db.update(guild_id, {'votes': votes}) self.db.update(guild_id, {'votes': votes})
@voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.") @voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.")
@@ -231,7 +231,7 @@ class Voice(Cog, VoiceExtension):
logging.info(f"User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild.id}") logging.info(f"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) await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return return
vc = await self.get_voice_client(ctx) vc = await self.get_voice_client(ctx)
if await self.voice_check(ctx) and vc: if await self.voice_check(ctx) and vc:
self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True}) self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True})
@@ -440,9 +440,9 @@ class Voice(Cog, VoiceExtension):
logging.info(f"Track added to favorites for user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"Track added to favorites for user {ctx.author.id} in guild {ctx.guild.id}")
await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True) await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True)
@track.command(description="Запустить мою волну по текущему треку.") @track.command(name='vibe', description="Запустить мою волну по текущему треку.")
async def vibe(self, ctx: discord.ApplicationContext) -> None: async def track_vibe(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Vibe command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"Vibe (track) command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
if not await self.voice_check(ctx): if not await self.voice_check(ctx):
return return
@@ -460,3 +460,20 @@ class Voice(Cog, VoiceExtension):
await self.send_menu_message(ctx) await self.send_menu_message(ctx)
await self.update_vibe(ctx, 'track', guild['current_track']['id']) await self.update_vibe(ctx, 'track', guild['current_track']['id'])
@discord.slash_command(name='vibe', description="Запустить Мою Волну.")
async def user_vibe(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Vibe (user) command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
if not await self.voice_check(ctx):
return
guild = self.db.get_guild(ctx.guild.id)
channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not guild['always_allow_menu']:
logging.info(f"Action declined: other members are present in the voice channel")
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
return
await self.send_menu_message(ctx)
await self.update_vibe(ctx, 'user', 'onyourwave')

View File

@@ -106,7 +106,8 @@ class BaseGuildsDatabase:
shuffle=False, shuffle=False,
repeat=False, repeat=False,
votes={}, votes={},
vibing=False vibing=False,
current_viber_id=None
)) ))
def update(self, gid: int, data: Guild) -> None: def update(self, gid: int, data: Guild) -> None:
@@ -152,7 +153,8 @@ class BaseGuildsDatabase:
shuffle=False, shuffle=False,
repeat=False, repeat=False,
votes={}, votes={},
vibing=False vibing=False,
current_viber_id=None
) )
for field, default_value in fields.items(): for field, default_value in fields.items():
if field not in existing_fields: if field not in existing_fields:

View File

@@ -24,6 +24,7 @@ class Guild(TypedDict, total=False):
repeat: bool repeat: bool
votes: dict[str, MessageVotes] votes: dict[str, MessageVotes]
vibing: bool vibing: bool
current_viber_id: int | None
class ExplicitGuild(TypedDict): class ExplicitGuild(TypedDict):
_id: int _id: int
@@ -42,4 +43,5 @@ class ExplicitGuild(TypedDict):
shuffle: bool shuffle: bool
repeat: bool repeat: bool
votes: dict[str, MessageVotes] votes: dict[str, MessageVotes]
vibing: bool vibing: bool
current_viber_id: int | None

View File

@@ -1,5 +1,5 @@
import logging import logging
from typing import Literal, cast from typing import Any, Literal, cast
import discord import discord
from yandex_music import Track, Album, Artist, Playlist from yandex_music import Track, Album, Artist, Playlist
@@ -19,11 +19,11 @@ class PlayButton(Button, VoiceExtension):
logging.debug(f"Callback triggered for type: '{type(self.item).__name__}'") logging.debug(f"Callback triggered for type: '{type(self.item).__name__}'")
if not interaction.guild: if not interaction.guild:
logging.warning("No guild found in context.") logging.warning("No guild found in PlayButton callback")
return return
if not await self.voice_check(interaction): if not await self.voice_check(interaction):
logging.debug("Voice check failed") logging.debug("Voice check failed in PlayButton callback")
return return
gid = interaction.guild.id gid = interaction.guild.id
@@ -41,7 +41,7 @@ class PlayButton(Button, VoiceExtension):
elif isinstance(self.item, Album): elif isinstance(self.item, Album):
album = await self.item.with_tracks_async() album = await self.item.with_tracks_async()
if not album or not album.volumes: if not album or not album.volumes:
logging.debug("Failed to fetch album tracks") logging.debug("Failed to fetch album tracks in PlayButton callback")
await interaction.respond("Не удалось получить треки альбома.", ephemeral=True) await interaction.respond("Не удалось получить треки альбома.", ephemeral=True)
return return
@@ -53,7 +53,7 @@ class PlayButton(Button, VoiceExtension):
elif isinstance(self.item, Artist): elif isinstance(self.item, Artist):
artist_tracks = await self.item.get_tracks_async() artist_tracks = await self.item.get_tracks_async()
if not artist_tracks: if not artist_tracks:
logging.debug("Failed to fetch artist tracks") logging.debug("Failed to fetch artist tracks in PlayButton callback")
await interaction.respond("Не удалось получить треки артиста.", ephemeral=True) await interaction.respond("Не удалось получить треки артиста.", ephemeral=True)
return return
@@ -65,7 +65,7 @@ class PlayButton(Button, VoiceExtension):
elif isinstance(self.item, Playlist): elif isinstance(self.item, Playlist):
short_tracks = await self.item.fetch_tracks_async() short_tracks = await self.item.fetch_tracks_async()
if not short_tracks: if not short_tracks:
logging.debug("Failed to fetch playlist tracks") logging.debug("Failed to fetch playlist tracks in PlayButton callback")
await interaction.respond("Не удалось получить треки из плейлиста.", delete_after=15) await interaction.respond("Не удалось получить треки из плейлиста.", delete_after=15)
return return
@@ -77,7 +77,7 @@ class PlayButton(Button, VoiceExtension):
elif isinstance(self.item, list): elif isinstance(self.item, list):
tracks = self.item.copy() tracks = self.item.copy()
if not tracks: if not tracks:
logging.debug("Empty tracks list") logging.debug("Empty tracks list in PlayButton callback")
await interaction.respond("Не удалось получить треки.", delete_after=15) await interaction.respond("Не удалось получить треки.", delete_after=15)
return return
@@ -89,7 +89,7 @@ class PlayButton(Button, VoiceExtension):
raise ValueError(f"Unknown item type: '{type(self.item).__name__}'") 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.get(f'vote_{action}') and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
logging.debug(f"Starting vote for '{action}'") logging.debug(f"Starting vote for '{action}' (from PlayButton callback)")
message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30)) message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30))
response = await message.original_response() response = await message.original_response()
@@ -109,7 +109,7 @@ class PlayButton(Button, VoiceExtension):
} }
) )
else: else:
logging.debug(f"Skipping vote for '{action}'") logging.debug(f"Skipping vote for '{action}' (from PlayButton callback)")
if guild['current_track'] is not None: if guild['current_track'] is not None:
self.db.modify_track(gid, tracks, 'next', 'extend') self.db.modify_track(gid, tracks, 'next', 'extend')
@@ -129,6 +129,41 @@ class PlayButton(Button, VoiceExtension):
else: else:
await interaction.respond(response_message, delete_after=15) await interaction.respond(response_message, delete_after=15)
class MyVibeButton(Button, VoiceExtension):
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *args, **kwargs):
Button.__init__(self, *args, **kwargs)
VoiceExtension.__init__(self, None)
self.item = item
async def callback(self, interaction: discord.Interaction):
logging.debug(f"[VIBE] Button callback for '{type(self.item).__name__}'")
if not await self.voice_check(interaction):
return
gid = interaction.guild_id
if not gid:
logging.warning(f"[VIBE] Guild ID is None in button callback")
return
guild = 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: dict[Any, Literal['track', 'album', 'artist', 'playlist', 'user']] = {
Track: 'track', Album: 'album', Artist: 'artist', Playlist: 'playlist', list: 'user'
} # NOTE: Likes playlist should have its own entry instead of 'user:onyourwave'
await self.send_menu_message(interaction)
await self.update_vibe(
interaction,
track_type_map[type(self.item)],
cast(int, self.item.uid) if isinstance(self.item, Playlist) else cast(int | str, self.item.id) if not isinstance(self.item, list) else 'onyourwave'
)
class ListenView(View): class ListenView(View):
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False): def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False):
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout) super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
@@ -150,11 +185,13 @@ class ListenView(View):
self.add_item(PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray)) self.add_item(PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray))
return return
self.button1: Button = Button(label="Слушать в приложении", style=ButtonStyle.gray, url=link_app) self.button1: Button = Button(label="Слушать в приложении", style=ButtonStyle.gray, url=link_app, row=0)
self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web) self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web, row=0)
self.button3: PlayButton = PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray) self.button3: PlayButton = PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray, row=0)
self.button4: MyVibeButton = MyVibeButton(item, label="Моя Волна", style=ButtonStyle.gray, emoji="🌊", row=1)
if item.available: if item.available:
# self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason. # self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason.
self.add_item(self.button2) self.add_item(self.button2)
self.add_item(self.button3) self.add_item(self.button3)
self.add_item(self.button4)

View File

@@ -1,9 +1,10 @@
import logging import logging
from typing import Self, cast from typing import Self, cast
from discord.ui import View, Button, Item from discord.ui import View, Button, Item, Modal, Select
from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption
import yandex_music.exceptions
from yandex_music import Track, ClientAsync from yandex_music import Track, ClientAsync
from MusicBot.cogs.utils.voice_extension import VoiceExtension from MusicBot.cogs.utils.voice_extension import VoiceExtension
@@ -123,8 +124,15 @@ class LyricsButton(Button, VoiceExtension):
ClientAsync(ym_token), # type: ignore # Async client can be used here ClientAsync(ym_token), # type: ignore # Async client can be used here
)) ))
lyrics = await track.get_lyrics_async() try:
lyrics = await track.get_lyrics_async()
except yandex_music.exceptions.NotFoundError:
logging.debug('Lyrics not found')
await interaction.respond("❌ Текст песни не найден. Яндекс нам соврал (опять)!", delete_after=15, ephemeral=True)
return
if not lyrics: if not lyrics:
logging.debug('Lyrics not found')
return return
embed = Embed( embed = Embed(
@@ -137,6 +145,36 @@ class LyricsButton(Button, VoiceExtension):
embed.add_field(name='', value=subtext, inline=False) embed.add_field(name='', value=subtext, inline=False)
await interaction.respond(embed=embed, ephemeral=True) await interaction.respond(embed=embed, ephemeral=True)
class MyVibeButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
logging.info('[VIBE] My vibe button callback')
if not await self.voice_check(interaction):
return
if not interaction.guild_id:
logging.warning('[VIBE] No guild id in button callback')
return
track = self.db.get_track(interaction.guild_id, 'current')
if track:
logging.info(f"[VIBE] Playing vibe for track '{track["id"]}'")
await self.update_vibe(
interaction,
'track',
track['id'],
button_callback=True
)
else:
logging.info('[VIBE] Playing on your wave')
await self.update_vibe(
interaction,
'user',
'onyourwave',
button_callback=True
)
class MenuView(View, VoiceExtension): class MenuView(View, VoiceExtension):
@@ -156,8 +194,9 @@ class MenuView(View, VoiceExtension):
self.like_button = LikeButton(style=ButtonStyle.secondary, emoji='❤️', row=1) self.like_button = LikeButton(style=ButtonStyle.secondary, emoji='❤️', row=1)
self.lyrics_button = LyricsButton(style=ButtonStyle.secondary, emoji='📋', row=1) self.lyrics_button = LyricsButton(style=ButtonStyle.secondary, emoji='📋', row=1)
self.vibe_button = MyVibeButton(style=ButtonStyle.secondary, emoji='🌊', row=1)
async def init(self) -> Self: async def init(self, *, disable: bool = False) -> Self:
current_track = self.guild['current_track'] current_track = self.guild['current_track']
likes = await self.get_likes(self.ctx) likes = await self.get_likes(self.ctx)
@@ -177,6 +216,10 @@ class MenuView(View, VoiceExtension):
self.add_item(self.like_button) self.add_item(self.like_button)
self.add_item(self.lyrics_button) self.add_item(self.lyrics_button)
self.add_item(self.vibe_button)
if disable:
self.disable_all_items()
return self return self
@@ -186,7 +229,7 @@ class MenuView(View, VoiceExtension):
return return
if self.guild['current_menu']: if self.guild['current_menu']:
self.db.update(self.ctx.guild_id, {'current_menu': None, 'previous_tracks': []}) self.db.update(self.ctx.guild_id, {'current_menu': None, 'previous_tracks': [], 'vibing': False})
message = await self.get_menu_message(self.ctx, self.guild['current_menu']) message = await self.get_menu_message(self.ctx, self.guild['current_menu'])
if message: if message:
await message.delete() await message.delete()