impr: Async database and code optimization.

This commit is contained in:
Lemon4ksan
2025-02-03 22:31:06 +03:00
parent 09b28de205
commit 1ab823569e
11 changed files with 600 additions and 534 deletions

View File

@@ -23,7 +23,7 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]:
return []
users_db = BaseUsersDatabase()
token = users_db.get_ym_token(ctx.interaction.user.id)
token = await users_db.get_ym_token(ctx.interaction.user.id)
if not token:
logging.info(f"[GENERAL] User {ctx.interaction.user.id} has no token")
return []
@@ -186,21 +186,21 @@ class General(Cog):
about = cast(yandex_music.Status, client.me).to_dict()
uid = ctx.author.id
self.users_db.update(uid, {'ym_token': token})
await self.users_db.update(uid, {'ym_token': token})
logging.info(f"[GENERAL] Token saved for user {ctx.author.id}")
await ctx.respond(f'Привет, {about['account']['first_name']}!', delete_after=15, ephemeral=True)
@account.command(description="Удалить токен из датабазы бота.")
async def remove(self, ctx: discord.ApplicationContext) -> None:
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})
await self.users_db.update(ctx.user.id, {'ym_token': None})
await ctx.respond(f'Токен был удалён.', delete_after=15, ephemeral=True)
@account.command(description="Получить плейлист «Мне нравится»")
async def likes(self, ctx: discord.ApplicationContext) -> None:
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 = await self.users_db.get_ym_token(ctx.user.id)
if not token:
logging.info(f"[GENERAL] No token found for user {ctx.user.id}")
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
@@ -232,7 +232,7 @@ class General(Cog):
async def playlists(self, ctx: discord.ApplicationContext) -> None:
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 = await self.users_db.get_ym_token(ctx.user.id)
if not token:
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
return
@@ -247,11 +247,11 @@ class General(Cog):
(playlist.title if playlist.title else 'Без названия', playlist.track_count if playlist.track_count else 0) for playlist in playlists_list
]
self.users_db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0})
await self.users_db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0})
embed = generate_playlists_embed(0, playlists)
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=await MyPlaylists(ctx).init(), ephemeral=True)
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
@discord.option(
@@ -276,8 +276,8 @@ class General(Cog):
) -> None:
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)
token = self.users_db.get_ym_token(ctx.user.id)
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_explicit': 1})
token = await self.users_db.get_ym_token(ctx.user.id)
if not token:
logging.info(f"[GENERAL] No token found for user {ctx.user.id}")
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)

View File

@@ -19,7 +19,9 @@ class Settings(Cog):
@settings.command(name="show", description="Показать текущие настройки бота.")
async def show(self, ctx: discord.ApplicationContext) -> None:
guild = self.db.get_guild(ctx.guild.id)
guild = await self.db.get_guild(ctx.guild.id, projection={
'allow_explicit': 1, 'always_allow_menu': 1, 'vote_next_track': 1, 'vote_add_track': 1, 'vote_add_album': 1, 'vote_add_artist': 1, 'vote_add_playlist': 1
})
embed = discord.Embed(title="Настройки бота", color=0xfed42b)
explicit = "✅ - Разрешены" if guild['allow_explicit'] else "❌ - Запрещены"
@@ -44,8 +46,8 @@ class Settings(Cog):
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return
guild = self.db.get_guild(ctx.guild.id)
self.db.update(ctx.guild.id, {'allow_explicit': not guild['allow_explicit']})
guild = await self.db.get_guild(ctx.guild.id, projection={'allow_explicit': 1})
await self.db.update(ctx.guild.id, {'allow_explicit': not guild['allow_explicit']})
await ctx.respond(f"Треки с содержанием не для детей теперь {'разрешены' if not guild['allow_explicit'] else 'запрещены'}.", delete_after=15, ephemeral=True)
@settings.command(name="menu", description="Разрешить или запретить создание меню проигрывателя, даже если в канале больше одного человека.")
@@ -55,8 +57,8 @@ class Settings(Cog):
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return
guild = self.db.get_guild(ctx.guild.id)
self.db.update(ctx.guild.id, {'always_allow_menu': not guild['always_allow_menu']})
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1})
await self.db.update(ctx.guild.id, {'always_allow_menu': not guild['always_allow_menu']})
await ctx.respond(f"Меню проигрывателя теперь {'можно' if not guild['always_allow_menu'] else 'нельзя'} создавать в каналах с несколькими людьми.", delete_after=15, ephemeral=True)
@settings.command(name="vote", description="Настроить голосование.")
@@ -73,10 +75,10 @@ class Settings(Cog):
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return
guild = self.db.get_guild(ctx.guild.id)
guild = await self.db.get_guild(ctx.guild.id, projection={'vote_next_track': 1, 'vote_add_track': 1, 'vote_add_album': 1, 'vote_add_artist': 1, 'vote_add_playlist': 1})
if vote_type == '-Всё':
self.db.update(ctx.guild.id, {
await self.db.update(ctx.guild.id, {
'vote_next_track': False,
'vote_add_track': False,
'vote_add_album': False,
@@ -86,7 +88,7 @@ class Settings(Cog):
)
response_message = "Голосование выключено."
elif vote_type == '+Всё':
self.db.update(ctx.guild.id, {
await self.db.update(ctx.guild.id, {
'vote_next_track': True,
'vote_add_track': True,
'vote_add_album': True,
@@ -96,19 +98,19 @@ class Settings(Cog):
)
response_message = "Голосование включено."
elif vote_type == 'Переключение':
self.db.update(ctx.guild.id, {'vote_next_track': not guild['vote_next_track']})
await self.db.update(ctx.guild.id, {'vote_next_track': not guild['vote_next_track']})
response_message = "Голосование за переключение трека " + ("выключено." if guild['vote_next_track'] else "включено.")
elif vote_type == 'Трек':
self.db.update(ctx.guild.id, {'vote_add_track': not guild['vote_add_track']})
await self.db.update(ctx.guild.id, {'vote_add_track': not guild['vote_add_track']})
response_message = "Голосование за добавление трека " + ("выключено." if guild['vote_add_track'] else "включено.")
elif vote_type == 'Альбом':
self.db.update(ctx.guild.id, {'vote_add_album': not guild['vote_add_album']})
await self.db.update(ctx.guild.id, {'vote_add_album': not guild['vote_add_album']})
response_message = "Голосование за добавление альбома " + ("выключено." if guild['vote_add_album'] else "включено.")
elif vote_type == 'Артист':
self.db.update(ctx.guild.id, {'vote_add_artist': not guild['vote_add_artist']})
await self.db.update(ctx.guild.id, {'vote_add_artist': not guild['vote_add_artist']})
response_message = "Голосование за добавление артиста " + ("выключено." if guild['vote_add_artist'] else "включено.")
elif vote_type == 'Плейлист':
self.db.update(ctx.guild.id, {'vote_add_playlist': not guild['vote_add_playlist']})
await self.db.update(ctx.guild.id, {'vote_add_playlist': not guild['vote_add_playlist']})
response_message = "Голосование за добавление плейлиста " + ("выключено." if guild['vote_add_playlist'] else "включено.")
await ctx.respond(response_message, delete_after=15, ephemeral=True)

View File

@@ -11,7 +11,7 @@ from yandex_music import Track, Album, Artist, Playlist, Label
from discord import Embed
async def generate_item_embed(item: Track | Album | Artist | Playlist | list[Track], vibing: bool = False) -> Embed:
"""Generate item embed. list[Track] is used for likes.
"""Generate item embed. list[Track] is used for likes. If vibing is True, add vibing image.
Args:
item (yandex_music.Track | yandex_music.Album | yandex_music.Artist | yandex_music.Playlist): Item to be processed.
@@ -19,7 +19,7 @@ async def generate_item_embed(item: Track | Album | Artist | Playlist | list[Tra
Returns:
discord.Embed: Item embed.
"""
logging.debug(f"Generating embed for type: '{type(item).__name__}'")
logging.debug(f"[EMBEDS] Generating embed for type: '{type(item).__name__}'")
if isinstance(item, Track):
embed = await _generate_track_embed(item)

View File

@@ -27,6 +27,11 @@ class VoiceExtension:
self.users_db = BaseUsersDatabase()
async def send_menu_message(self, ctx: ApplicationContext | Interaction) -> None:
"""Send menu message to the channel. Delete old menu message if exists.
Args:
ctx (ApplicationContext | Interaction): Context.
"""
from MusicBot.ui import MenuView
logging.info("[VC_EXT] Sending menu message")
@@ -34,20 +39,23 @@ class VoiceExtension:
logging.warning("[VC_EXT] Guild id not found in context inside 'create_menu'")
return
guild = self.db.get_guild(ctx.guild_id)
embed = None
guild = await self.db.get_guild(ctx.guild_id, projection={'current_track': 1, 'current_menu': 1, 'vibing': 1})
if guild['current_track']:
track = cast(Track, Track.de_json(
track = cast(Track, await asyncio.to_thread(
Track.de_json,
guild['current_track'],
client=YMClient() # type: ignore # Async client can be used here.
YMClient() # type: ignore # Async client can be used here.
))
embed = await generate_item_embed(track, guild['vibing'])
vc = await self.get_voice_client(ctx)
if vc and vc.is_paused():
embed.set_footer(text='Приостановлено')
else:
embed.remove_footer()
else:
embed = None
if guild['current_menu']:
logging.info(f"[VC_EXT] Deleting old menu message {guild['current_menu']} in guild {ctx.guild_id}")
@@ -61,12 +69,12 @@ class VoiceExtension:
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})
await 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, menu_mid: int) -> discord.Message | None:
"""Fetch the menu message by its id. Return the message if found, None if not.
"""Fetch the menu message by its id. Return the message if found.
Reset `current_menu` field in the database if not found.
Args:
@@ -95,24 +103,26 @@ class VoiceExtension:
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
except discord.DiscordException as e:
logging.debug(f"[VC_EXT] Failed to get menu message: {e}")
self.db.update(ctx.guild_id, {'current_menu': None})
await self.db.update(ctx.guild_id, {'current_menu': None})
return None
if menu:
logging.debug("[VC_EXT] Menu message found")
else:
logging.debug("[VC_EXT] Menu message not found. Resetting current_menu field.")
self.db.update(ctx.guild_id, {'current_menu': None})
await self.db.update(ctx.guild_id, {'current_menu': None})
return menu
async def update_menu_embed(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
menu_mid: int,
menu_mid: int | None = None,
*,
menu_message: discord.Message | None = None,
button_callback: bool = False
) -> bool:
"""Update current menu message by its id. Return True if updated, False if not.
"""Update current menu message by its id. Return True if updated, False otherwise.
Args:
ctx (ApplicationContext | Interaction): Context.
@@ -137,24 +147,31 @@ class VoiceExtension:
if not gid or not uid:
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'update_menu_embed'")
return False
if not menu_message:
if not menu_mid:
logging.debug("[VC_EXT] No menu message or menu message id provided")
return False
menu = await self.get_menu_message(ctx, menu_mid)
else:
menu = menu_message
menu = await self.get_menu_message(ctx, menu_mid)
if not menu:
return False
token = self.users_db.get_ym_token(uid)
token = await self.users_db.get_ym_token(uid)
if not token:
logging.debug(f"[VC_EXT] No token found for user {uid}")
return False
guild = self.db.get_guild(gid)
current_track = guild['current_track']
if not current_track:
guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_track': 1})
if not guild['current_track']:
logging.debug("[VC_EXT] No current track found")
return False
track = cast(Track, Track.de_json(
current_track,
guild['current_track'],
client=YMClient(token) # type: ignore # Async client can be used here.
))
@@ -164,17 +181,21 @@ class VoiceExtension:
if gid in menu_views:
menu_views[gid].stop()
menu_views[gid] = await MenuView(ctx).init()
if isinstance(ctx, Interaction) and button_callback:
# If interaction from menu buttons
await ctx.edit(embed=embed, view=menu_views[gid])
else:
# If interaction from other buttons or commands. They should have their own response.
await menu.edit(embed=embed, view=menu_views[gid])
except discord.NotFound:
logging.warning("[VC_EXT] Menu message not found")
if gid in menu_views:
menu_views[gid].stop()
del menu_views[gid]
return False
logging.debug("[VC_EXT] Menu embed updated")
@@ -194,7 +215,8 @@ class VoiceExtension:
Args:
ctx (ApplicationContext | Interaction): Context.
type (Literal['track', 'album', 'artist', 'playlist', 'user']): Type of the item.
id (str | int): ID of the item.
id (str | int): ID of the YM item.
update_settings (bool, optional): Update vibe settings usind data from database. Defaults to False.
button_callback (bool, optional): If the function is called from button callback. Defaults to False.
Returns:
@@ -208,18 +230,17 @@ class VoiceExtension:
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'vibe_update'")
return None
user = self.users_db.get_user(uid)
user = await self.users_db.get_user(uid, projection={'ym_token': 1, 'vibe_settings': 1})
if not user['ym_token']:
logging.info(f"[VC_EXT] User {uid} has no YM token")
await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True)
return
return None
client = await self.init_ym_client(ctx, user['ym_token'])
if not client:
return
self.users_db.update(uid, {'vibe_type': type, 'vibe_id': id})
guild = self.db.get_guild(gid)
return None
guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_track': 1})
if not guild['vibing']:
feedback = await client.rotor_station_feedback_radio_started(
@@ -228,8 +249,10 @@ class VoiceExtension:
timestamp=time()
)
logging.debug(f"[VIBE] Radio started feedback: {feedback}")
if not feedback:
return None
tracks = await client.rotor_station_tracks(f"{type}:{id}")
self.db.update(gid, {'vibing': True})
if update_settings:
settings = user['vibe_settings']
@@ -259,17 +282,23 @@ class VoiceExtension:
if not tracks:
logging.warning("[VIBE] Failed to get next vibe tracks")
await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже.", ephemeral=True)
return
return None
logging.debug(f"[VIBE] Got next vibe tracks: {[track.track.title for track in tracks.sequence if track.track]}")
self.users_db.update(uid, {'vibe_batch_id': tracks.batch_id})
next_tracks = [cast(Track, track.track) for track in tracks.sequence]
self.db.update(gid, {
'next_tracks': [track.to_dict() for track in next_tracks[1:]],
'current_viber_id': uid
await self.users_db.update(uid, {
'vibe_type': type,
'vibe_id': id,
'vibe_batch_id': tracks.batch_id
})
await self.db.update(gid, {
'next_tracks': [track.to_dict() for track in next_tracks[1:]],
'current_viber_id': uid,
'vibing': True
})
await self.stop_playing(ctx)
return await self.play_track(ctx, next_tracks[0], button_callback=button_callback)
@@ -286,7 +315,7 @@ class VoiceExtension:
logging.warning("[VC_EXT] User or guild not found in context inside 'voice_check'")
return False
token = self.users_db.get_ym_token(ctx.user.id)
token = await self.users_db.get_ym_token(ctx.user.id)
if not token:
logging.debug(f"[VC_EXT] No token found for user {ctx.user.id}")
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
@@ -310,7 +339,7 @@ class VoiceExtension:
return False
if check_vibe_privilage:
guild = self.db.get_guild(ctx.guild.id)
guild = await self.db.get_guild(ctx.guild.id, projection={'current_viber_id': 1, 'vibing': 1})
member = cast(discord.Member, ctx.user)
if guild['vibing'] and ctx.user.id != guild['current_viber_id'] and not member.guild_permissions.manage_channels:
logging.debug("[VIBE] Context user is not the current viber")
@@ -363,7 +392,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.
If sound is already playing, add track id to the queue. There's no response to the context.
Sends feedback for vibe track playing. There's no response to the context.
Args:
ctx (ApplicationContext | Interaction): Context
@@ -376,18 +405,15 @@ class VoiceExtension:
Returns:
str | None: Song title or None.
"""
from MusicBot.ui import MenuView
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
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("Guild ID or User ID not found in context")
return None
vc = await self.get_voice_client(ctx) if not vc else vc
if not vc:
vc = await self.get_voice_client(ctx)
if not vc:
return None
return None
if isinstance(ctx, Interaction):
loop = ctx.client.loop
@@ -400,13 +426,13 @@ class VoiceExtension:
else:
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
self.db.update(gid, {'current_track': track.to_dict()})
guild = self.db.get_guild(gid)
await self.db.set_current_track(gid, track)
guild = await self.db.get_guild(gid, projection={'current_menu': 1, 'vibing': 1})
try:
await asyncio.gather(
track.download_async(f'music/{gid}.mp3'),
self._update_menu(ctx, guild, track, menu_message, button_callback)
self._download_track(gid, track),
self.update_menu_embed(ctx, guild['current_menu'], menu_message=menu_message, button_callback=button_callback)
)
except yandex_music.exceptions.TimedOutError:
logging.warning(f"[VC_EXT] Timed out while downloading track '{track.title}'")
@@ -419,17 +445,21 @@ class VoiceExtension:
async with aiofiles.open(f'music/{gid}.mp3', "rb") as f:
track_bytes = io.BytesIO(await f.read())
song = discord.FFmpegPCMAudio(track_bytes, pipe=True, options='-vn -filter:a "volume=0.15"')
if not guild['current_menu']:
await asyncio.sleep(1)
song = discord.FFmpegPCMAudio(track_bytes, pipe=True, options='-vn -b:a 64k -filter:a "volume=0.15"')
# Giving FFMPEG enough time to process the audio file
if not guild['vibing']:
await asyncio.sleep(0.75)
else:
await asyncio.sleep(0.25)
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop))
logging.info(f"[VC_EXT] Playing track '{track.title}'")
self.db.update(gid, {'is_stopped': False})
await self.db.update(gid, {'is_stopped': False})
if guild['vibing']:
user = self.users_db.get_user(uid)
user = await self.users_db.get_user(uid)
feedback = await cast(YMClient, track.client).rotor_station_feedback_track_started(
f"{user['vibe_type']}:{user['vibe_id']}",
track.id,
@@ -454,16 +484,16 @@ class VoiceExtension:
logging.warning("[VC_EXT] Guild ID not found in context")
return
guild = self.db.get_guild(gid)
guild = await self.db.get_guild(gid, projection={'current_menu': 1, 'current_track': 1, 'vibing': 1})
if gid in menu_views:
menu_views[gid].stop()
del menu_views[gid]
if not vc:
vc = await self.get_voice_client(ctx)
vc = await self.get_voice_client(ctx) if not vc else vc
if vc:
logging.debug("[VC_EXT] Stopping playback")
self.db.update(gid, {'current_track': None, 'is_stopped': True})
await self.db.update(gid, {'current_track': None, 'is_stopped': True})
vc.stop()
if full:
@@ -472,13 +502,13 @@ class VoiceExtension:
if menu:
await menu.delete()
self.db.update(gid, {
await self.db.update(gid, {
'current_menu': None, 'repeat': False, 'shuffle': False, 'previous_tracks': [], 'next_tracks': [], 'vibing': False
})
logging.info(f"[VOICE] Playback stopped in guild {gid}")
if guild['vibing']:
user = self.users_db.get_user(uid)
user = await self.users_db.get_user(uid)
token = user['ym_token']
if not token:
logging.info(f"[VOICE] User {uid} has no YM token")
@@ -539,8 +569,8 @@ class VoiceExtension:
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'next_track'")
return None
guild = self.db.get_guild(gid)
user = self.users_db.get_user(uid)
guild = await self.db.get_guild(gid, projection={'shuffle': 1, 'repeat': 1, 'is_stopped': 1, 'current_menu': 1, 'vibing': 1, 'current_track': 1})
user = await self.users_db.get_user(uid)
if not user['ym_token']:
logging.debug(f"[VC_EXT] No token found for user {uid}")
return None
@@ -602,14 +632,14 @@ class VoiceExtension:
next_track = guild['current_track']
elif guild['shuffle']:
logging.debug("[VC_EXT] Shuffling tracks")
next_track = self.db.get_random_track(gid)
next_track = await self.db.pop_random_track(gid, 'next')
else:
logging.debug("[VC_EXT] Getting next track")
next_track = self.db.get_track(gid, 'next')
next_track = await self.db.get_track(gid, 'next')
if guild['current_track'] and guild['current_menu'] and not guild['repeat']:
logging.debug("[VC_EXT] Adding current track to history")
self.db.modify_track(gid, guild['current_track'], 'previous', 'insert')
await self.db.modify_track(gid, guild['current_track'], 'previous', 'insert')
if next_track:
ym_track = Track.de_json(
@@ -643,8 +673,9 @@ class VoiceExtension:
button_callback=button_callback
)
logging.info("No next track found")
self.db.update(gid, {'is_stopped': True, 'current_track': None})
logging.info("[VIBE] No next track found")
if after:
await self.db.update(gid, {'is_stopped': True, 'current_track': None})
return None
async def prev_track(self, ctx: ApplicationContext | Interaction, button_callback: bool = False) -> str | None:
@@ -663,9 +694,10 @@ class VoiceExtension:
return None
gid = ctx.guild.id
token = self.users_db.get_ym_token(ctx.user.id)
current_track = self.db.get_track(gid, 'current')
prev_track = self.db.get_track(gid, 'previous')
token = await self.users_db.get_ym_token(ctx.user.id)
current_track = await self.db.get_track(gid, 'current')
prev_track = await self.db.get_track(gid, 'previous')
print(prev_track)
if not token:
logging.debug(f"[VC_EXT] No token found for user {ctx.user.id}")
@@ -676,7 +708,7 @@ class VoiceExtension:
track: dict[str, Any] | None = prev_track
elif current_track:
logging.debug("[VC_EXT] No previous track found. Repeating current track")
track = self.db.get_track(gid, 'current')
track = current_track
else:
logging.debug("[VC_EXT] No previous or current track found")
track = None
@@ -711,8 +743,8 @@ class VoiceExtension:
logging.warning("Guild ID or User ID not found in context inside 'play_track'")
return None
current_track = self.db.get_track(gid, 'current')
token = self.users_db.get_ym_token(uid)
current_track = await self.db.get_track(gid, 'current')
token = await self.users_db.get_ym_token(uid)
if not token:
logging.debug(f"[VC_EXT] No token found for user {uid}")
return None
@@ -741,8 +773,8 @@ class VoiceExtension:
logging.warning("[VC_EXT] Guild or User not found in context inside 'like_track'")
return None
current_track = self.db.get_track(ctx.guild.id, 'current')
token = self.users_db.get_ym_token(ctx.user.id)
current_track = await self.db.get_track(ctx.guild.id, 'current')
token = await self.users_db.get_ym_token(ctx.user.id)
if not current_track or not token:
logging.debug("[VC_EXT] Current track or token not found in 'like_track'")
return None
@@ -782,7 +814,7 @@ class VoiceExtension:
logging.warning("[VC_EXT] Guild or User not found in context inside 'dislike_track'")
return False
current_track = self.db.get_track(ctx.guild.id, 'current')
current_track = await self.db.get_track(ctx.guild.id, 'current')
if not current_track:
logging.debug("[VC_EXT] Current track not found in 'dislike_track'")
return False
@@ -797,42 +829,6 @@ class VoiceExtension:
)
return res
async def _retry_update_menu_embed(
self,
ctx: ApplicationContext | Interaction,
menu_mid: int,
button_callback: bool
) -> None:
update = await self.update_menu_embed(ctx, menu_mid, button_callback)
for _ in range(10):
if update:
break
await asyncio.sleep(0.25)
update = await self.update_menu_embed(ctx, menu_mid, button_callback)
async def _update_menu(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
guild: ExplicitGuild,
track: Track,
menu_message: discord.Message | None,
button_callback: bool
) -> None:
from MusicBot.ui import MenuView
gid = cast(int, ctx.guild_id)
if guild['current_menu'] and not isinstance(ctx, RawReactionActionEvent):
if menu_message:
try:
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:
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)
else:
await self._retry_update_menu_embed(ctx, guild['current_menu'], 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.
@@ -847,7 +843,7 @@ class VoiceExtension:
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
token = await self.users_db.get_ym_token(uid) if uid else None
if not token:
logging.debug("No token found in 'init_ym_client'")
@@ -855,6 +851,12 @@ class VoiceExtension:
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
return None
if not hasattr(self, '_ym_clients'):
self._ym_clients = {}
if token in self._ym_clients:
return self._ym_clients[token]
try:
client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError:
@@ -862,4 +864,26 @@ class VoiceExtension:
if not isinstance(ctx, discord.RawReactionActionEvent):
await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True)
return None
self._ym_clients[token] = client
return client
async def _retry_update_menu_embed(
self,
ctx: ApplicationContext | Interaction,
menu_mid: int,
button_callback: bool
) -> None:
update = await self.update_menu_embed(ctx, menu_mid, button_callback=button_callback)
for _ in range(10):
if update:
break
await asyncio.sleep(0.25)
update = await self.update_menu_embed(ctx, menu_mid, button_callback=button_callback)
async def _download_track(self, gid: int, track: Track) -> None:
try:
await track.download_async(f'music/{gid}.mp3')
except yandex_music.exceptions.TimedOutError:
logging.warning(f"[VC_EXT] Timeout downloading {track.title}")
raise

View File

@@ -25,7 +25,7 @@ class Voice(Cog, VoiceExtension):
logging.info(f"[VOICE] Voice state update for member {member.id} in guild {member.guild.id}")
gid = member.guild.id
guild = self.db.get_guild(gid)
guild = await self.db.get_guild(gid, projection={'current_menu': 1, 'always_allow_menu': 1})
discord_guild = await self.typed_bot.fetch_guild(gid)
current_menu = guild['current_menu']
@@ -46,13 +46,13 @@ class Voice(Cog, VoiceExtension):
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})
await self.db.update(gid, {'previous_tracks': [], 'next_tracks': [], 'vibing': False, 'current_menu': None, 'repeat': False, 'shuffle': False})
vc.stop()
elif len(channel.members) > 2 and not guild['always_allow_menu']:
if current_menu:
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, 'vibing': False})
await self.db.update(gid, {'current_menu': None, 'repeat': False, 'shuffle': False, 'vibing': False})
try:
message = await channel.fetch_message(current_menu)
await message.delete()
@@ -82,7 +82,7 @@ class Voice(Cog, VoiceExtension):
if not message or message.author.id != bot_id:
return
if not self.users_db.get_ym_token(payload.user_id):
if not await self.users_db.get_ym_token(payload.user_id):
await message.remove_reaction(payload.emoji, payload.member)
await channel.send("Для участия в голосовании необходимо авторизоваться через /account login.", delete_after=15)
return
@@ -90,7 +90,7 @@ class Voice(Cog, VoiceExtension):
guild_id = payload.guild_id
if not guild_id:
return
guild = self.db.get_guild(guild_id)
guild = await self.db.get_guild(guild_id, projection={'votes': 1, 'current_track': 1})
votes = guild['votes']
if payload.message_id not in votes:
@@ -113,7 +113,7 @@ class Voice(Cog, VoiceExtension):
if vote_data['action'] == 'next':
logging.info(f"[VOICE] Skipping track for message {payload.message_id}")
self.db.update(guild_id, {'is_stopped': False})
await self.db.update(guild_id, {'is_stopped': False})
title = await self.next_track(payload)
await message.clear_reactions()
await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15)
@@ -128,8 +128,8 @@ class Voice(Cog, VoiceExtension):
logging.info(f"[VOICE] Recieved empty vote context for message {payload.message_id}")
return
self.db.update(guild_id, {'is_stopped': False})
self.db.modify_track(guild_id, track, 'next', 'append')
await self.db.update(guild_id, {'is_stopped': False})
await self.db.modify_track(guild_id, track, 'next', 'append')
if guild['current_track']:
await message.edit(content=f"Трек был добавлен в очередь!", delete_after=15)
@@ -149,8 +149,8 @@ class Voice(Cog, VoiceExtension):
logging.info(f"[VOICE] Recieved empty vote context for message {payload.message_id}")
return
self.db.update(guild_id, {'is_stopped': False})
self.db.modify_track(guild_id, tracks, 'next', 'extend')
await self.db.update(guild_id, {'is_stopped': False})
await self.db.modify_track(guild_id, tracks, 'next', 'extend')
if guild['current_track']:
await message.edit(content=f"Контент был добавлен в очередь!", delete_after=15)
@@ -166,7 +166,7 @@ class Voice(Cog, VoiceExtension):
await message.edit(content='Запрос был отклонён.', delete_after=15)
del votes[str(payload.message_id)]
self.db.update(guild_id, {'votes': votes})
await self.db.update(guild_id, {'votes': votes})
@Cog.listener()
async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent) -> None:
@@ -177,7 +177,7 @@ class Voice(Cog, VoiceExtension):
guild_id = payload.guild_id
if not guild_id:
return
guild = self.db.get_guild(guild_id)
guild = await self.db.get_guild(guild_id, projection={'votes': 1})
votes = guild['votes']
channel = cast(discord.VoiceChannel, self.typed_bot.get_channel(payload.channel_id))
@@ -196,13 +196,15 @@ class Voice(Cog, VoiceExtension):
logging.info(f"[VOICE] User {payload.user_id} removed negative vote for message {payload.message_id}")
del vote_data['negative_votes'][payload.user_id]
self.db.update(guild_id, {'votes': votes})
await self.db.update(guild_id, {'votes': votes})
@voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.")
async def menu(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"[VOICE] Menu command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
if not await self.voice_check(ctx):
return
guild = self.db.get_guild(ctx.guild.id)
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1})
channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not guild['always_allow_menu']:
@@ -257,7 +259,7 @@ class Voice(Cog, VoiceExtension):
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)
elif await self.voice_check(ctx):
self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []})
await self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []})
await ctx.respond("Очередь и история сброшены.", delete_after=15, ephemeral=True)
logging.info(f"[VOICE] Queue and history cleared in guild {ctx.guild.id}")
@@ -267,11 +269,11 @@ class Voice(Cog, VoiceExtension):
if not await self.voice_check(ctx):
return
self.users_db.update(ctx.user.id, {'queue_page': 0})
await self.users_db.update(ctx.user.id, {'queue_page': 0})
tracks = self.db.get_tracks_list(ctx.guild.id, 'next')
tracks = await self.db.get_tracks_list(ctx.guild.id, 'next')
embed = generate_queue_embed(0, tracks)
await ctx.respond(embed=embed, view=QueueView(ctx), ephemeral=True)
await ctx.respond(embed=embed, view=await QueueView(ctx).init(), ephemeral=True)
logging.info(f"[VOICE] Queue embed sent to user {ctx.author.id} in guild {ctx.guild.id}")
@@ -290,7 +292,7 @@ class Voice(Cog, VoiceExtension):
if not vc.is_paused():
vc.pause()
menu = self.db.get_current_menu(ctx.guild.id)
menu = await self.db.get_current_menu(ctx.guild.id)
if menu:
await self.update_menu_embed(ctx, menu)
@@ -314,7 +316,7 @@ class Voice(Cog, VoiceExtension):
elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)):
if vc.is_paused():
vc.resume()
menu = self.db.get_current_menu(ctx.guild.id)
menu = await self.db.get_current_menu(ctx.guild.id)
if menu:
await self.update_menu_embed(ctx, menu)
logging.info(f"[VOICE] Track resumed in guild {ctx.guild.id}")
@@ -346,7 +348,7 @@ class Voice(Cog, VoiceExtension):
return
gid = ctx.guild.id
guild = self.db.get_guild(gid)
guild = await self.db.get_guild(gid, projection={'next_tracks': 1, 'vote_next_track': 1})
if not guild['next_tracks']:
logging.info(f"[VOICE] No tracks in queue in guild {ctx.guild.id}")
await ctx.respond("❌ Нет песенен в очереди.", delete_after=15, ephemeral=True)
@@ -364,7 +366,7 @@ class Voice(Cog, VoiceExtension):
await response.add_reaction('')
await response.add_reaction('')
self.db.update_vote(
await self.db.update_vote(
gid,
response.id,
{
@@ -378,7 +380,7 @@ class Voice(Cog, VoiceExtension):
else:
logging.info(f"[VOICE] Skipping vote for user {ctx.author.id} in guild {ctx.guild.id}")
self.db.update(gid, {'is_stopped': False})
await self.db.update(gid, {'is_stopped': False})
title = await self.next_track(ctx)
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
@@ -412,7 +414,7 @@ class Voice(Cog, VoiceExtension):
if not await self.voice_check(ctx):
return
guild = self.db.get_guild(ctx.guild.id)
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1, 'current_track': 1})
channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not guild['always_allow_menu']:
@@ -425,7 +427,9 @@ class Voice(Cog, VoiceExtension):
return
await self.send_menu_message(ctx)
await self.update_vibe(ctx, 'track', guild['current_track']['id'])
feedback = await self.update_vibe(ctx, 'track', guild['current_track']['id'])
if not feedback:
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True)
@voice.command(name='vibe', description="Запустить Мою Волну.")
async def user_vibe(self, ctx: discord.ApplicationContext) -> None:
@@ -433,7 +437,7 @@ class Voice(Cog, VoiceExtension):
if not await self.voice_check(ctx):
return
guild = self.db.get_guild(ctx.guild.id)
guild = await self.db.get_guild(ctx.guild.id, projection={'always_allow_menu': 1})
channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not guild['always_allow_menu']:
@@ -442,4 +446,6 @@ class Voice(Cog, VoiceExtension):
return
await self.send_menu_message(ctx)
await self.update_vibe(ctx, 'user', 'onyourwave')
feedback = await self.update_vibe(ctx, 'user', 'onyourwave')
if not feedback:
await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", ephemeral=True)