mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-12 01:11:39 +03:00
impr: Enforce menu view, vote improvement and bug fixes.
This commit is contained in:
@@ -9,10 +9,10 @@ from yandex_music import Track, TrackShort, ClientAsync as YMClient
|
||||
|
||||
import discord
|
||||
from discord.ui import View
|
||||
from discord import Interaction, ApplicationContext, RawReactionActionEvent
|
||||
from discord import Interaction, ApplicationContext, RawReactionActionEvent, VoiceChannel
|
||||
|
||||
from MusicBot.cogs.utils import generate_item_embed
|
||||
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase, ExplicitGuild, ExplicitUser
|
||||
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase, ExplicitGuild, ExplicitUser, MessageVotes
|
||||
|
||||
menu_views: dict[int, View] = {} # Store menu views and delete them when needed to prevent memory leaks for after callbacks.
|
||||
|
||||
@@ -23,13 +23,16 @@ class VoiceExtension:
|
||||
self.db = VoiceGuildsDatabase()
|
||||
self.users_db = BaseUsersDatabase()
|
||||
|
||||
async def send_menu_message(self, ctx: ApplicationContext | Interaction, *, disable: bool = False) -> bool:
|
||||
async def send_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, *, disable: bool = False) -> bool:
|
||||
"""Send menu message to the channel and delete old menu message if exists. Return True if sent.
|
||||
|
||||
Args:
|
||||
ctx (ApplicationContext | Interaction): Context.
|
||||
disable (bool, optional): Disable menu message. Defaults to False.
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If bot instance is not set and ctx is RawReactionActionEvent.
|
||||
|
||||
Returns:
|
||||
bool: True if sent, False if not.
|
||||
"""
|
||||
@@ -65,7 +68,23 @@ class VoiceExtension:
|
||||
await message.delete()
|
||||
|
||||
await self._update_menu_views_dict(ctx, disable=disable)
|
||||
interaction = await ctx.respond(view=menu_views[ctx.guild_id], embed=embed)
|
||||
|
||||
if isinstance(ctx, (ApplicationContext, Interaction)):
|
||||
interaction = await ctx.respond(view=menu_views[ctx.guild_id], embed=embed)
|
||||
else:
|
||||
if not self.bot:
|
||||
raise ValueError("Bot instance is not set.")
|
||||
|
||||
channel = cast(VoiceChannel, self.bot.get_channel(ctx.channel_id))
|
||||
if not channel:
|
||||
logging.warning(f"[VC_EXT] Channel {ctx.channel_id} not found in guild {ctx.guild_id}")
|
||||
return False
|
||||
|
||||
interaction = await channel.send(
|
||||
view=menu_views[ctx.guild_id],
|
||||
embed=embed # type: ignore # Wrong typehints.
|
||||
)
|
||||
|
||||
response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction
|
||||
await self.db.update(ctx.guild_id, {'current_menu': response.id})
|
||||
|
||||
@@ -105,18 +124,17 @@ class VoiceExtension:
|
||||
await self.db.update(ctx.guild_id, {'current_menu': None})
|
||||
return None
|
||||
|
||||
if menu:
|
||||
logging.debug(f"[VC_EXT] Menu message {menu_mid} successfully fetched")
|
||||
else:
|
||||
if not menu:
|
||||
logging.debug(f"[VC_EXT] Menu message {menu_mid} not found in guild {ctx.guild_id}")
|
||||
await self.db.update(ctx.guild_id, {'current_menu': None})
|
||||
return None
|
||||
|
||||
logging.debug(f"[VC_EXT] Menu message {menu_mid} successfully fetched")
|
||||
return menu
|
||||
|
||||
async def update_menu_full(
|
||||
self,
|
||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
menu_mid: int | None = None,
|
||||
*,
|
||||
menu_message: discord.Message | None = None,
|
||||
button_callback: bool = False
|
||||
@@ -132,7 +150,7 @@ class VoiceExtension:
|
||||
Returns:
|
||||
bool: True if updated, False if not.
|
||||
"""
|
||||
logging.debug(
|
||||
logging.info(
|
||||
f"[VC_EXT] Updating menu embed using " + (
|
||||
"interaction context" if isinstance(ctx, Interaction) else
|
||||
"application context" if isinstance(ctx, ApplicationContext) else
|
||||
@@ -147,14 +165,11 @@ class VoiceExtension:
|
||||
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'update_menu_embed'")
|
||||
return False
|
||||
|
||||
guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_track': 1})
|
||||
|
||||
if not menu_message:
|
||||
if not menu_mid:
|
||||
logging.warning("[VC_EXT] No menu message or menu message id provided")
|
||||
return False
|
||||
menu_message = await self.get_menu_message(ctx, menu_mid)
|
||||
guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_menu': 1, 'current_track': 1})
|
||||
if not guild['current_menu']:
|
||||
return False
|
||||
|
||||
menu_message = await self.get_menu_message(ctx, guild['current_menu']) if not menu_message else menu_message
|
||||
if not menu_message:
|
||||
return False
|
||||
|
||||
@@ -168,6 +183,16 @@ class VoiceExtension:
|
||||
))
|
||||
embed = await generate_item_embed(track, guild['vibing'])
|
||||
|
||||
vc = await self.get_voice_client(ctx)
|
||||
if not vc:
|
||||
logging.warning("[VC_EXT] Voice client not found")
|
||||
return False
|
||||
|
||||
if vc.is_paused():
|
||||
embed.set_footer(text='Приостановлено')
|
||||
else:
|
||||
embed.remove_footer()
|
||||
|
||||
await self._update_menu_views_dict(ctx)
|
||||
try:
|
||||
if isinstance(ctx, Interaction) and button_callback:
|
||||
@@ -186,7 +211,6 @@ class VoiceExtension:
|
||||
async def update_menu_view(
|
||||
self,
|
||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
guild: ExplicitGuild,
|
||||
*,
|
||||
menu_message: discord.Message | None = None,
|
||||
button_callback: bool = False,
|
||||
@@ -206,6 +230,11 @@ class VoiceExtension:
|
||||
"""
|
||||
logging.debug("[VC_EXT] Updating menu view")
|
||||
|
||||
if not ctx.guild_id:
|
||||
logging.warning("[VC_EXT] Guild ID not found in context inside 'update_menu_view'")
|
||||
return False
|
||||
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={'current_menu': 1})
|
||||
if not guild['current_menu']:
|
||||
return False
|
||||
|
||||
@@ -217,10 +246,10 @@ class VoiceExtension:
|
||||
try:
|
||||
if isinstance(ctx, Interaction) and button_callback:
|
||||
# If interaction from menu buttons
|
||||
await ctx.edit(view=menu_views[guild['_id']])
|
||||
await ctx.edit(view=menu_views[ctx.guild_id])
|
||||
else:
|
||||
# If interaction from other buttons or commands. They should have their own response.
|
||||
await menu_message.edit(view=menu_views[guild['_id']])
|
||||
await menu_message.edit(view=menu_views[ctx.guild_id])
|
||||
except discord.NotFound:
|
||||
logging.warning("[VC_EXT] Menu message not found")
|
||||
return False
|
||||
@@ -340,7 +369,7 @@ class VoiceExtension:
|
||||
|
||||
if not isinstance(ctx.channel, discord.VoiceChannel):
|
||||
logging.debug("[VC_EXT] User is not in a voice channel")
|
||||
await ctx.respond("❌ Вы должны отправить команду в голосовом канале.", delete_after=15, ephemeral=True)
|
||||
await ctx.respond("❌ Вы должны отправить команду в чате голосового канала.", delete_after=15, ephemeral=True)
|
||||
return False
|
||||
|
||||
if ctx.user.id not in ctx.channel.voice_states:
|
||||
@@ -408,7 +437,7 @@ class VoiceExtension:
|
||||
retry: bool = False
|
||||
) -> str | None:
|
||||
"""Download ``track`` by its id and play it in the voice channel. Return track title on success.
|
||||
Send feedback for vibe track playing if vibing. Should be called if voice requirements are met.
|
||||
Send vibe feedback for playing track if vibing. Should be called when voice requirements are met.
|
||||
|
||||
Args:
|
||||
ctx (ApplicationContext | Interaction): Context.
|
||||
@@ -457,25 +486,31 @@ class VoiceExtension:
|
||||
|
||||
if menu_message or guild['current_menu']:
|
||||
# Updating menu message before playing to prevent delay and avoid FFMPEG lags.
|
||||
await self.update_menu_full(ctx, guild['current_menu'], menu_message=menu_message, button_callback=button_callback)
|
||||
await self.update_menu_full(ctx, menu_message=menu_message, button_callback=button_callback)
|
||||
|
||||
if not guild['vibing']:
|
||||
# Giving FFMPEG enough time to process the audio file
|
||||
await asyncio.sleep(1)
|
||||
|
||||
loop = self._get_current_event_loop(ctx)
|
||||
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop))
|
||||
try:
|
||||
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop))
|
||||
except discord.errors.ClientException as e:
|
||||
logging.error(f"[VC_EXT] Error while playing track '{track.title}': {e}")
|
||||
if not isinstance(ctx, RawReactionActionEvent):
|
||||
await ctx.respond(f"❌ Не удалось проиграть трек. Попробуйте сбросить меню.", delete_after=15, ephemeral=True)
|
||||
|
||||
logging.info(f"[VC_EXT] Playing track '{track.title}'")
|
||||
await self.db.update(gid, {'is_stopped': False})
|
||||
|
||||
if guild['vibing']:
|
||||
await self._my_vibe_send_start_feedback(ctx, track, uid)
|
||||
await self._my_vibe_start_feedback(ctx, track, uid)
|
||||
|
||||
return track.title
|
||||
|
||||
async def stop_playing(
|
||||
self, ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
self,
|
||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
*,
|
||||
vc: discord.VoiceClient | None = None,
|
||||
full: bool = False
|
||||
@@ -514,7 +549,7 @@ class VoiceExtension:
|
||||
return False
|
||||
|
||||
if guild['vibing'] and guild['current_track']:
|
||||
if not await self._my_vibe_send_stop_feedback(ctx, guild, user):
|
||||
if not await self._my_vibe_stop_feedback(ctx, guild, user):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -571,10 +606,10 @@ class VoiceExtension:
|
||||
await self.db.modify_track(gid, guild['current_track'], 'previous', 'insert')
|
||||
|
||||
if after and guild['current_menu']:
|
||||
await self.update_menu_view(ctx, guild, menu_message=menu_message, disable=True)
|
||||
await self.update_menu_view(ctx, menu_message=menu_message, disable=True)
|
||||
|
||||
if guild['vibing'] and guild['current_track'] and not isinstance(ctx, discord.RawReactionActionEvent):
|
||||
if not await self._send_next_vibe_feedback(ctx, guild, user, client, after=after):
|
||||
if not await self._my_vibe_feedback(ctx, guild, user, client, after=after):
|
||||
await ctx.respond("❌ Что-то пошло не так. Попробуйте снова.", ephemeral=True)
|
||||
return None
|
||||
|
||||
@@ -598,7 +633,7 @@ class VoiceExtension:
|
||||
next_track = await self.db.get_track(gid, 'next')
|
||||
|
||||
if next_track:
|
||||
title = await self._play_next_track(ctx, next_track, client=client, vc=vc, button_callback=button_callback)
|
||||
title = await self._play_track(ctx, next_track, client=client, vc=vc, button_callback=button_callback)
|
||||
|
||||
if after and not guild['current_menu']:
|
||||
if isinstance(ctx, discord.RawReactionActionEvent):
|
||||
@@ -618,7 +653,7 @@ class VoiceExtension:
|
||||
|
||||
return None
|
||||
|
||||
async def prev_track(self, ctx: ApplicationContext | Interaction, button_callback: bool = False) -> str | None:
|
||||
async def previous_track(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, button_callback: bool = False) -> str | None:
|
||||
"""Switch to the previous track in the queue. Repeat current track if no previous one found.
|
||||
Return track title on success.
|
||||
|
||||
@@ -629,12 +664,17 @@ class VoiceExtension:
|
||||
Returns:
|
||||
(str | None): Track title or None.
|
||||
"""
|
||||
if not ctx.guild or not ctx.user:
|
||||
logging.warning("Guild or User not found in context inside 'prev_track'")
|
||||
logging.debug("[VC_EXT] Switching to previous track")
|
||||
|
||||
gid = ctx.guild_id
|
||||
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
|
||||
|
||||
if not gid or not uid:
|
||||
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'next_track'")
|
||||
return None
|
||||
|
||||
current_track = await self.db.get_track(ctx.guild.id, 'current')
|
||||
prev_track = await self.db.get_track(ctx.guild.id, 'previous')
|
||||
current_track = await self.db.get_track(gid, 'current')
|
||||
prev_track = await self.db.get_track(gid, 'previous')
|
||||
|
||||
if prev_track:
|
||||
logging.debug("[VC_EXT] Previous track found")
|
||||
@@ -647,7 +687,7 @@ class VoiceExtension:
|
||||
track = None
|
||||
|
||||
if track:
|
||||
return await self._play_next_track(ctx, track, button_callback=button_callback)
|
||||
return await self._play_track(ctx, track, button_callback=button_callback)
|
||||
|
||||
return None
|
||||
|
||||
@@ -721,7 +761,8 @@ class VoiceExtension:
|
||||
add_func = client.users_dislikes_tracks_add
|
||||
remove_func = client.users_dislikes_tracks_remove
|
||||
|
||||
if not tracks:
|
||||
if tracks is None:
|
||||
logging.debug(f"[VC_EXT] No {action}s found")
|
||||
return (False, None)
|
||||
|
||||
if str(current_track['id']) not in [str(track.id) for track in tracks]:
|
||||
@@ -771,6 +812,82 @@ class VoiceExtension:
|
||||
self._ym_clients[token] = client
|
||||
return client
|
||||
|
||||
async def proccess_vote(self, ctx: RawReactionActionEvent, guild: ExplicitGuild, channel: VoiceChannel, vote_data: MessageVotes) -> bool:
|
||||
"""Proccess vote and perform action from `vote_data` and respond. Return True on success.
|
||||
|
||||
Args:
|
||||
ctx (RawReactionActionEvent): Context.
|
||||
guild (ExplicitGuild): Guild data.
|
||||
message (Message): Message.
|
||||
vote_data (MessageVotes): Vote data.
|
||||
|
||||
Returns:
|
||||
bool: Success status.
|
||||
"""
|
||||
logging.info(f"[VOICE] Performing '{vote_data['action']}' action for message {ctx.message_id}")
|
||||
|
||||
if not guild['current_menu']:
|
||||
await self.send_menu_message(ctx)
|
||||
|
||||
if vote_data['action'] in ('next', 'previous'):
|
||||
if not guild.get(f'{vote_data['action']}_tracks'):
|
||||
await channel.send(content=f"❌ Очередь пуста!", delete_after=15)
|
||||
|
||||
elif not (await self.next_track(ctx) if vote_data['action'] == 'next' else await self.previous_track(ctx)):
|
||||
await channel.send(content=f"❌ Ошибка при смене трека! Попробуйте ещё раз.", delete_after=15)
|
||||
|
||||
elif vote_data['action'] == 'add_track':
|
||||
track = vote_data['vote_content']
|
||||
if not track:
|
||||
logging.info(f"[VOICE] Recieved empty vote context for message {ctx.message_id}")
|
||||
return False
|
||||
|
||||
await self.db.modify_track(guild['_id'], track, 'next', 'append')
|
||||
|
||||
if guild['current_track']:
|
||||
await channel.send(content=f"✅ Трек был добавлен в очередь!", delete_after=15)
|
||||
else:
|
||||
if not await self.next_track(ctx):
|
||||
await channel.send(content=f"❌ Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15)
|
||||
|
||||
elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'):
|
||||
tracks = vote_data['vote_content']
|
||||
if not tracks:
|
||||
logging.info(f"[VOICE] Recieved empty vote context for message {ctx.message_id}")
|
||||
return False
|
||||
|
||||
await self.db.update(guild['_id'], {'is_stopped': False})
|
||||
await self.db.modify_track(guild['_id'], tracks, 'next', 'extend')
|
||||
|
||||
if guild['current_track']:
|
||||
await channel.send(content=f"✅ Контент был добавлен в очередь!", delete_after=15)
|
||||
else:
|
||||
if not await self.next_track(ctx):
|
||||
await channel.send(content=f"❌ Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15)
|
||||
|
||||
elif vote_data['action'] == 'play/pause':
|
||||
vc = await self.get_voice_client(ctx)
|
||||
if not vc:
|
||||
await channel.send(content=f"❌ Ошибка при изменении воспроизведения! Попробуйте ещё раз.", delete_after=15)
|
||||
return False
|
||||
|
||||
if vc.is_playing():
|
||||
vc.pause()
|
||||
else:
|
||||
vc.resume()
|
||||
|
||||
await self.update_menu_full(ctx)
|
||||
|
||||
elif vote_data['action'] in ('repeat', 'shuffle'):
|
||||
await self.db.update(guild['_id'], {vote_data['action']: not guild[vote_data['action']]})
|
||||
await self.update_menu_view(ctx)
|
||||
|
||||
else:
|
||||
logging.warning(f"[VOICE] Unknown action '{vote_data['action']}' for message {ctx.message_id}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def _update_menu_views_dict(
|
||||
self,
|
||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
@@ -836,7 +953,7 @@ class VoiceExtension:
|
||||
})
|
||||
return True
|
||||
|
||||
async def _my_vibe_send_start_feedback(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, track: Track, uid: int):
|
||||
async def _my_vibe_start_feedback(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, track: Track, uid: int):
|
||||
"""Send vibe start feedback to Yandex Music. Return True on success.
|
||||
|
||||
Args:
|
||||
@@ -861,7 +978,7 @@ class VoiceExtension:
|
||||
logging.debug(f"[VIBE] Track started feedback: {feedback}")
|
||||
return True
|
||||
|
||||
async def _my_vibe_send_stop_feedback(
|
||||
async def _my_vibe_stop_feedback(
|
||||
self,
|
||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
guild: ExplicitGuild,
|
||||
@@ -904,7 +1021,7 @@ class VoiceExtension:
|
||||
logging.info(f"[VOICE] User {user['_id']} finished vibing with result: {res}")
|
||||
return True
|
||||
|
||||
async def _send_next_vibe_feedback(
|
||||
async def _my_vibe_feedback(
|
||||
self,
|
||||
ctx: ApplicationContext | Interaction,
|
||||
guild: ExplicitGuild,
|
||||
@@ -926,6 +1043,7 @@ class VoiceExtension:
|
||||
Returns:
|
||||
bool: True on success, False otherwise.
|
||||
"""
|
||||
# TODO: Should be refactored to prevent duplication with `_my_vibe_stop_feedback` and `_my_vibe_start_feedback`
|
||||
logging.debug(f"[VC_EXT] Sending vibe feedback, after: {after}")
|
||||
|
||||
if not user['vibe_type'] or not user['vibe_id']:
|
||||
@@ -964,21 +1082,21 @@ class VoiceExtension:
|
||||
|
||||
return feedback
|
||||
|
||||
async def _play_next_track(
|
||||
async def _play_track(
|
||||
self,
|
||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
next_track: dict[str, Any],
|
||||
track: dict[str, Any],
|
||||
*,
|
||||
client: YMClient | None = None,
|
||||
vc: discord.VoiceClient | None = None,
|
||||
menu_message: discord.Message | None = None,
|
||||
button_callback: bool = False,
|
||||
) -> str | None:
|
||||
"""Play the `next_track` in the voice channel. Avoids additional button and vibe checks.
|
||||
"""Play `track` in the voice channel. Avoids additional vibe checks used in `next_track` and `previous_track`.
|
||||
|
||||
Args:
|
||||
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
|
||||
next_track (dict[str, Any]): Next track to play.
|
||||
track (dict[str, Any]): Track to play.
|
||||
vc (discord.VoiceClient | None, optional): Voice client. Defaults to None.
|
||||
menu_message (discord.Message | None, optional): Menu message to update. Defaults to None.
|
||||
button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False.
|
||||
@@ -986,6 +1104,7 @@ class VoiceExtension:
|
||||
Returns:
|
||||
str | None: Song title or None.
|
||||
"""
|
||||
# TODO: This should be refactored to avoid code duplication with `next_track` and `previous_track`.
|
||||
client = await self.init_ym_client(ctx) if not client else client
|
||||
|
||||
if not client:
|
||||
@@ -998,7 +1117,7 @@ class VoiceExtension:
|
||||
return None
|
||||
|
||||
ym_track = cast(Track, Track.de_json(
|
||||
next_track,
|
||||
track,
|
||||
client=client # type: ignore # Async client can be used here.
|
||||
))
|
||||
return await self.play_track(
|
||||
@@ -1010,7 +1129,7 @@ class VoiceExtension:
|
||||
)
|
||||
|
||||
def _get_current_event_loop(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> asyncio.AbstractEventLoop:
|
||||
"""Get the current event loop. If the context is a RawReactionActionEvent, get the loop from the bot.
|
||||
"""Get the current event loop. If the context is a RawReactionActionEvent, get the loop from the self.bot instance.
|
||||
|
||||
Args:
|
||||
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
|
||||
|
||||
Reference in New Issue
Block a user