feat: MyVibe settings and memory leak fix.

This commit is contained in:
Lemon4ksan
2025-01-30 20:08:46 +03:00
parent c353de429f
commit b3962c8928
9 changed files with 455 additions and 245 deletions

View File

@@ -30,7 +30,7 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]:
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 {ctx.interaction.user.id} provided invalid token") logging.info(f"[GENERAL] User {ctx.interaction.user.id} provided invalid token")
return ['❌ Недействительный токен.'] return ['❌ Недействительный токен.']
content_type = ctx.options['тип'] content_type = ctx.options['тип']
@@ -81,7 +81,7 @@ class General(Cog):
default='all' default='all'
) )
async def help(self, ctx: discord.ApplicationContext, command: str) -> None: async def help(self, ctx: discord.ApplicationContext, command: str) -> None:
logging.info(f"Help command invoked by {ctx.user.id} for command '{command}'") logging.info(f"[GENERAL] Help command invoked by {ctx.user.id} for command '{command}'")
response_message = None response_message = None
embed = discord.Embed( embed = discord.Embed(
@@ -171,32 +171,32 @@ class General(Cog):
@account.command(description="Ввести токен от Яндекс Музыки.") @account.command(description="Ввести токен от Яндекс Музыки.")
@discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.") @discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.")
async def login(self, ctx: discord.ApplicationContext, token: str) -> None: async def login(self, ctx: discord.ApplicationContext, token: str) -> None:
logging.info(f"Login command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[GENERAL] Login command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
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"Invalid token provided by user {ctx.author.id}") logging.info(f"[GENERAL] Invalid token provided by user {ctx.author.id}")
await ctx.respond('❌ Недействительный токен.', delete_after=15, ephemeral=True) await ctx.respond('❌ Недействительный токен.', delete_after=15, ephemeral=True)
return return
about = cast(yandex_music.Status, client.me).to_dict() about = cast(yandex_music.Status, client.me).to_dict()
uid = ctx.author.id uid = ctx.author.id
self.users_db.update(uid, {'ym_token': token}) self.users_db.update(uid, {'ym_token': token})
logging.info(f"Token saved for user {ctx.author.id}") logging.info(f"[GENERAL] Token saved for user {ctx.author.id}")
await ctx.respond(f'Привет, {about['account']['first_name']}!', delete_after=15, ephemeral=True) await ctx.respond(f'Привет, {about['account']['first_name']}!', delete_after=15, ephemeral=True)
@account.command(description="Удалить токен из датабазы бота.") @account.command(description="Удалить токен из датабазы бота.")
async def remove(self, ctx: discord.ApplicationContext) -> None: async def remove(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Remove command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[GENERAL] Remove command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
self.users_db.update(ctx.user.id, {'ym_token': None}) self.users_db.update(ctx.user.id, {'ym_token': None})
await ctx.respond(f'Токен был удалён.', delete_after=15, ephemeral=True) await ctx.respond(f'Токен был удалён.', delete_after=15, ephemeral=True)
@account.command(description="Получить плейлист «Мне нравится»") @account.command(description="Получить плейлист «Мне нравится»")
async def likes(self, ctx: discord.ApplicationContext) -> None: async def likes(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Likes command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[GENERAL] Likes command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
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.info(f"No token found for user {ctx.user.id}") logging.info(f"[GENERAL] 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 return
client = await YMClient(token).init() client = await YMClient(token).init()
@@ -206,23 +206,23 @@ class General(Cog):
return return
likes = await client.users_likes_tracks() likes = await client.users_likes_tracks()
if likes is None: if likes is None:
logging.info(f"Failed to fetch likes for user {ctx.user.id}") logging.info(f"[GENERAL] Failed to fetch likes for user {ctx.user.id}")
await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True)
return return
elif not likes: elif not likes:
logging.info(f"Empty likes for user {ctx.user.id}") logging.info(f"[GENERAL] Empty likes for user {ctx.user.id}")
await ctx.respond('У вас нет треков в плейлисте «Мне нравится».', delete_after=15, ephemeral=True) await ctx.respond('У вас нет треков в плейлисте «Мне нравится».', delete_after=15, ephemeral=True)
return return
real_tracks = await gather(*[track_short.fetch_track_async() for track_short in likes.tracks], return_exceptions=True) 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 tracks = [track for track in real_tracks if not isinstance(track, BaseException)] # Can't fetch user tracks
embed = await generate_item_embed(tracks) embed = await generate_item_embed(tracks)
logging.info(f"Successfully fetched likes for user {ctx.user.id}") logging.info(f"[GENERAL] Successfully fetched likes for user {ctx.user.id}")
await ctx.respond(embed=embed, view=ListenView(tracks)) await ctx.respond(embed=embed, view=ListenView(tracks))
@account.command(description="Получить ваши плейлисты.") @account.command(description="Получить ваши плейлисты.")
async def playlists(self, ctx: discord.ApplicationContext) -> None: async def playlists(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Playlists command invoked by user {ctx.user.id} in guild {ctx.guild_id}") logging.info(f"[GENERAL] Playlists command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
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:
@@ -242,7 +242,7 @@ class General(Cog):
self.users_db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0}) self.users_db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0})
embed = generate_playlists_embed(0, playlists) embed = generate_playlists_embed(0, playlists)
logging.info(f"Successfully fetched playlists for user {ctx.user.id}") logging.info(f"[GENERAL] 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.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.") @discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
@@ -266,19 +266,19 @@ class General(Cog):
content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'], content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'],
name: str name: str
) -> None: ) -> None:
logging.info(f"Find command invoked by user {ctx.user.id} in guild {ctx.guild_id} for '{content_type}' with name '{name}'") logging.info(f"[GENERAL] Find command invoked by user {ctx.user.id} in guild {ctx.guild_id} for '{content_type}' with name '{name}'")
guild = self.db.get_guild(ctx.guild_id) guild = self.db.get_guild(ctx.guild_id)
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.info(f"No token found for user {ctx.user.id}") logging.info(f"[GENERAL] No token found for user {ctx.user.id}")
await ctx.respond("Необходимо указать свой токен доступа с помощью команды /login.", delete_after=15, 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 {ctx.user.id} provided invalid token") logging.info(f"[GENERAL] User {ctx.user.id} provided invalid token")
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True) await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
return return
@@ -291,20 +291,20 @@ class General(Cog):
playlists = await client.users_playlists_list(client.me.account.uid) playlists = await client.users_playlists_list(client.me.account.uid)
result = next((playlist for playlist in playlists if playlist.title == name), None) result = next((playlist for playlist in playlists if playlist.title == name), None)
if not result: if not result:
logging.info(f"User {ctx.user.id} playlist '{name}' not found") logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' not found")
await ctx.respond("❌ Плейлист не найден.", delete_after=15, ephemeral=True) await ctx.respond("❌ Плейлист не найден.", delete_after=15, ephemeral=True)
return return
tracks = await result.fetch_tracks_async() tracks = await result.fetch_tracks_async()
if not tracks: if not tracks:
logging.info(f"User {ctx.user.id} playlist '{name}' is empty") logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' is empty")
await ctx.respond("❌ Плейлист пуст.", delete_after=15, ephemeral=True) await ctx.respond("❌ Плейлист пуст.", delete_after=15, ephemeral=True)
return return
for track_short in tracks: for track_short in tracks:
track = cast(Track, track_short.track) track = cast(Track, track_short.track)
if (track.explicit or track.content_warning) and not guild['allow_explicit']: if (track.explicit or track.content_warning) and not guild['allow_explicit']:
logging.info(f"User {ctx.user.id} playlist '{name}' contains explicit content and is not allowed on this server") logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' contains explicit content and is not allowed on this server")
await ctx.respond("❌ Explicit контент запрещён на этом сервере.", delete_after=15, ephemeral=True) await ctx.respond("❌ Explicit контент запрещён на этом сервере.", delete_after=15, ephemeral=True)
return return
@@ -328,7 +328,7 @@ class General(Cog):
content = result.playlists content = result.playlists
if not content: if not content:
logging.info(f"User {ctx.user.id} search for '{name}' returned no results") logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no results")
await ctx.respond("❌ По запросу ничего не найдено.", delete_after=15, ephemeral=True) await ctx.respond("❌ По запросу ничего не найдено.", delete_after=15, ephemeral=True)
return return
content = content.results[0] content = content.results[0]
@@ -337,35 +337,35 @@ class General(Cog):
view = ListenView(content) view = ListenView(content)
if isinstance(content, (Track, Album)) and (content.explicit or content.content_warning) and not guild['allow_explicit']: if isinstance(content, (Track, Album)) and (content.explicit or content.content_warning) and not guild['allow_explicit']:
logging.info(f"User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server") logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server")
await ctx.respond("❌ Explicit контент запрещён на этом сервере.", delete_after=15, ephemeral=True) await ctx.respond("❌ Explicit контент запрещён на этом сервере.", delete_after=15, ephemeral=True)
return return
elif isinstance(content, Artist): elif isinstance(content, Artist):
tracks = await content.get_tracks_async() tracks = await content.get_tracks_async()
if not tracks: if not tracks:
logging.info(f"User {ctx.user.id} search for '{name}' returned no tracks") logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no tracks")
await ctx.respond("❌ Треки от этого исполнителя не найдены.", delete_after=15, ephemeral=True) await ctx.respond("❌ Треки от этого исполнителя не найдены.", delete_after=15, ephemeral=True)
return return
for track in tracks: for track in tracks:
if (track.explicit or track.content_warning) and not guild['allow_explicit']: if (track.explicit or track.content_warning) and not guild['allow_explicit']:
logging.info(f"User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server") logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server")
view = None view = None
embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки") embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки")
break break
elif isinstance(content, Playlist): elif isinstance(content, Playlist):
tracks = await content.fetch_tracks_async() tracks = await content.fetch_tracks_async()
if not tracks: if not tracks:
logging.info(f"User {ctx.user.id} search for '{name}' returned no tracks") logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no tracks")
await ctx.respond("❌ Пустой плейлист.", delete_after=15, ephemeral=True) await ctx.respond("❌ Пустой плейлист.", delete_after=15, ephemeral=True)
return return
for track_short in content.tracks: for track_short in content.tracks:
track = cast(Track, track_short.track) track = cast(Track, track_short.track)
if (track.explicit or track.content_warning) and not guild['allow_explicit']: if (track.explicit or track.content_warning) and not guild['allow_explicit']:
logging.info(f"User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server") logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server")
view = None view = None
embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки") embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки")
break break
logging.info(f"Successfully generated '{content_type}' message for user {ctx.author.id}") logging.info(f"[GENERAL] Successfully generated '{content_type}' message for user {ctx.author.id}")
await ctx.respond(embed=embed, view=view) await ctx.respond(embed=embed, view=view)

View File

@@ -1,7 +1,8 @@
from .embeds import generate_item_embed from .embeds import generate_item_embed
from .voice_extension import VoiceExtension from .voice_extension import VoiceExtension, menu_views
__all__ = [ __all__ = [
"generate_item_embed", "generate_item_embed",
"VoiceExtension", "VoiceExtension",
"menu_views"
] ]

View File

@@ -36,8 +36,8 @@ async def generate_item_embed(item: Track | Album | Artist | Playlist | list[Tra
if vibing: if vibing:
embed.set_image( embed.set_image(
url="https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExbjd6M3VscnZnMXFlb3NtMHY2Zm5tbTVvMm8yY21nNXhpN214YzhyaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7HxhnYcJljc3ON77O3/giphy.gif" url="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExaWN5dG50YWtxeDcwNnZpaDdqY3A3bHBsYXkyb29rdXoyajNjdWMxYiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/IilXmX8tjwfXgSwjBr/giphy.gif"
) # TODO: Get better gif )
return embed return embed
def _generate_likes_embed(tracks: list[Track]) -> Embed: def _generate_likes_embed(tracks: list[Track]) -> Embed:

View File

@@ -7,6 +7,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.ui import View
from discord import Interaction, ApplicationContext, RawReactionActionEvent from discord import Interaction, ApplicationContext, RawReactionActionEvent
from MusicBot.cogs.utils import generate_item_embed from MusicBot.cogs.utils import generate_item_embed
@@ -14,6 +15,8 @@ from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
# TODO: RawReactionActionEvent is poorly supported. # TODO: RawReactionActionEvent is poorly supported.
menu_views: dict[int, View] = {} # Store menu views and delete them when needed to prevent memory leaks for after callbacks.
class VoiceExtension: class VoiceExtension:
def __init__(self, bot: discord.Bot | None) -> None: def __init__(self, bot: discord.Bot | None) -> None:
@@ -23,13 +26,13 @@ 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("[VC] Sending player menu") logging.info("[VC_EXT] Sending menu message")
if not ctx.guild: if not ctx.guild_id:
logging.warning("[VC] Guild not found in context inside 'create_menu'") logging.warning("[VC_EXT] Guild id 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']:
@@ -45,57 +48,61 @@ class VoiceExtension:
embed.remove_footer() embed.remove_footer()
if guild['current_menu']: if guild['current_menu']:
logging.info(f"[VC] Deleting old player menu {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}")
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()
interaction = cast(discord.Interaction, await ctx.respond(view=await MenuView(ctx).init(), embed=embed)) if ctx.guild_id in menu_views:
response = await interaction.original_response() menu_views[ctx.guild_id].stop()
self.db.update(ctx.guild.id, {'current_menu': response.id}) menu_views[ctx.guild_id] = await MenuView(ctx).init()
logging.info(f"[VC] New player menu {response.id} created in guild {ctx.guild.id}") interaction = cast(discord.Interaction, await ctx.respond(view=menu_views[ctx.guild_id], embed=embed))
response = await interaction.original_response()
self.db.update(ctx.guild_id, {'current_menu': response.id})
logging.info(f"[VC_EXT] New menu message {response.id} created in guild {ctx.guild_id}")
async def get_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, player_mid: int) -> discord.Message | None: async def get_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, menu_mid: int) -> discord.Message | None:
"""Fetch the player message by its id. Return the message if found, None if not. """Fetch the menu message by its id. Return the message if found, None if not.
Reset `current_menu` field in the database if not found. Reset `current_menu` field in the database if not found.
Args: Args:
ctx (ApplicationContext | Interaction): Context. ctx (ApplicationContext | Interaction): Context.
player_mid (int): Id of the player message. menu_mid (int): Id of the menu message.
Returns: Returns:
discord.Message | None: Player message or None. discord.Message | None: Menu message or None.
""" """
logging.debug(f"[VC] Fetching player message {player_mid}...") logging.debug(f"[VC_EXT] Fetching menu message {menu_mid}...")
if not ctx.guild_id: if not ctx.guild_id:
logging.warning("[VC] Guild ID not found in context") logging.warning("[VC_EXT] Guild ID not found in context")
return None return None
try: try:
if isinstance(ctx, Interaction): if isinstance(ctx, Interaction):
player = ctx.client.get_message(player_mid) menu = ctx.client.get_message(menu_mid)
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.")
player = self.bot.get_message(player_mid) menu = self.bot.get_message(menu_mid)
elif isinstance(ctx, ApplicationContext): elif isinstance(ctx, ApplicationContext):
player = await ctx.fetch_message(player_mid) menu = await ctx.fetch_message(menu_mid)
else: else:
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.") raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
except discord.DiscordException as e: except discord.DiscordException as e:
logging.debug(f"[VC] Failed to get player message: {e}") logging.debug(f"[VC_EXT] Failed to get menu message: {e}")
self.db.update(ctx.guild_id, {'current_menu': None}) self.db.update(ctx.guild_id, {'current_menu': None})
return None return None
if player: if menu:
logging.debug("[VC] Player message found") logging.debug("[VC_EXT] Menu message found")
else: else:
logging.debug("[VC] Player message not found. Resetting current_menu field.") logging.debug("[VC_EXT] Menu message not found. Resetting current_menu field.")
self.db.update(ctx.guild_id, {'current_menu': None}) self.db.update(ctx.guild_id, {'current_menu': None})
return player return menu
async def update_menu_embed( async def update_menu_embed(
self, self,
@@ -103,11 +110,11 @@ class VoiceExtension:
menu_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 menu message by its id. Return True if updated, False if not.
Args: Args:
ctx (ApplicationContext | Interaction): Context. ctx (ApplicationContext | Interaction): Context.
menu_mid (int): Id of the player message. There can only be only one player in the guild. menu_mid (int): Id of the menu message. There can only be only one menu 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:
@@ -115,7 +122,7 @@ class VoiceExtension:
""" """
from MusicBot.ui import MenuView from MusicBot.ui import MenuView
logging.debug( logging.debug(
f"[VC] Updating player embed using " + ( f"[VC_EXT] Updating menu 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"
@@ -126,22 +133,22 @@ class VoiceExtension:
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("[VC] Guild ID or User ID not found in context inside 'update_player_embed'") logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'update_menu_embed'")
return False return False
player = await self.get_menu_message(ctx, menu_mid) menu = await self.get_menu_message(ctx, menu_mid)
if not player: if not menu:
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"[VC] No token found for user {uid}") logging.debug(f"[VC_EXT] 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("[VC] No current track found") logging.debug("[VC_EXT] No current track found")
return False return False
track = cast(Track, Track.de_json( track = cast(Track, Track.de_json(
@@ -152,16 +159,23 @@ class VoiceExtension:
embed = await generate_item_embed(track, guild['vibing']) embed = await generate_item_embed(track, guild['vibing'])
try: try:
if gid in menu_views:
menu_views[gid].stop()
menu_views[gid] = await MenuView(ctx).init()
if isinstance(ctx, Interaction) and button_callback: if isinstance(ctx, Interaction) and button_callback:
# If interaction from player buttons # If interaction from menu buttons
await ctx.edit(embed=embed, view=await MenuView(ctx).init()) await ctx.edit(embed=embed, view=menu_views[gid])
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 player.edit(embed=embed, view=await MenuView(ctx).init()) await menu.edit(embed=embed, view=menu_views[gid])
except discord.NotFound: except discord.NotFound:
logging.warning("[VC] Player message not found") logging.warning("[VC_EXT] Menu message not found")
if gid in menu_views:
menu_views[gid].stop()
del menu_views[gid]
return False return False
logging.debug("[VC_EXT] Menu embed updated")
return True return True
async def update_vibe( async def update_vibe(
@@ -170,6 +184,7 @@ class VoiceExtension:
type: Literal['track', 'album', 'artist', 'playlist', 'user'], type: Literal['track', 'album', 'artist', 'playlist', 'user'],
id: str | int, id: str | int,
*, *,
update_settings: bool = False,
button_callback: bool = False button_callback: bool = False
) -> str | None: ) -> str | None:
"""Update vibe state. Return track title on success. """Update vibe state. Return track title on success.
@@ -183,25 +198,22 @@ class VoiceExtension:
Returns: Returns:
str | None: Track title or None. str | None: Track title or None.
""" """
logging.info(f"[VC] Updating vibe for guild {ctx.guild_id} with type '{type}' and id '{id}'") logging.info(f"[VC_EXT] 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("[VC] Guild ID or User ID not found in context inside 'vibe_update'") logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'vibe_update'")
return None return None
token = self.users_db.get_ym_token(uid) user = self.users_db.get_user(uid)
if not token: if not user['ym_token']:
logging.info(f"[VC] User {uid} has no YM token") logging.info(f"[VC_EXT] User {uid} has no YM token")
await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True) await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True)
return return
try: client = await self.init_ym_client(ctx, user['ym_token'])
client = await YMClient(token).init() if not client:
except yandex_music.exceptions.UnauthorizedError:
logging.info(f"[VC] User {uid} provided invalid token")
await ctx.respond('❌ Недействительный токен.')
return return
self.users_db.update(uid, {'vibe_type': type, 'vibe_id': id}) self.users_db.update(uid, {'vibe_type': type, 'vibe_id': id})
@@ -214,10 +226,17 @@ class VoiceExtension:
timestamp=time() timestamp=time()
) )
logging.debug(f"[VIBE] Radio started feedback: {feedback}") logging.debug(f"[VIBE] Radio started feedback: {feedback}")
tracks = await client.rotor_station_tracks(f"{type}:{id}") tracks = await client.rotor_station_tracks(f"{type}:{id}")
self.db.update(gid, {'vibing': True}) self.db.update(gid, {'vibing': True})
elif guild['current_track']: elif guild['current_track']:
if update_settings:
settings = user['vibe_settings']
await client.rotor_station_settings2(
f"{type}:{id}",
mood_energy=settings['mood'],
diversity=settings['diversity'],
language=settings['lang']
)
tracks = await client.rotor_station_tracks( tracks = await client.rotor_station_tracks(
f"{type}:{id}", f"{type}:{id}",
queue=guild['current_track']['id'] queue=guild['current_track']['id']
@@ -252,24 +271,29 @@ class VoiceExtension:
bool: Check result. bool: Check result.
""" """
if not ctx.user or not ctx.guild: if not ctx.user or not ctx.guild:
logging.warning("[VC] User or guild not found in context inside 'voice_check'") logging.warning("[VC_EXT] 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"[VC] No token found for user {ctx.user.id}") logging.debug(f"[VC_EXT] 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
if not isinstance(ctx.channel, discord.VoiceChannel): if not isinstance(ctx.channel, discord.VoiceChannel):
logging.debug("[VC] User is not in a voice channel") 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 return False
if ctx.user.id not in ctx.channel.voice_states:
logging.debug("[VC_EXT] User is not connected to the voice channel")
await ctx.respond("❌ Вы должны находиться в голосовом канале.", delete_after=15, ephemeral=True)
return False
voice_clients = ctx.client.voice_clients if isinstance(ctx, Interaction) else ctx.bot.voice_clients voice_clients = ctx.client.voice_clients if isinstance(ctx, Interaction) else ctx.bot.voice_clients
voice_chat = discord.utils.get(voice_clients, guild=ctx.guild) voice_chat = discord.utils.get(voice_clients, guild=ctx.guild)
if not voice_chat: if not voice_chat:
logging.debug("[VC] Voice client not found") logging.debug("[VC_EXT] 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
@@ -280,7 +304,7 @@ class VoiceExtension:
await ctx.respond("❌ Вы не можете взаимодействовать с чужой волной!", delete_after=15, ephemeral=True) await ctx.respond("❌ Вы не можете взаимодействовать с чужой волной!", delete_after=15, ephemeral=True)
return False return False
logging.debug("[VC] Voice requirements met") logging.debug("[VC_EXT] 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:
@@ -292,29 +316,26 @@ class VoiceExtension:
Returns: Returns:
discord.VoiceClient | None: Voice client or None. discord.VoiceClient | None: Voice client or None.
""" """
if isinstance(ctx, Interaction): if isinstance(ctx, (Interaction, ApplicationContext)):
voice_clients = ctx.client.voice_clients voice_clients = ctx.client.voice_clients if isinstance(ctx, Interaction) else ctx.bot.voice_clients
guild = ctx.guild 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("[VC] Guild ID not found in context inside get_voice_client") logging.warning("[VC_EXT] Guild ID not found in context inside get_voice_client")
return None return None
voice_clients = self.bot.voice_clients voice_clients = self.bot.voice_clients
guild = await self.bot.fetch_guild(ctx.guild_id) guild = await self.bot.fetch_guild(ctx.guild_id)
elif isinstance(ctx, ApplicationContext):
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__}'.")
voice_chat = discord.utils.get(voice_clients, guild=guild) voice_chat = discord.utils.get(voice_clients, guild=guild)
if voice_chat: if voice_chat:
logging.debug("[VC] Voice client found") logging.debug("[VC_EXT] Voice client found")
else: else:
logging.debug("[VC] Voice client not found") logging.debug("[VC_EXT] Voice client not found")
return cast(discord.VoiceClient | None, voice_chat) return cast(discord.VoiceClient | None, voice_chat)
@@ -347,7 +368,7 @@ 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
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("[VC] Guild ID or User ID not found in context inside 'play_track'") logging.warning("Guild ID or User ID not found in context")
return None return None
if not vc: if not vc:
@@ -366,14 +387,17 @@ 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) self.db.update(gid, {'current_track': track.to_dict()})
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
if guild['current_menu'] and not isinstance(ctx, RawReactionActionEvent): if guild['current_menu'] and not isinstance(ctx, RawReactionActionEvent):
if menu_message: if menu_message:
try: try:
await menu_message.edit(embed=await generate_item_embed(track, guild['vibing']), view=await MenuView(ctx).init()) if gid in menu_views:
menu_views[gid].stop()
menu_views[gid] = await MenuView(ctx).init()
await menu_message.edit(embed=await generate_item_embed(track, guild['vibing']), view=menu_views[gid])
except discord.errors.NotFound: except discord.errors.NotFound:
logging.warning("[VC] Menu message not found. Using 'update_menu_embed' instead.") logging.warning("[VC_EXT] Menu message not found. Using 'update_menu_embed' instead.")
await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback) await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback)
else: else:
await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback) await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback)
@@ -382,18 +406,17 @@ class VoiceExtension:
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: # sometimes track takes too long to download. except yandex_music.exceptions.TimedOutError: # sometimes track takes too long to download.
logging.warning(f"[VC] Timed out while downloading track '{track.title}'") logging.warning(f"[VC_EXT] 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)
if not retry: if not retry:
channel = cast(discord.VoiceChannel, ctx.channel) 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) return await self.play_track(ctx, track, vc=vc, button_callback=button_callback, retry=True)
await channel.send(f"😔 Снова не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15) 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"[VC] Playing track '{track.title}'") logging.info(f"[VC_EXT] Playing track '{track.title}'")
self.db.update(gid, {'is_stopped': False}) self.db.update(gid, {'is_stopped': False})
@@ -413,13 +436,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("[VC] Guild ID not found in context") logging.warning("[VC_EXT] 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("[VC] Stopping playback") logging.debug("[VC_EXT] 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()
@@ -450,20 +473,21 @@ class VoiceExtension:
menu_message = 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("[VC_EXT] Guild ID or User ID not found in context inside 'next_track'")
return None return None
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
user = self.users_db.get_user(uid) user = self.users_db.get_user(uid)
token = self.users_db.get_ym_token(uid) if not user['ym_token']:
if not token: logging.debug(f"[VC_EXT] 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 self.init_ym_client(ctx, user['ym_token'])
if not client:
return None
if guild['is_stopped'] and after: if guild['is_stopped'] and after:
logging.debug("Playback is stopped, skipping after callback...") logging.debug("[VC_EXT] Playback is stopped, skipping after callback...")
return None return None
if not vc: if not vc:
@@ -474,7 +498,10 @@ class VoiceExtension:
if after and guild['current_menu']: if after and guild['current_menu']:
menu_message = await self.get_menu_message(ctx, guild['current_menu']) menu_message = await self.get_menu_message(ctx, guild['current_menu'])
if menu_message: if menu_message:
await menu_message.edit(view=await MenuView(ctx).init(disable=True)) if gid in menu_views:
menu_views[gid].stop()
menu_views[gid] = await MenuView(ctx).init(disable=True)
await menu_message.edit(view=menu_views[gid])
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']: if not user['vibe_type'] or not user['vibe_id']:
@@ -483,23 +510,23 @@ class VoiceExtension:
if guild['current_track']: if guild['current_track']:
if after: if after:
res = await client.rotor_station_feedback_track_finished( feedback = await client.rotor_station_feedback_track_finished(
f'{user['vibe_type']}:{user['vibe_id']}', f'{user['vibe_type']}:{user['vibe_id']}',
guild['current_track']['id'], guild['current_track']['id'],
guild['current_track']['duration_ms'] // 1000, guild['current_track']['duration_ms'] // 1000,
user['vibe_batch_id'], # type: ignore # Wrong typehints user['vibe_batch_id'], # type: ignore # Wrong typehints
time() time()
) )
logging.debug(f"[VIBE] Finished track: {res}") logging.debug(f"[VIBE] Finished track: {feedback}")
else: else:
res = await client.rotor_station_feedback_skip( feedback = await client.rotor_station_feedback_skip(
f'{user['vibe_type']}:{user['vibe_id']}', f'{user['vibe_type']}:{user['vibe_id']}',
guild['current_track']['id'], guild['current_track']['id'],
guild['current_track']['duration_ms'] // 1000, guild['current_track']['duration_ms'] // 1000,
user['vibe_batch_id'], # type: ignore # Wrong typehints user['vibe_batch_id'], # type: ignore # Wrong typehints
time() time()
) )
logging.debug(f"[VIBE] Skipped track: {res}") logging.debug(f"[VIBE] Skipped track: {feedback}")
return await self.update_vibe( return await self.update_vibe(
ctx, ctx,
user['vibe_type'], user['vibe_type'],
@@ -508,17 +535,17 @@ class VoiceExtension:
) )
if guild['repeat'] and after: if guild['repeat'] and after:
logging.debug("Repeating current track") logging.debug("[VC_EXT] Repeating current track")
next_track = guild['current_track'] next_track = guild['current_track']
elif guild['shuffle']: elif guild['shuffle']:
logging.debug("Shuffling tracks") logging.debug("[VC_EXT] Shuffling tracks")
next_track = self.db.get_random_track(gid) next_track = self.db.get_random_track(gid)
else: else:
logging.debug("Getting next track") logging.debug("[VC_EXT] Getting next track")
next_track = self.db.get_track(gid, 'next') next_track = self.db.get_track(gid, 'next')
if guild['current_track'] and guild['current_menu'] and not guild['repeat']: if guild['current_track'] and guild['current_menu'] and not guild['repeat']:
logging.debug("Adding current track to history") logging.debug("[VC_EXT] Adding current track to history")
self.db.modify_track(gid, guild['current_track'], 'previous', 'insert') self.db.modify_track(gid, guild['current_track'], 'previous', 'insert')
if next_track: if next_track:
@@ -578,17 +605,17 @@ class VoiceExtension:
prev_track = self.db.get_track(gid, 'previous') prev_track = self.db.get_track(gid, 'previous')
if not token: if not token:
logging.debug(f"No token found for user {ctx.user.id}") logging.debug(f"[VC_EXT] No token found for user {ctx.user.id}")
return None return None
if prev_track: if prev_track:
logging.debug("Previous track found") logging.debug("[VC_EXT] Previous track found")
track: dict[str, Any] | None = prev_track track: dict[str, Any] | None = prev_track
elif current_track: elif current_track:
logging.debug("No previous track found. Repeating current track") logging.debug("[VC_EXT] No previous track found. Repeating current track")
track = self.db.get_track(gid, 'current') track = self.db.get_track(gid, 'current')
else: else:
logging.debug("No previous or current track found") logging.debug("[VC_EXT] No previous or current track found")
track = None track = None
if track: if track:
@@ -624,16 +651,16 @@ class VoiceExtension:
current_track = self.db.get_track(gid, 'current') current_track = self.db.get_track(gid, 'current')
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_EXT] No token found for user {uid}")
return None return None
if not current_track: if not current_track:
logging.debug("Current track not found in 'get_likes'") logging.debug("[VC_EXT] Current track not found in 'get_likes'")
return None return None
client = await YMClient(token).init() client = await YMClient(token).init()
likes = await client.users_likes_tracks() likes = await client.users_likes_tracks()
if not likes: if not likes:
logging.debug("No likes found") logging.debug("[VC_EXT] No likes found")
return None return None
return likes.tracks return likes.tracks
@@ -648,13 +675,13 @@ class VoiceExtension:
str | None: Track title or None. str | None: Track title or None.
""" """
if not ctx.guild or not ctx.user: if not ctx.guild or not ctx.user:
logging.warning("Guild or User not found in context inside 'like_track'") logging.warning("[VC_EXT] Guild or User not found in context inside 'like_track'")
return None return None
current_track = self.db.get_track(ctx.guild.id, 'current') current_track = self.db.get_track(ctx.guild.id, 'current')
token = self.users_db.get_ym_token(ctx.user.id) token = self.users_db.get_ym_token(ctx.user.id)
if not current_track or not token: if not current_track or not token:
logging.debug("Current track or token not found in 'like_track'") logging.debug("[VC_EXT] Current track or token not found in 'like_track'")
return None return None
client = await YMClient(token).init() client = await YMClient(token).init()
@@ -668,11 +695,11 @@ class VoiceExtension:
) )
) )
if str(ym_track.id) not in [str(track.id) for track in likes]: if str(ym_track.id) not in [str(track.id) for track in likes]:
logging.debug("Track not found in likes. Adding...") logging.debug("[VC_EXT] Track not found in likes. Adding...")
await ym_track.like_async() await ym_track.like_async()
return ym_track.title return ym_track.title
else: else:
logging.debug("Track found in likes. Removing...") logging.debug("[VC_EXT] Track found in likes. Removing...")
if not client.me or not client.me.account or not client.me.account.uid: if not client.me or not client.me.account or not client.me.account.uid:
logging.debug("Client account not found") logging.debug("Client account not found")
return None return None
@@ -689,4 +716,35 @@ class VoiceExtension:
for _ in range(10): for _ in range(10):
if update: if update:
break break
await asyncio.sleep(0.25)
update = await self.update_menu_embed(ctx, menu_mid, button_callback) update = await self.update_menu_embed(ctx, menu_mid, button_callback)
async def init_ym_client(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, token: str | None = None) -> YMClient | None:
"""Initialize Yandex Music client. Return client on success. Return None if no token found and respond to the context.
Args:
ctx (ApplicationContext | Interaction): Context.
token (str | None, optional): Token. Defaults to None.
Returns:
YMClient | None: Client or None.
"""
if not token:
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
token = self.users_db.get_ym_token(uid) if uid else None
if not token:
logging.debug("No token found in 'init_ym_client'")
if not isinstance(ctx, discord.RawReactionActionEvent):
await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True)
return None
try:
client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError:
logging.debug("UnauthorizedError in 'init_ym_client'")
if not isinstance(ctx, discord.RawReactionActionEvent):
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
return None
return client

View File

@@ -5,10 +5,7 @@ from typing import cast
import discord import discord
from discord.ext.commands import Cog from discord.ext.commands import Cog
import yandex_music.exceptions from MusicBot.cogs.utils import VoiceExtension, menu_views
from yandex_music import ClientAsync
from MusicBot.cogs.utils import VoiceExtension
from MusicBot.ui import QueueView, generate_queue_embed from MusicBot.ui import QueueView, generate_queue_embed
def setup(bot: discord.Bot): def setup(bot: discord.Bot):
@@ -22,31 +19,39 @@ class Voice(Cog, VoiceExtension):
def __init__(self, bot: discord.Bot): def __init__(self, bot: discord.Bot):
VoiceExtension.__init__(self, bot) VoiceExtension.__init__(self, bot)
self.typed_bot: discord.Bot = bot self.typed_bot: discord.Bot = bot # should be removed later
@Cog.listener() @Cog.listener()
async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState) -> None: async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState) -> None:
logging.info(f"Voice state update for member {member.id} in guild {member.guild.id}") logging.info(f"[VOICE] Voice state update for member {member.id} in guild {member.guild.id}")
gid = member.guild.id gid = member.guild.id
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
discord_guild = await self.typed_bot.fetch_guild(gid) discord_guild = await self.typed_bot.fetch_guild(gid)
current_menu = self.db.get_current_menu(gid) current_menu = guild['current_menu']
channel = after.channel or before.channel channel = after.channel or before.channel
if not channel: if not channel:
logging.info(f"No channel found for member {member.id}") logging.info(f"[VOICE] No channel found for member {member.id}")
return return
vc = cast(discord.VoiceClient | None, discord.utils.get(self.typed_bot.voice_clients, guild=discord_guild)) vc = cast(discord.VoiceClient | None, discord.utils.get(self.typed_bot.voice_clients, guild=discord_guild))
if len(channel.members) == 1 and vc: if len(channel.members) == 1 and vc:
logging.info(f"Clearing history and stopping playback for guild {gid}") logging.info(f"[VOICE] Clearing history and stopping playback for guild {gid}")
self.db.update(gid, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True}) if guild['current_menu']:
message = self.typed_bot.get_message(guild['current_menu'])
if message:
await message.delete()
if member.guild.id in menu_views:
menu_views[member.guild.id].stop()
del menu_views[member.guild.id]
self.db.update(gid, {'previous_tracks': [], 'next_tracks': [], 'vibing': False, 'current_menu': None, 'repeat': False, 'shuffle': False})
vc.stop() vc.stop()
elif len(channel.members) > 2 and not guild['always_allow_menu']: elif len(channel.members) > 2 and not guild['always_allow_menu']:
if current_menu: if current_menu:
logging.info(f"Disabling current player for guild {gid} due to multiple members") logging.info(f"[VOICE] Disabling current menu for guild {gid} due to multiple members")
self.db.update(gid, {'current_menu': None, 'repeat': False, 'shuffle': False}) self.db.update(gid, {'current_menu': None, 'repeat': False, 'shuffle': False})
try: try:
@@ -55,10 +60,14 @@ class Voice(Cog, VoiceExtension):
await channel.send("Меню отключено из-за большого количества участников.", delete_after=15) await channel.send("Меню отключено из-за большого количества участников.", delete_after=15)
except (discord.NotFound, discord.Forbidden): except (discord.NotFound, discord.Forbidden):
pass pass
if member.guild.id in menu_views:
menu_views[member.guild.id].stop()
del menu_views[member.guild.id]
@Cog.listener() @Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None: async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None:
logging.info(f"Reaction added by user {payload.user_id} in channel {payload.channel_id}") logging.info(f"[VOICE] Reaction added by user {payload.user_id} in channel {payload.channel_id}")
if not self.typed_bot.user or not payload.member: if not self.typed_bot.user or not payload.member:
return return
@@ -86,24 +95,24 @@ class Voice(Cog, VoiceExtension):
votes = guild['votes'] votes = guild['votes']
if payload.message_id not in votes: if payload.message_id not in votes:
logging.info(f"Message {payload.message_id} not found in votes") logging.info(f"[VOICE] Message {payload.message_id} not found in votes")
return return
vote_data = votes[str(payload.message_id)] vote_data = votes[str(payload.message_id)]
if payload.emoji.name == '': if payload.emoji.name == '':
logging.info(f"User {payload.user_id} voted positively for message {payload.message_id}") logging.info(f"[VOICE] User {payload.user_id} voted positively for message {payload.message_id}")
vote_data['positive_votes'].append(payload.user_id) vote_data['positive_votes'].append(payload.user_id)
elif payload.emoji.name == '': elif payload.emoji.name == '':
logging.info(f"User {payload.user_id} voted negatively for message {payload.message_id}") logging.info(f"[VOICE] User {payload.user_id} voted negatively for message {payload.message_id}")
vote_data['negative_votes'].append(payload.user_id) vote_data['negative_votes'].append(payload.user_id)
total_members = len(channel.members) total_members = len(channel.members)
required_votes = 2 if total_members <= 5 else 4 if total_members <= 10 else 6 if total_members <= 15 else 9 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: if len(vote_data['positive_votes']) >= required_votes:
logging.info(f"Enough positive votes for message {payload.message_id}") logging.info(f"[VOICE] Enough positive votes for message {payload.message_id}")
if vote_data['action'] == 'next': if vote_data['action'] == 'next':
logging.info(f"Skipping track for message {payload.message_id}") logging.info(f"[VOICE] Skipping track for message {payload.message_id}")
self.db.update(guild_id, {'is_stopped': False}) self.db.update(guild_id, {'is_stopped': False})
title = await self.next_track(payload) title = await self.next_track(payload)
@@ -112,12 +121,12 @@ class Voice(Cog, VoiceExtension):
del votes[str(payload.message_id)] del votes[str(payload.message_id)]
elif vote_data['action'] == 'add_track': elif vote_data['action'] == 'add_track':
logging.info(f"Adding track for message {payload.message_id}") logging.info(f"[VOICE] Adding track for message {payload.message_id}")
await message.clear_reactions() await message.clear_reactions()
track = vote_data['vote_content'] track = vote_data['vote_content']
if not track: if not track:
logging.info(f"Recieved empty vote context for message {payload.message_id}") logging.info(f"[VOICE] Recieved empty vote context for message {payload.message_id}")
return return
self.db.update(guild_id, {'is_stopped': False}) self.db.update(guild_id, {'is_stopped': False})
@@ -132,13 +141,13 @@ class Voice(Cog, VoiceExtension):
del votes[str(payload.message_id)] del votes[str(payload.message_id)]
elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'): elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'):
logging.info(f"Performing '{vote_data['action']}' action for message {payload.message_id}") logging.info(f"[VOICE] Performing '{vote_data['action']}' action for message {payload.message_id}")
await message.clear_reactions() await message.clear_reactions()
tracks = vote_data['vote_content'] tracks = vote_data['vote_content']
if not tracks: if not tracks:
logging.info(f"Recieved empty vote context for message {payload.message_id}") logging.info(f"[VOICE] Recieved empty vote context for message {payload.message_id}")
return return
self.db.update(guild_id, {'is_stopped': False}) self.db.update(guild_id, {'is_stopped': False})
@@ -153,7 +162,7 @@ class Voice(Cog, VoiceExtension):
del votes[str(payload.message_id)] del votes[str(payload.message_id)]
elif len(vote_data['negative_votes']) >= required_votes: elif len(vote_data['negative_votes']) >= required_votes:
logging.info(f"Enough negative votes for message {payload.message_id}") logging.info(f"[VOICE] Enough negative votes for message {payload.message_id}")
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)]
@@ -162,7 +171,7 @@ class Voice(Cog, VoiceExtension):
@Cog.listener() @Cog.listener()
async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent) -> None: async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent) -> None:
logging.info(f"Reaction removed by user {payload.user_id} in channel {payload.channel_id}") logging.info(f"[VOICE] 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
@@ -182,23 +191,23 @@ class Voice(Cog, VoiceExtension):
vote_data = votes[str(payload.message_id)] vote_data = votes[str(payload.message_id)]
if payload.emoji.name == '✔️': if payload.emoji.name == '✔️':
logging.info(f"User {payload.user_id} removed positive vote for message {payload.message_id}") logging.info(f"[VOICE] User {payload.user_id} removed positive vote for message {payload.message_id}")
del vote_data['positive_votes'][payload.user_id] del vote_data['positive_votes'][payload.user_id]
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"[VOICE] 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="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.")
async def menu(self, ctx: discord.ApplicationContext) -> None: async def menu(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Menu command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Menu command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
guild = self.db.get_guild(ctx.guild.id) guild = self.db.get_guild(ctx.guild.id)
channel = cast(discord.VoiceChannel, ctx.channel) channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not guild['always_allow_menu']: if len(channel.members) > 2 and not guild['always_allow_menu']:
logging.info(f"Action declined: other members are present in the voice channel") logging.info(f"[VOICE] Action declined: other members are present in the voice channel")
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True) await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
return return
@@ -206,7 +215,7 @@ class Voice(Cog, VoiceExtension):
@voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.") @voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.")
async def join(self, ctx: discord.ApplicationContext) -> None: async def join(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Join command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Join command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
member = cast(discord.Member, ctx.author) member = cast(discord.Member, ctx.author)
if not member.guild_permissions.manage_channels: if not member.guild_permissions.manage_channels:
@@ -219,16 +228,16 @@ class Voice(Cog, VoiceExtension):
else: else:
response_message = "❌ Вы должны отправить команду в голосовом канале." response_message = "❌ Вы должны отправить команду в голосовом канале."
logging.info(f"Join command response: {response_message}") logging.info(f"[VOICE] Join command response: {response_message}")
await ctx.respond(response_message, delete_after=15, ephemeral=True) await ctx.respond(response_message, delete_after=15, ephemeral=True)
@voice.command(description="Заставить бота покинуть голосовой канал.") @voice.command(description="Заставить бота покинуть голосовой канал.")
async def leave(self, ctx: discord.ApplicationContext) -> None: async def leave(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Leave command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Leave command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
member = cast(discord.Member, ctx.author) member = cast(discord.Member, ctx.author)
if not member.guild_permissions.manage_channels: if not member.guild_permissions.manage_channels:
logging.info(f"User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild.id}") 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) await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return return
@@ -238,26 +247,26 @@ class Voice(Cog, VoiceExtension):
vc.stop() vc.stop()
await vc.disconnect(force=True) await vc.disconnect(force=True)
await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True) await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True)
logging.info(f"Successfully disconnected from voice channel in guild {ctx.guild.id}") logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild.id}")
@queue.command(description="Очистить очередь треков и историю прослушивания.") @queue.command(description="Очистить очередь треков и историю прослушивания.")
async def clear(self, ctx: discord.ApplicationContext) -> None: async def clear(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Clear queue command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Clear queue command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
member = cast(discord.Member, ctx.author) member = cast(discord.Member, ctx.author)
channel = cast(discord.VoiceChannel, ctx.channel) channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not member.guild_permissions.manage_channels: if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
logging.info(f"User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild.id}") 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) await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
elif await self.voice_check(ctx): elif await self.voice_check(ctx):
self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []}) 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"Queue and history cleared in guild {ctx.guild.id}") logging.info(f"[VOICE] Queue and history cleared in guild {ctx.guild.id}")
@queue.command(description="Получить очередь треков.") @queue.command(description="Получить очередь треков.")
async def get(self, ctx: discord.ApplicationContext) -> None: async def get(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Get queue command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Get queue 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
@@ -267,95 +276,91 @@ class Voice(Cog, VoiceExtension):
embed = generate_queue_embed(0, tracks) embed = generate_queue_embed(0, tracks)
await ctx.respond(embed=embed, view=QueueView(ctx), ephemeral=True) await ctx.respond(embed=embed, view=QueueView(ctx), ephemeral=True)
logging.info(f"Queue embed sent to user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Queue embed sent to user {ctx.author.id} in guild {ctx.guild.id}")
@track.command(description="Приостановить текущий трек.") @track.command(description="Приостановить текущий трек.")
async def pause(self, ctx: discord.ApplicationContext) -> None: async def pause(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Pause command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Pause command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
member = cast(discord.Member, ctx.author) member = cast(discord.Member, ctx.author)
channel = cast(discord.VoiceChannel, ctx.channel) channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not member.guild_permissions.manage_channels: if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
logging.info(f"User {ctx.author.id} does not have permissions to pause the track in guild {ctx.guild.id}") 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) 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: elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)) is not None:
if not vc.is_paused(): if not vc.is_paused():
vc.pause() vc.pause()
player = self.db.get_current_menu(ctx.guild.id) menu = self.db.get_current_menu(ctx.guild.id)
if player: if menu:
await self.update_menu_embed(ctx, player) await self.update_menu_embed(ctx, menu)
logging.info(f"Track paused in guild {ctx.guild.id}") logging.info(f"[VOICE] Track paused in guild {ctx.guild.id}")
await ctx.respond("Воспроизведение приостановлено.", delete_after=15, ephemeral=True) await ctx.respond("Воспроизведение приостановлено.", delete_after=15, ephemeral=True)
else: else:
logging.info(f"Track already paused in guild {ctx.guild.id}") logging.info(f"[VOICE] Track already paused in guild {ctx.guild.id}")
await ctx.respond("Воспроизведение уже приостановлено.", delete_after=15, ephemeral=True) await ctx.respond("Воспроизведение уже приостановлено.", delete_after=15, ephemeral=True)
@track.command(description="Возобновить текущий трек.") @track.command(description="Возобновить текущий трек.")
async def resume(self, ctx: discord.ApplicationContext) -> None: async def resume(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Resume command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Resume command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
member = cast(discord.Member, ctx.author) member = cast(discord.Member, ctx.author)
channel = cast(discord.VoiceChannel, ctx.channel) channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not member.guild_permissions.manage_channels: if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
logging.info(f"User {ctx.author.id} does not have permissions to resume the track in guild {ctx.guild.id}") 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) await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)): elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)):
if vc.is_paused(): if vc.is_paused():
vc.resume() vc.resume()
player = self.db.get_current_menu(ctx.guild.id) menu = self.db.get_current_menu(ctx.guild.id)
if player: if menu:
await self.update_menu_embed(ctx, player) await self.update_menu_embed(ctx, menu)
logging.info(f"Track resumed in guild {ctx.guild.id}") logging.info(f"[VOICE] Track resumed in guild {ctx.guild.id}")
await ctx.respond("Воспроизведение восстановлено.", delete_after=15, ephemeral=True) await ctx.respond("Воспроизведение восстановлено.", delete_after=15, ephemeral=True)
else: else:
logging.info(f"Track is not paused in guild {ctx.guild.id}") logging.info(f"[VOICE] Track is not paused in guild {ctx.guild.id}")
await ctx.respond("Воспроизведение не на паузе.", delete_after=15, ephemeral=True) await ctx.respond("Воспроизведение не на паузе.", delete_after=15, ephemeral=True)
@track.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.") @track.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.")
async def stop(self, ctx: discord.ApplicationContext) -> None: async def stop(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Stop command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Stop command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
member = cast(discord.Member, ctx.author) member = cast(discord.Member, ctx.author)
channel = cast(discord.VoiceChannel, ctx.channel) channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not member.guild_permissions.manage_channels: if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
logging.info(f"User {ctx.author.id} tried to stop playback in guild {ctx.guild.id} but there are other users in the channel") logging.info(f"[VOICE] User {ctx.author.id} tried to stop playback in guild {ctx.guild.id} but there are other users in the channel")
await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True) await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
elif await self.voice_check(ctx): elif await self.voice_check(ctx):
guild = self.db.get_guild(ctx.guild.id)
await self.stop_playing(ctx) await self.stop_playing(ctx)
current_menu = self.db.get_current_menu(ctx.guild.id) if guild['current_menu']:
if current_menu: menu = await self.get_menu_message(ctx, guild['current_menu'])
player = await self.get_menu_message(ctx, current_menu) if menu:
if player: await menu.delete()
await player.delete()
self.db.update(ctx.guild.id, { self.db.update(ctx.guild.id, {
'current_menu': None, 'repeat': False, 'shuffle': False, 'previous_tracks': [], 'next_tracks': [], 'vibing': False 'current_menu': None, 'repeat': False, 'shuffle': False, 'previous_tracks': [], 'next_tracks': [], 'vibing': False
}) })
logging.info(f"Playback stopped in guild {ctx.guild.id}") logging.info(f"[VOICE] Playback stopped in guild {ctx.guild.id}")
guild = self.db.get_guild(ctx.guild_id)
if guild['vibing']: if guild['vibing']:
user = self.users_db.get_user(ctx.user.id) user = self.users_db.get_user(ctx.user.id)
token = user['ym_token'] token = user['ym_token']
if not token: if not token:
logging.info(f"User {ctx.user.id} has no YM token") logging.info(f"[VOICE] User {ctx.user.id} has no YM token")
await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True) await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True)
return return
try: client = await self.init_ym_client(ctx, user['ym_token'])
client = await ClientAsync(token).init() if not client:
except yandex_music.exceptions.UnauthorizedError:
logging.info(f"User {ctx.user.id} provided invalid token")
await ctx.respond('❌ Недействительный токен.')
return return
track = guild['current_track'] track = guild['current_track']
@@ -369,20 +374,24 @@ class Voice(Cog, VoiceExtension):
cast(str, user['vibe_batch_id']), cast(str, user['vibe_batch_id']),
time() time()
) )
logging.info(f"User {ctx.user.id} finished vibing with result: {res}") logging.info(f"[VOICE] User {ctx.user.id} finished vibing with result: {res}")
if ctx.guild.id in menu_views:
menu_views[ctx.guild.id].stop()
del menu_views[ctx.guild.id]
await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True) await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True)
@track.command(description="Переключиться на следующую песню в очереди.") @track.command(description="Переключиться на следующую песню в очереди.")
async def next(self, ctx: discord.ApplicationContext) -> None: async def next(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Next command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Next 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
gid = ctx.guild.id gid = ctx.guild.id
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
if not guild['next_tracks']: if not guild['next_tracks']:
logging.info(f"No tracks in queue in guild {ctx.guild.id}") logging.info(f"[VOICE] No tracks in queue in guild {ctx.guild.id}")
await ctx.respond("❌ Нет песенен в очереди.", delete_after=15, ephemeral=True) await ctx.respond("❌ Нет песенен в очереди.", delete_after=15, ephemeral=True)
return return
@@ -390,7 +399,7 @@ class Voice(Cog, VoiceExtension):
channel = cast(discord.VoiceChannel, ctx.channel) channel = cast(discord.VoiceChannel, ctx.channel)
if guild['vote_next_track'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels: if guild['vote_next_track'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
logging.info(f"User {ctx.author.id} started vote to skip track in guild {ctx.guild.id}") 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)) message = cast(discord.Interaction, await ctx.respond(f"{member.mention} хочет пропустить текущий трек.\n\nВыполнить переход?", delete_after=30))
response = await message.original_response() response = await message.original_response()
@@ -410,7 +419,7 @@ class Voice(Cog, VoiceExtension):
} }
) )
else: else:
logging.info(f"Skipping vote for user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Skipping vote for user {ctx.author.id} in guild {ctx.guild.id}")
self.db.update(gid, {'is_stopped': False}) self.db.update(gid, {'is_stopped': False})
title = await self.next_track(ctx) title = await self.next_track(ctx)
@@ -418,14 +427,14 @@ class Voice(Cog, VoiceExtension):
@track.command(description="Добавить трек в избранное или убрать, если он уже там.") @track.command(description="Добавить трек в избранное или убрать, если он уже там.")
async def like(self, ctx: discord.ApplicationContext) -> None: async def like(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Like command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"[VOICE] Like 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
vc = await self.get_voice_client(ctx) vc = await self.get_voice_client(ctx)
if not vc or not vc.is_playing: if not vc or not vc.is_playing:
logging.info(f"No current track in {ctx.guild.id}") logging.info(f"[VOICE] No current track in {ctx.guild.id}")
await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True) await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
return return
@@ -434,15 +443,15 @@ class Voice(Cog, VoiceExtension):
logging.warning(f"Like command failed for user {ctx.author.id} in guild {ctx.guild.id}") logging.warning(f"Like command failed for user {ctx.author.id} in guild {ctx.guild.id}")
await ctx.respond("❌ Операция не удалась.", delete_after=15, ephemeral=True) await ctx.respond("❌ Операция не удалась.", delete_after=15, ephemeral=True)
elif result == 'TRACK REMOVED': elif result == 'TRACK REMOVED':
logging.info(f"Track removed from favorites for user {ctx.author.id} in guild {ctx.guild.id}") 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) await ctx.respond("Трек был удалён из избранного.", delete_after=15, ephemeral=True)
else: else:
logging.info(f"Track added to favorites for user {ctx.author.id} in guild {ctx.guild.id}") 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) await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True)
@track.command(name='vibe', description="Запустить мою волну по текущему треку.") @track.command(name='vibe', description="Запустить мою волну по текущему треку.")
async def track_vibe(self, ctx: discord.ApplicationContext) -> None: async def track_vibe(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"Vibe (track) command invoked by user {ctx.author.id} in guild {ctx.guild.id}") 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): if not await self.voice_check(ctx):
return return
@@ -450,11 +459,11 @@ class Voice(Cog, VoiceExtension):
channel = cast(discord.VoiceChannel, ctx.channel) channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not guild['always_allow_menu']: if len(channel.members) > 2 and not guild['always_allow_menu']:
logging.info(f"Action declined: other members are present in the voice channel") logging.info(f"[VOICE] Action declined: other members are present in the voice channel")
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True) await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
return return
if not guild['current_track']: if not guild['current_track']:
logging.info(f"No current track in {ctx.guild.id}") logging.info(f"[VOICE] No current track in {ctx.guild.id}")
await ctx.respond("❌ Нет воспроизводимого трека.", ephemeral=True) await ctx.respond("❌ Нет воспроизводимого трека.", ephemeral=True)
return return
@@ -463,7 +472,7 @@ class Voice(Cog, VoiceExtension):
@discord.slash_command(name='vibe', description="Запустить Мою Волну.") @discord.slash_command(name='vibe', description="Запустить Мою Волну.")
async def user_vibe(self, ctx: discord.ApplicationContext) -> None: 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}") logging.info(f"[VOICE] Vibe (user) command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
if not await self.voice_check(ctx): if not await self.voice_check(ctx):
return return
@@ -471,7 +480,7 @@ class Voice(Cog, VoiceExtension):
channel = cast(discord.VoiceChannel, ctx.channel) channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not guild['always_allow_menu']: if len(channel.members) > 2 and not guild['always_allow_menu']:
logging.info(f"Action declined: other members are present in the voice channel") logging.info(f"[VOICE] Action declined: other members are present in the voice channel")
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True) await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
return return

View File

@@ -1,6 +1,6 @@
"""This documents initialises databse and contains methods to access it.""" """This documents initialises databse and contains methods to access it."""
from typing import cast from typing import Any, cast
from pymongo import MongoClient from pymongo import MongoClient
from pymongo.collection import Collection from pymongo.collection import Collection
@@ -29,15 +29,20 @@ class BaseUsersDatabase:
queue_page=0, queue_page=0,
vibe_batch_id=None, vibe_batch_id=None,
vibe_type=None, vibe_type=None,
vibe_id=None vibe_id=None,
vibe_settings={
'mood': 'all',
'diversity': 'default',
'lang': 'any'
}
)) ))
def update(self, uid: int, data: User) -> None: def update(self, uid: int, data: User | dict[Any, Any]) -> None:
"""Update user record. """Update user record.
Args: Args:
uid (int): User id. uid (int): User id.
data (dict[Any, Any]): Updated data. data (User | dict[Any, Any]): Updated data.
""" """
self.get_user(uid) self.get_user(uid)
users.update_one({'_id': uid}, {"$set": data}) users.update_one({'_id': uid}, {"$set": data})
@@ -65,7 +70,12 @@ class BaseUsersDatabase:
queue_page=0, queue_page=0,
vibe_batch_id=None, vibe_batch_id=None,
vibe_type=None, vibe_type=None,
vibe_id=None vibe_id=None,
vibe_settings={
'mood': 'all',
'diversity': 'default',
'lang': 'any'
}
) )
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

@@ -133,24 +133,13 @@ class VoiceGuildsDatabase(BaseGuildsDatabase):
self.update(gid, {'next_tracks': tracks}) self.update(gid, {'next_tracks': tracks})
return track return track
def set_current_track(self, gid: int, track: Track | dict[str, Any]) -> None:
"""Set current track.
Args:
gid (int): Guild id.
track (Track | dict[str, Any]): Track or dictionary covertable to yandex_music.Track.
"""
if isinstance(track, Track):
track = track.to_dict()
self.update(gid, {'current_track': track})
def get_current_menu(self, gid: int) -> int | None: def get_current_menu(self, gid: int) -> int | None:
"""Get current player. """Get current menu.
Args: Args:
gid (int): Guild id. gid (int): Guild id.
Returns: int | None: Player message id or None if not present. Returns: int | None: Menu message id or None if not present.
""" """
guild = self.get_guild(gid) guild = self.get_guild(gid)
return guild['current_menu'] return guild['current_menu']

View File

@@ -1,4 +1,10 @@
from typing import TypedDict, Literal from typing import TypedDict, TypeAlias, Literal
VibeSettingsOptions: TypeAlias = Literal[
'active', 'fun', 'calm', 'sad', 'all',
'favorite', 'discover', 'popular', 'default',
'russian', 'not-russian', 'without-words', 'any',
]
class User(TypedDict, total=False): class User(TypedDict, total=False):
ym_token: str | None ym_token: str | None
@@ -8,6 +14,7 @@ class User(TypedDict, total=False):
vibe_batch_id: str | None vibe_batch_id: str | None
vibe_type: Literal['track', 'album', 'artist', 'playlist', 'user'] | None vibe_type: Literal['track', 'album', 'artist', 'playlist', 'user'] | None
vibe_id: str | int | None vibe_id: str | int | None
vibe_settings: dict[Literal['mood', 'diversity', 'lang'], VibeSettingsOptions]
class ExplicitUser(TypedDict): class ExplicitUser(TypedDict):
_id: int _id: int
@@ -18,3 +25,4 @@ class ExplicitUser(TypedDict):
vibe_batch_id: str | None vibe_batch_id: str | None
vibe_type: Literal['track', 'album', 'artist', 'playlist', 'user'] | None vibe_type: Literal['track', 'album', 'artist', 'playlist', 'user'] | None
vibe_id: str | int | None vibe_id: str | int | None
vibe_settings: dict[Literal['mood', 'diversity', 'lang'], VibeSettingsOptions]

View File

@@ -1,12 +1,12 @@
import logging import logging
from typing import Self, cast from typing import Self, cast
from discord.ui import View, Button, Item, Modal, Select 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
import yandex_music.exceptions 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, menu_views
class ToggleRepeatButton(Button, VoiceExtension): class ToggleRepeatButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -14,13 +14,17 @@ class ToggleRepeatButton(Button, VoiceExtension):
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
logging.info('Repeat button callback...') logging.info('[MENU] Repeat button callback...')
if not interaction.guild: if not await self.voice_check(interaction) or not interaction.guild:
return return
gid = interaction.guild.id gid = interaction.guild.id
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
self.db.update(gid, {'repeat': not guild['repeat']}) self.db.update(gid, {'repeat': not guild['repeat']})
await interaction.edit(view=await MenuView(interaction).init())
if gid in menu_views:
menu_views[gid].stop()
menu_views[gid] = await MenuView(interaction).init()
await interaction.edit(view=menu_views[gid])
class ToggleShuffleButton(Button, VoiceExtension): class ToggleShuffleButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -28,13 +32,17 @@ class ToggleShuffleButton(Button, VoiceExtension):
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
logging.info('Shuffle button callback...') logging.info('[MENU] Shuffle button callback...')
if not interaction.guild: if not await self.voice_check(interaction) or not interaction.guild:
return return
gid = interaction.guild.id gid = interaction.guild.id
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
self.db.update(gid, {'shuffle': not guild['shuffle']}) self.db.update(gid, {'shuffle': not guild['shuffle']})
await interaction.edit(view=await MenuView(interaction).init())
if gid in menu_views:
menu_views[gid].stop()
menu_views[gid] = await MenuView(interaction).init()
await interaction.edit(view=menu_views[gid])
class PlayPauseButton(Button, VoiceExtension): class PlayPauseButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -42,7 +50,7 @@ class PlayPauseButton(Button, VoiceExtension):
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
logging.info('Play/Pause button callback...') logging.info('[MENU] Play/Pause button callback...')
if not await self.voice_check(interaction): if not await self.voice_check(interaction):
return return
@@ -67,7 +75,7 @@ class NextTrackButton(Button, VoiceExtension):
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
logging.info('Next track button callback...') logging.info('[MENU] Next track button callback...')
if not await self.voice_check(interaction): if not await self.voice_check(interaction):
return return
title = await self.next_track(interaction, button_callback=True) title = await self.next_track(interaction, button_callback=True)
@@ -80,7 +88,7 @@ class PrevTrackButton(Button, VoiceExtension):
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
logging.info('Previous track button callback...') logging.info('[MENU] Previous track button callback...')
if not await self.voice_check(interaction): if not await self.voice_check(interaction):
return return
title = await self.prev_track(interaction, button_callback=True) title = await self.prev_track(interaction, button_callback=True)
@@ -93,15 +101,23 @@ class LikeButton(Button, VoiceExtension):
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
logging.info('Like button callback...') logging.info('[MENU] Like button callback...')
if not await self.voice_check(interaction): if not await self.voice_check(interaction):
return return
if not interaction.guild:
return
gid = interaction.guild.id
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing: if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True) await interaction.respond("❌ Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
await self.like_track(interaction) await self.like_track(interaction)
await interaction.edit(view=await MenuView(interaction).init())
if gid in menu_views:
menu_views[gid].stop()
menu_views[gid] = await MenuView(interaction).init()
await interaction.edit(view=menu_views[gid])
class LyricsButton(Button, VoiceExtension): class LyricsButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -109,7 +125,7 @@ class LyricsButton(Button, VoiceExtension):
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None: async def callback(self, interaction: Interaction) -> None:
logging.info('Lyrics button callback...') logging.info('[MENU] Lyrics button callback...')
if not await self.voice_check(interaction) or not interaction.guild_id or not interaction.user: if not await self.voice_check(interaction) or not interaction.guild_id or not interaction.user:
return return
@@ -127,12 +143,12 @@ class LyricsButton(Button, VoiceExtension):
try: try:
lyrics = await track.get_lyrics_async() lyrics = await track.get_lyrics_async()
except yandex_music.exceptions.NotFoundError: except yandex_music.exceptions.NotFoundError:
logging.debug('Lyrics not found') logging.debug('[MENU] Lyrics not found')
await interaction.respond("❌ Текст песни не найден. Яндекс нам соврал (опять)!", delete_after=15, ephemeral=True) await interaction.respond("❌ Текст песни не найден. Яндекс нам соврал (опять)!", delete_after=15, ephemeral=True)
return return
if not lyrics: if not lyrics:
logging.debug('Lyrics not found') logging.debug('[MENU] Lyrics not found')
return return
embed = Embed( embed = Embed(
@@ -160,7 +176,7 @@ class MyVibeButton(Button, VoiceExtension):
track = self.db.get_track(interaction.guild_id, 'current') track = self.db.get_track(interaction.guild_id, 'current')
if track: if track:
logging.info(f"[VIBE] Playing vibe for track '{track["id"]}'") logging.info(f"[MENU] Playing vibe for track '{track["id"]}'")
await self.update_vibe( await self.update_vibe(
interaction, interaction,
'track', 'track',
@@ -176,6 +192,119 @@ class MyVibeButton(Button, VoiceExtension):
button_callback=True button_callback=True
) )
class MyVibeSelect(Select, VoiceExtension):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
logging.info('[VIBE] My vibe select callback')
if not interaction.user:
logging.warning('[VIBE] No user in select callback')
return
custom_id = interaction.custom_id
if custom_id not in ('diversity', 'mood', 'lang'):
logging.warning(f'[VIBE] Unknown custom_id: {custom_id}')
return
data = interaction.data
if not data or 'values' not in data:
logging.warning('[VIBE] No data in select callback')
return
data_value = data['values'][0]
if data_value not in (
'fun', 'active', 'calm', 'sad', 'all',
'favorite', 'popular', 'discover', 'default',
'not-russian', 'russian', 'without-words', 'any'
):
logging.warning(f'[VIBE] Unknown data_value: {data_value}')
return
logging.info(f"[VIBE] Settings option '{custom_id}' updated to {data_value}")
self.users_db.update(interaction.user.id, {f'vibe_settings.{custom_id}': data_value})
view = MyVibeSettingsView(interaction)
view.disable_all_items()
await interaction.edit(view=view)
await self.update_vibe(interaction, 'user', 'onyourwave', update_settings=True)
view.enable_all_items()
await interaction.edit(view=view)
class MyVibeSettingsView(View, VoiceExtension):
def __init__(self, interaction: Interaction, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = False):
View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout)
VoiceExtension.__init__(self, None)
if not interaction.user:
logging.warning('[VIBE] No user in settings view')
return
settings = self.users_db.get_user(interaction.user.id)['vibe_settings']
diversity_settings = settings['diversity']
diversity = [
SelectOption(label='Любое', value='default'),
SelectOption(label='Любимое', value='favorite', default=diversity_settings == 'favorite'),
SelectOption(label='Незнакомое', value='discover', default=diversity_settings == 'discover'),
SelectOption(label='Популярное', value='popular', default=diversity_settings == 'popular')
]
mood_settings = settings['mood']
mood = [
SelectOption(label='Любое', value='all'),
SelectOption(label='Бодрое', value='active', default=mood_settings == 'active'),
SelectOption(label='Весёлое', value='fun', default=mood_settings == 'fun'),
SelectOption(label='Спокойное', value='calm', default=mood_settings == 'calm'),
SelectOption(label='Грустное', value='sad', default=mood_settings == 'sad')
]
lang_settings = settings['lang']
lang = [
SelectOption(label='Любое', value='any'),
SelectOption(label='Русский', value='russian', default=lang_settings == 'russian'),
SelectOption(label='Иностранный', value='not-russian', default=lang_settings == 'not-russian'),
SelectOption(label='Без слов', value='without-words', default=lang_settings == 'without-words')
]
feel_select = MyVibeSelect(
ComponentType.string_select,
placeholder='По характеру',
options=diversity,
row=0,
custom_id='diversity'
)
mood_select = MyVibeSelect(
ComponentType.string_select,
placeholder='По настроению',
options=mood,
row=1,
custom_id='mood'
)
lang_select = MyVibeSelect(
ComponentType.string_select,
placeholder='По языку',
options=lang,
row=2,
custom_id='lang'
)
for select in [feel_select, mood_select, lang_select]:
self.add_item(select)
class MyVibeSettingsButton(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 settings button callback')
if not await self.voice_check(interaction) or not interaction.user:
return
await interaction.respond('Настройки "Моей Волны"', view=MyVibeSettingsView(interaction), ephemeral=True)
class MenuView(View, VoiceExtension): class MenuView(View, VoiceExtension):
def __init__(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False): def __init__(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False):
@@ -195,6 +324,7 @@ 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) self.vibe_button = MyVibeButton(style=ButtonStyle.secondary, emoji='🌊', row=1)
self.vibe_settings_button = MyVibeSettingsButton(style=ButtonStyle.success, emoji='🛠', row=1)
async def init(self, *, disable: bool = False) -> Self: async def init(self, *, disable: bool = False) -> Self:
current_track = self.guild['current_track'] current_track = self.guild['current_track']
@@ -216,7 +346,11 @@ 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 self.guild['vibing']:
self.add_item(self.vibe_settings_button)
else:
self.add_item(self.vibe_button)
if disable: if disable:
self.disable_all_items() self.disable_all_items()
@@ -229,6 +363,7 @@ class MenuView(View, VoiceExtension):
return return
if self.guild['current_menu']: if self.guild['current_menu']:
await self.stop_playing(self.ctx)
self.db.update(self.ctx.guild_id, {'current_menu': None, 'previous_tracks': [], 'vibing': False}) 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: