impr: Code and logs improvement.

This commit is contained in:
Lemon4ksan
2025-01-24 22:43:42 +03:00
parent 3f9698fa7b
commit 3adcde37eb
5 changed files with 240 additions and 151 deletions

View File

@@ -35,7 +35,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.debug(f"Help command invoked by {ctx.user.id} for command '{command}'") logging.info(f"Help command invoked by {ctx.user.id} for command '{command}'")
response_message = None response_message = None
embed = discord.Embed( embed = discord.Embed(
color=0xfed42b color=0xfed42b
@@ -109,32 +109,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.debug(f"Login command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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.debug(f"Invalid token provided by user {ctx.author.id}") logging.info(f"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.debug(f"Token saved for user {ctx.author.id}") logging.info(f"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.debug(f"Remove command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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.debug(f"Likes command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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.debug(f"No token found for user {ctx.user.id}") logging.info(f"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()
@@ -144,22 +144,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.debug(f"Failed to fetch likes for user {ctx.user.id}") logging.info(f"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.debug(f"Empty likes for user {ctx.user.id}") logging.info(f"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 = generate_likes_embed(tracks) embed = generate_likes_embed(tracks)
logging.debug(f"Successfully fetched likes for user {ctx.user.id}") logging.info(f"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}")
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:
await ctx.respond('❌ Необходимо указать свой токен доступа с помощью команды /login.', delete_after=15, ephemeral=True) await ctx.respond('❌ Необходимо указать свой токен доступа с помощью команды /login.', delete_after=15, ephemeral=True)
@@ -174,7 +175,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.debug(f"Successfully fetched playlists for user {ctx.user.id}") logging.info(f"Successfully fetched playlists for user {ctx.user.id}")
await ctx.respond(embed=embed, view=MyPlaylists(ctx), ephemeral=True) await ctx.respond(embed=embed, view=MyPlaylists(ctx), ephemeral=True)
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.") @discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
@@ -196,18 +197,18 @@ class General(Cog):
name: str, name: str,
content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'] = 'Трек' content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'] = 'Трек'
) -> None: ) -> None:
logging.debug(f"User {ctx.user.id} invoked find command for '{content_type}' with name '{name}'") logging.info(f"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.debug(f"No token found for user {ctx.user.id}") logging.info(f"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
try: try:
client = await YMClient(token).init() client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError: except yandex_music.exceptions.UnauthorizedError:
logging.debug(f"User {ctx.user.id} provided invalid token") logging.info(f"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
@@ -220,20 +221,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.debug(f"User {ctx.user.id} playlist '{name}' not found") logging.info(f"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.debug(f"User {ctx.user.id} playlist '{name}' is empty") logging.info(f"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.debug(f"User {ctx.user.id} playlist '{name}' contains explicit content and is not allowed on this server") logging.info(f"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
@@ -257,7 +258,7 @@ class General(Cog):
content = result.playlists content = result.playlists
if not content: if not content:
logging.debug(f"User {ctx.user.id} search for '{name}' returned no results") logging.info(f"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]
@@ -266,35 +267,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.debug(f"User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server") logging.info(f"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.debug(f"User {ctx.user.id} search for '{name}' returned no tracks") logging.info(f"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.debug(f"User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server") logging.info(f"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.debug(f"User {ctx.user.id} search for '{name}' returned no tracks") logging.info(f"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.debug(f"User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server") logging.info(f"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.debug(f"Successfully generated '{content_type}' message for user {ctx.author.id}") logging.info(f"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,5 +1,5 @@
import logging import logging
from typing import cast from typing import Literal, cast
import discord import discord
from yandex_music import Track, Album, Artist, Playlist from yandex_music import Track, Album, Artist, Playlist
@@ -31,6 +31,7 @@ class PlayButton(Button, VoiceExtension):
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
channel = cast(discord.VoiceChannel, interaction.channel) channel = cast(discord.VoiceChannel, interaction.channel)
member = cast(discord.Member, interaction.user) member = cast(discord.Member, interaction.user)
action: Literal['add_track', 'add_album', 'add_artist', 'add_playlist']
if isinstance(self.item, Track): if isinstance(self.item, Track):
tracks = [self.item] tracks = [self.item]
@@ -38,39 +39,46 @@ class PlayButton(Button, VoiceExtension):
vote_message = f"{member.mention} хочет добавить трек **{self.item.title}** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить трек **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
response_message = f"Трек **{self.item.title}** был добавлен в очередь." response_message = f"Трек **{self.item.title}** был добавлен в очередь."
play_message = f"Сейчас играет: **{self.item.title}**!" play_message = f"Сейчас играет: **{self.item.title}**!"
elif isinstance(self.item, Album): elif isinstance(self.item, Album):
album = await self.item.with_tracks_async() album = await self.item.with_tracks_async()
if not album or not album.volumes: if not album or not album.volumes:
logging.debug("Failed to fetch album tracks") logging.debug("Failed to fetch album tracks")
await interaction.respond("Не удалось получить треки альбома.", ephemeral=True) await interaction.respond("Не удалось получить треки альбома.", ephemeral=True)
return return
tracks = [track for volume in album.volumes for track in volume] tracks = [track for volume in album.volumes for track in volume]
action = 'add_album' action = 'add_album'
vote_message = f"{member.mention} хочет добавить альбом **{self.item.title}** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить альбом **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
response_message = f"Альбом **{self.item.title}** был добавлен в очередь." response_message = f"Альбом **{self.item.title}** был добавлен в очередь."
play_message = f"Сейчас играет: **{self.item.title}**!" play_message = f"Сейчас играет: **{self.item.title}**!"
elif isinstance(self.item, Artist): elif isinstance(self.item, Artist):
artist_tracks = await self.item.get_tracks_async() artist_tracks = await self.item.get_tracks_async()
if not artist_tracks: if not artist_tracks:
logging.debug("Failed to fetch artist tracks") logging.debug("Failed to fetch artist tracks")
await interaction.respond("Не удалось получить треки артиста.", ephemeral=True) await interaction.respond("Не удалось получить треки артиста.", ephemeral=True)
return return
tracks = artist_tracks.tracks.copy() tracks = artist_tracks.tracks.copy()
action = 'add_artist' action = 'add_artist'
vote_message = f"{member.mention} хочет добавить треки от **{self.item.name}** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить треки от **{self.item.name}** в очередь.\n\n Голосуйте за добавление."
response_message = f"Песни артиста **{self.item.name}** были добавлены в очередь." response_message = f"Песни артиста **{self.item.name}** были добавлены в очередь."
play_message = f"Сейчас играет: **{self.item.name}**!" play_message = f"Сейчас играет: **{self.item.name}**!"
elif isinstance(self.item, Playlist): elif isinstance(self.item, Playlist):
short_tracks = await self.item.fetch_tracks_async() short_tracks = await self.item.fetch_tracks_async()
if not short_tracks: if not short_tracks:
logging.debug("Failed to fetch playlist tracks") logging.debug("Failed to fetch playlist tracks")
await interaction.respond("Не удалось получить треки из плейлиста.", delete_after=15) await interaction.respond("Не удалось получить треки из плейлиста.", delete_after=15)
return return
tracks = [cast(Track, short_track.track) for short_track in short_tracks] tracks = [cast(Track, short_track.track) for short_track in short_tracks]
action = 'add_playlist' action = 'add_playlist'
vote_message = f"{member.mention} хочет добавить плейлист **{self.item.title}** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить плейлист **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
response_message = f"Плейлист **{self.item.title}** был добавлен в очередь." response_message = f"Плейлист **{self.item.title}** был добавлен в очередь."
play_message = f"Сейчас играет: **{self.item.title}**!" play_message = f"Сейчас играет: **{self.item.title}**!"
elif isinstance(self.item, list): elif isinstance(self.item, list):
tracks = self.item.copy() tracks = self.item.copy()
if not tracks: if not tracks:
@@ -81,16 +89,19 @@ class PlayButton(Button, VoiceExtension):
action = 'add_playlist' action = 'add_playlist'
vote_message = f"{member.mention} хочет добавить плейлист **** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить плейлист **** в очередь.\n\n Голосуйте за добавление."
response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь." response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь."
play_message = f"Сейчас играет: **{tracks[0].title}**!"
else: else:
raise ValueError(f"Unknown item type: '{type(self.item).__name__}'") raise ValueError(f"Unknown item type: '{type(self.item).__name__}'")
if guild.get(f'vote_{action}') and len(channel.members) > 2 and not member.guild_permissions.manage_channels: if guild.get(f'vote_{action}') and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
logging.debug(f"Starting vote for '{action}'") logging.debug(f"Starting vote for '{action}'")
message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30)) message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30))
response = await message.original_response() response = await message.original_response()
await response.add_reaction('') await response.add_reaction('')
await response.add_reaction('') await response.add_reaction('')
self.db.update_vote( self.db.update_vote(
gid, gid,
response.id, response.id,
@@ -104,27 +115,30 @@ class PlayButton(Button, VoiceExtension):
) )
else: else:
logging.debug(f"Skipping vote for '{action}'") logging.debug(f"Skipping vote for '{action}'")
if guild['current_track'] is not None: if guild['current_track'] is not None:
self.db.modify_track(gid, tracks, 'next', 'extend') self.db.modify_track(gid, tracks, 'next', 'extend')
response_message = response_message
else: else:
track = tracks.pop(0) track = tracks.pop(0)
self.db.modify_track(gid, tracks, 'next', 'extend') self.db.modify_track(gid, tracks, 'next', 'extend')
await self.play_track(interaction, track) await self.play_track(interaction, track)
response_message = play_message response_message = f"Сейчас играет: **{tracks[0].title}**!"
current_player = None
if guild['current_player']: if guild['current_player']:
current_player = await self.get_player_message(interaction, guild['current_player']) current_player = await self.get_player_message(interaction, guild['current_player'])
if current_player and interaction.message:
logging.debug(f"Deleting interaction message '{interaction.message.id}': current player '{current_player.id}' found")
await interaction.message.delete()
await interaction.respond(response_message, delete_after=15) if current_player and interaction.message:
logging.debug(f"Deleting interaction message '{interaction.message.id}': current player '{current_player.id}' found")
await interaction.message.delete()
else:
await interaction.respond(response_message, delete_after=15)
class ListenView(View): class ListenView(View):
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = None, disable_on_timeout: bool = False): def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = None, disable_on_timeout: bool = False):
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout) super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
logging.debug(f"Creating view for type: '{type(item).__name__}'") logging.debug(f"Creating view for type: '{type(item).__name__}'")
if isinstance(item, Track): if isinstance(item, Track):
link_app = f"yandexmusic://album/{item.albums[0].id}/track/{item.id}" link_app = f"yandexmusic://album/{item.albums[0].id}/track/{item.id}"
link_web = f"https://music.yandex.ru/album/{item.albums[0].id}/track/{item.id}" link_web = f"https://music.yandex.ru/album/{item.albums[0].id}/track/{item.id}"
@@ -140,9 +154,11 @@ class ListenView(View):
elif isinstance(item, list): # Can't open other person's likes elif isinstance(item, list): # Can't open other person's likes
self.add_item(PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray)) self.add_item(PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray))
return return
self.button1: Button = Button(label="Слушать в приложении", style=ButtonStyle.gray, url=link_app) self.button1: Button = Button(label="Слушать в приложении", style=ButtonStyle.gray, url=link_app)
self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web) self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web)
self.button3: PlayButton = PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray) self.button3: PlayButton = PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray)
if item.available: if item.available:
# self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason. # self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason.
self.add_item(self.button2) self.add_item(self.button2)
@@ -158,6 +174,7 @@ async def generate_item_embed(item: Track | Album | Artist | Playlist) -> Embed:
discord.Embed: Item embed. discord.Embed: Item embed.
""" """
logging.debug(f"Generating embed for type: '{type(item).__name__}'") logging.debug(f"Generating embed for type: '{type(item).__name__}'")
if isinstance(item, Track): if isinstance(item, Track):
return await generate_track_embed(item) return await generate_track_embed(item)
elif isinstance(item, Album): elif isinstance(item, Album):

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
import logging import logging
from typing import Literal, cast from typing import Any, Literal, cast
from yandex_music import Track, ClientAsync from yandex_music import Track, ClientAsync
@@ -23,7 +23,7 @@ class VoiceExtension:
Args: Args:
ctx (ApplicationContext | Interaction): Context. ctx (ApplicationContext | Interaction): Context.
player_mid (int): Id of the player message. There can only be only one player in the guild. player_mid (int): Id of the player message. There can only be only one player in the guild.
Returns: Returns:
bool: True if updated, False if not. bool: True if updated, False if not.
""" """
@@ -31,18 +31,18 @@ class VoiceExtension:
f"Updating player embed using " + f"Updating player embed using " +
"interaction context" if isinstance(ctx, Interaction) else "interaction context" if isinstance(ctx, Interaction) else
"application context" if isinstance(ctx, ApplicationContext) else "application context" if isinstance(ctx, ApplicationContext) else
"raw reaction context" + " ..." "raw reaction context"
) )
player = await self.get_player_message(ctx, player_mid)
if not player:
return False
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid: if not gid or not uid:
logging.warning("Guild ID or User ID not found in context") logging.warning("Guild ID or User ID not found in context inside 'update_player_embed'")
return False
player = await self.get_player_message(ctx, player_mid)
if not player:
return False return False
token = self.users_db.get_ym_token(uid) token = self.users_db.get_ym_token(uid)
@@ -120,9 +120,8 @@ class VoiceExtension:
Returns: Returns:
bool: Check result. bool: Check result.
""" """
logging.debug("Checking voice requirements...")
if not ctx.user: if not ctx.user:
logging.warning("User not found in context.") logging.warning("User 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)
@@ -159,14 +158,13 @@ class VoiceExtension:
Returns: Returns:
discord.VoiceClient | None: Voice client or None. discord.VoiceClient | None: Voice client or None.
""" """
logging.debug("Getting voice client...")
if isinstance(ctx, Interaction): if isinstance(ctx, Interaction):
voice_chat = discord.utils.get(ctx.client.voice_clients, guild=ctx.guild) voice_chat = discord.utils.get(ctx.client.voice_clients, guild=ctx.guild)
elif isinstance(ctx, RawReactionActionEvent): elif isinstance(ctx, RawReactionActionEvent):
if not self.bot: if not self.bot:
raise ValueError("Bot instance is not set.") raise ValueError("Bot instance is not set.")
if not ctx.guild_id: if not ctx.guild_id:
logging.warning("Guild ID not found in context") logging.warning("Guild ID not found in context inside get_voice_client")
return None return None
voice_chat = discord.utils.get(self.bot.voice_clients, guild=await self.bot.fetch_guild(ctx.guild_id)) voice_chat = discord.utils.get(self.bot.voice_clients, guild=await self.bot.fetch_guild(ctx.guild_id))
elif isinstance(ctx, ApplicationContext): elif isinstance(ctx, ApplicationContext):
@@ -175,19 +173,25 @@ class VoiceExtension:
raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.") raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.")
if voice_chat: if voice_chat:
logging.debug(f"Voice client found") logging.debug("Voice client found")
else: else:
logging.debug("Voice client not found") logging.debug("Voice client not found")
return cast((discord.VoiceClient | None), voice_chat) return cast((discord.VoiceClient | None), voice_chat)
async def play_track(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, track: Track) -> str | None: async def play_track(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
track: Track,
vc: discord.VoiceClient | None = None
) -> str | None:
"""Download ``track`` by its id and play it in the voice channel. Return track title on success. """Download ``track`` by its id and play it in the voice channel. Return track title on success.
If sound is already playing, add track id to the queue. There's no response to the context. If sound is already playing, add track id to the queue. There's no response to the context.
Args: Args:
ctx (ApplicationContext | Interaction): Context ctx (ApplicationContext | Interaction): Context
track (Track): Track to play. track (Track): Track to play.
vc (discord.VoiceClient | None): Voice client.
Returns: Returns:
str | None: Song title or None. str | None: Song title or None.
@@ -195,12 +199,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
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid: if not gid or not uid:
logging.warning("Guild ID or User ID not found in context") logging.warning("Guild ID or User ID not found in context inside 'play_track'")
return None return None
vc = await self.get_voice_client(ctx)
if not vc: if not vc:
return None vc = await self.get_voice_client(ctx)
if not vc:
return None
if isinstance(ctx, Interaction): if isinstance(ctx, Interaction):
loop = ctx.client.loop loop = ctx.client.loop
@@ -218,7 +223,7 @@ class VoiceExtension:
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"')
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.debug(f"Playing track '{track.title}'") logging.info(f"Playing track '{track.title}'")
self.db.set_current_track(gid, track) self.db.set_current_track(gid, track)
self.db.update(gid, {'is_stopped': False}) self.db.update(gid, {'is_stopped': False})
@@ -229,36 +234,44 @@ class VoiceExtension:
return track.title return track.title
async def stop_playing(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> None: async def stop_playing(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, vc: discord.VoiceClient | None = None) -> None:
logging.debug("Stopping playback...")
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
if not gid: if not gid:
logging.warning("Guild ID not found in context") logging.warning("Guild ID not found in context")
return return
vc = await self.get_voice_client(ctx) if not vc:
vc = await self.get_voice_client(ctx)
if vc: if vc:
logging.debug("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()
return
async def next_track(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, *, after: bool = False) -> str | None:
async def next_track(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
vc: discord.VoiceClient | None = None,
*,
after: bool = False
) -> str | None:
"""Switch to the next track in the queue. Return track title on success. """Switch to the next track in the queue. Return track title on success.
Doesn't change track if stopped. Stop playing if tracks list is empty. Doesn't change track if stopped. Stop playing if tracks list is empty.
Args: Args:
ctx (ApplicationContext | Interaction): Context ctx (ApplicationContext | Interaction): Context
vc (discord.VoiceClient, optional): Voice client.
after (bool, optional): Whether the function is being called by the after callback. Defaults to False. after (bool, optional): Whether the function is being called by the after callback. Defaults to False.
Returns: Returns:
str | None: Track title or None. str | None: Track title or None.
""" """
logging.debug("Switching to the next track")
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid: if not gid or not uid:
logging.warning("Guild ID or User ID not found in context.") logging.warning("Guild ID or User ID not found in context inside 'next_track'")
return return None
guild = self.db.get_guild(gid) guild = self.db.get_guild(gid)
token = self.users_db.get_ym_token(uid) token = self.users_db.get_ym_token(uid)
@@ -270,11 +283,13 @@ class VoiceExtension:
logging.debug("Playback is stopped, skipping...") logging.debug("Playback is stopped, skipping...")
return None return None
if not await self.get_voice_client(ctx): # Silently return if bot got kicked if not vc:
logging.debug("Voice client not found") vc = await self.get_voice_client(ctx)
return None if not vc: # Silently return if bot got kicked
return None
if guild['repeat'] and after: if guild['repeat'] and after:
logging.debug("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("Shuffling tracks")
@@ -283,7 +298,8 @@ class VoiceExtension:
logging.debug("Getting next track") logging.debug("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_player']: if guild['current_track'] and guild['current_player'] and not guild['repeat']:
logging.debug("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:
@@ -291,19 +307,20 @@ class VoiceExtension:
next_track, next_track,
client=ClientAsync(token) # type: ignore # Async client can be used here. client=ClientAsync(token) # type: ignore # Async client can be used here.
) )
await self.stop_playing(ctx) await self.stop_playing(ctx, vc)
title = await self.play_track( title = await self.play_track(
ctx, ctx,
ym_track # type: ignore # de_json should always work here. ym_track, # type: ignore # de_json should always work here.
vc
) )
if after and not guild['current_player'] and not isinstance(ctx, discord.RawReactionActionEvent): if after and not guild['current_player'] and not isinstance(ctx, discord.RawReactionActionEvent):
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15) await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
return title return title
else:
self.db.update(gid, {'is_stopped': True, 'current_track': None}) logging.info("No next track found")
self.db.update(gid, {'is_stopped': True, 'current_track': None})
return None return None
async def prev_track(self, ctx: ApplicationContext | Interaction) -> str | None: async def prev_track(self, ctx: ApplicationContext | Interaction) -> str | None:
@@ -316,9 +333,8 @@ class VoiceExtension:
Returns: Returns:
str | None: Track title or None. str | None: Track title or None.
""" """
logging.debug("Switching to the previous track")
if not ctx.guild or not ctx.user: if not ctx.guild or not ctx.user:
logging.debug("Guild or User not found in context") logging.warning("Guild or User not found in context inside 'prev_track'")
return None return None
gid = ctx.guild.id gid = ctx.guild.id
@@ -328,11 +344,11 @@ class VoiceExtension:
if not token: if not token:
logging.debug(f"No token found for user {ctx.user.id}") logging.debug(f"No token found for user {ctx.user.id}")
return return None
if prev_track: if prev_track:
logging.debug("Previous track found") logging.debug("Previous track found")
track = 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("No previous track found. Repeating current track")
track = self.db.get_track(gid, 'current') track = self.db.get_track(gid, 'current')
@@ -363,7 +379,7 @@ 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.") logging.warning("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')

View File

@@ -22,50 +22,50 @@ 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.bot = bot self.typed_bot: discord.Bot = bot
@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.debug(f"Voice state update for member {member.id} in guild {member.guild.id}") logging.info(f"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)
channel = after.channel or before.channel channel = after.channel or before.channel
if not channel: if not channel:
logging.debug(f"No channel found for member {member.id}") logging.info(f"No channel found for member {member.id}")
return return
discord_guild = await self.bot.fetch_guild(gid) 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.bot.voice_clients, guild=discord_guild))
if len(channel.members) == 1 and vc: if len(channel.members) == 1 and vc:
logging.debug(f"Clearing history and stopping playback for guild {gid}") logging.info(f"Clearing history and stopping playback for guild {gid}")
self.db.clear_history(gid) self.db.update(gid, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True})
self.db.update(gid, {'current_track': None, 'is_stopped': True})
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']:
current_player = self.db.get_current_player(gid) current_player = self.db.get_current_player(gid)
if current_player: if current_player:
logging.debug(f"Disabling current player for guild {gid} due to multiple members") logging.info(f"Disabling current player for guild {gid} due to multiple members")
self.db.update(gid, {'current_player': None, 'repeat': False, 'shuffle': False}) self.db.update(gid, {'current_player': None, 'repeat': False, 'shuffle': False})
try: try:
message = await channel.fetch_message(current_player) message = await channel.fetch_message(current_player)
await message.delete() await message.delete()
except (discord.NotFound, discord.Forbidden): except (discord.NotFound, discord.Forbidden):
pass pass
await channel.send("Текущий плеер отключён, так как в канале больше одного человека.", delete_after=15)
@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.debug(f"Reaction added by user {payload.user_id} in channel {payload.channel_id}") logging.info(f"Reaction added by user {payload.user_id} in channel {payload.channel_id}")
if not self.bot.user or not payload.member: if not self.typed_bot.user or not payload.member:
return return
bot_id = self.bot.user.id bot_id = self.typed_bot.user.id
if payload.user_id == bot_id: if payload.user_id == bot_id:
return return
channel = cast(discord.VoiceChannel, self.bot.get_channel(payload.channel_id)) channel = cast(discord.VoiceChannel, self.typed_bot.get_channel(payload.channel_id))
if not channel: if not channel:
return return
@@ -86,55 +86,69 @@ 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.debug(f"User {payload.user_id} voted positively for message {payload.message_id}") logging.info(f"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.debug(f"User {payload.user_id} voted negatively for message {payload.message_id}") logging.info(f"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.debug(f"Enough positive votes for message {payload.message_id}") logging.info(f"Enough positive votes for message {payload.message_id}")
if vote_data['action'] == 'next': if vote_data['action'] == 'next':
logging.debug(f"Skipping track for message {payload.message_id}") logging.info(f"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)
await message.clear_reactions() await message.clear_reactions()
await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15) await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15)
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.debug(f"Adding track for message {payload.message_id}") logging.info(f"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.debug(f"Recieved empty vote context for message {payload.message_id}") logging.info(f"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})
self.db.modify_track(guild_id, track, 'next', 'append') self.db.modify_track(guild_id, track, 'next', 'append')
if guild['current_track']: if guild['current_track']:
await message.edit(content=f"Трек был добавлен в очередь!", delete_after=15) await message.edit(content=f"Трек был добавлен в очередь!", delete_after=15)
else: else:
title = await self.next_track(payload) title = await self.next_track(payload)
await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15) await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15)
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.debug(f"Performing '{vote_data['action']}' action for message {payload.message_id}") logging.info(f"Performing '{vote_data['action']}' action for message {payload.message_id}")
tracks = vote_data['vote_content']
await message.clear_reactions() await message.clear_reactions()
tracks = vote_data['vote_content']
if not tracks: if not tracks:
logging.debug(f"Recieved empty vote context for message {payload.message_id}") logging.info(f"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})
self.db.modify_track(guild_id, tracks, 'next', 'extend') self.db.modify_track(guild_id, tracks, 'next', 'extend')
if guild['current_track']: if guild['current_track']:
await message.edit(content=f"Контент был добавлен в очередь!", delete_after=15) await message.edit(content=f"Контент был добавлен в очередь!", delete_after=15)
else: else:
title = await self.next_track(payload) title = await self.next_track(payload)
await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15) await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15)
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.debug(f"Enough negative votes for message {payload.message_id}") logging.info(f"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)]
@@ -143,8 +157,8 @@ 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.debug(f"Reaction removed by user {payload.user_id} in channel {payload.channel_id}") logging.info(f"Reaction removed by user {payload.user_id} in channel {payload.channel_id}")
if not self.bot.user: if not self.typed_bot.user:
return return
guild_id = payload.guild_id guild_id = payload.guild_id
@@ -153,27 +167,27 @@ class Voice(Cog, VoiceExtension):
guild = self.db.get_guild(guild_id) guild = self.db.get_guild(guild_id)
votes = guild['votes'] votes = guild['votes']
channel = cast(discord.VoiceChannel, self.bot.get_channel(payload.channel_id)) channel = cast(discord.VoiceChannel, self.typed_bot.get_channel(payload.channel_id))
if not channel: if not channel:
return return
message = await channel.fetch_message(payload.message_id) message = await channel.fetch_message(payload.message_id)
if not message or message.author.id != self.bot.user.id: if not message or message.author.id != self.typed_bot.user.id:
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.debug(f"User {payload.user_id} removed positive vote for message {payload.message_id}") logging.info(f"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.debug(f"User {payload.user_id} removed negative vote for message {payload.message_id}") logging.info(f"User {payload.user_id} removed negative vote for message {payload.message_id}")
del vote_data['negative_votes'][payload.user_id] del vote_data['negative_votes'][payload.user_id]
self.db.update(guild_id, {'votes': votes}) self.db.update(guild_id, {'votes': votes})
@voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.") @voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.")
async def menu(self, ctx: discord.ApplicationContext) -> None: async def menu(self, ctx: discord.ApplicationContext) -> None:
logging.debug(f"Menu command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"Menu 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
@@ -182,7 +196,7 @@ class Voice(Cog, VoiceExtension):
embed = None embed = None
if len(channel.members) > 2 and not guild['always_allow_menu']: if len(channel.members) > 2 and not guild['always_allow_menu']:
logging.debug(f"Action declined: other members are present in the voice channel") logging.info(f"Action declined: other members are present in the voice channel")
await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True) await ctx.respond("❌ Вы не единственный в голосовом канале.", ephemeral=True)
return return
@@ -200,7 +214,7 @@ class Voice(Cog, VoiceExtension):
embed.remove_footer() embed.remove_footer()
if guild['current_player']: if guild['current_player']:
logging.debug(f"Deleteing old player menu {guild['current_player']} in guild {ctx.guild.id}") logging.info(f"Deleteing old player menu {guild['current_player']} in guild {ctx.guild.id}")
message = await ctx.fetch_message(guild['current_player']) message = await ctx.fetch_message(guild['current_player'])
await message.delete() await message.delete()
@@ -208,14 +222,16 @@ class Voice(Cog, VoiceExtension):
response = await interaction.original_response() response = await interaction.original_response()
self.db.update(ctx.guild.id, {'current_player': response.id}) self.db.update(ctx.guild.id, {'current_player': response.id})
logging.info(f"New player menu {response.id} created in guild {ctx.guild.id}")
@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.debug(f"Join command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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)
vc = await self.get_voice_client(ctx)
if not member.guild_permissions.manage_channels: if not member.guild_permissions.manage_channels:
response_message = "У вас нет прав для выполнения этой команды." response_message = "У вас нет прав для выполнения этой команды."
elif vc and vc.is_connected(): elif (vc := await self.get_voice_client(ctx)) and vc.is_connected():
response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave." response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave."
elif isinstance(ctx.channel, discord.VoiceChannel): elif isinstance(ctx.channel, discord.VoiceChannel):
await ctx.channel.connect(timeout=15) await ctx.channel.connect(timeout=15)
@@ -223,117 +239,155 @@ class Voice(Cog, VoiceExtension):
else: else:
response_message = "❌ Вы должны отправить команду в голосовом канале." response_message = "❌ Вы должны отправить команду в голосовом канале."
logging.info(f"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.debug(f"Leave command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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}")
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True) await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return return
vc = await self.get_voice_client(ctx) vc = await self.get_voice_client(ctx)
if await self.voice_check(ctx) and vc: if await self.voice_check(ctx) and vc:
self.db.update(ctx.guild.id, {'current_track': None, 'is_stopped': True}) self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True})
self.db.clear_history(ctx.guild.id)
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}")
@queue.command(description="Очистить очередь треков и историю прослушивания.") @queue.command(description="Очистить очередь треков и историю прослушивания.")
async def clear(self, ctx: discord.ApplicationContext) -> None: async def clear(self, ctx: discord.ApplicationContext) -> None:
logging.debug(f"Clear queue command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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}")
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True) await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
elif await self.voice_check(ctx) and (len(channel.members) == 2 or member.guild_permissions.manage_channels): elif await self.voice_check(ctx):
self.db.clear_history(ctx.guild.id) 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}")
@queue.command(description="Получить очередь треков.") @queue.command(description="Получить очередь треков.")
async def get(self, ctx: discord.ApplicationContext) -> None: async def get(self, ctx: discord.ApplicationContext) -> None:
logging.debug(f"Get queue command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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
tracks = self.db.get_tracks_list(ctx.guild.id, 'next')
self.users_db.update(ctx.user.id, {'queue_page': 0}) self.users_db.update(ctx.user.id, {'queue_page': 0})
tracks = self.db.get_tracks_list(ctx.guild.id, 'next')
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}")
@track.command(description="Приостановить текущий трек.") @track.command(description="Приостановить текущий трек.")
async def pause(self, ctx: discord.ApplicationContext) -> None: async def pause(self, ctx: discord.ApplicationContext) -> None:
logging.debug(f"Pause command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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}")
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_player(ctx.guild.id) player = self.db.get_current_player(ctx.guild.id)
if player: if player:
await self.update_player_embed(ctx, player) await self.update_player_embed(ctx, player)
logging.info(f"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}")
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.debug(f"Resume command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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}")
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)):
if vc.is_paused(): if vc.is_paused():
vc.resume() vc.resume()
player = self.db.get_current_player(ctx.guild.id) player = self.db.get_current_player(ctx.guild.id)
if player: if player:
await self.update_player_embed(ctx, player) await self.update_player_embed(ctx, player)
logging.info(f"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}")
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.debug(f"Stop command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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")
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.clear_history(ctx.guild.id) self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []})
await self.stop_playing(ctx) await self.stop_playing(ctx)
current_player = self.db.get_current_player(ctx.guild.id) current_player = self.db.get_current_player(ctx.guild.id)
if current_player is not None: if current_player:
try: player = await self.get_player_message(ctx, current_player)
message = await ctx.fetch_message(current_player) if player:
await message.delete() await player.delete()
except discord.DiscordException:
pass logging.info(f"Playback stopped in guild {ctx.guild.id}")
self.db.update(ctx.guild.id, {'current_player': None, 'repeat': False, 'shuffle': False}) self.db.update(ctx.guild.id, {'current_player': None, 'repeat': False, 'shuffle': False})
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.debug(f"Next command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"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}")
await ctx.respond("❌ Нет песенен в очереди.", delete_after=15, ephemeral=True) await ctx.respond("❌ Нет песенен в очереди.", delete_after=15, ephemeral=True)
return return
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 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}")
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()
await response.add_reaction('') await response.add_reaction('')
await response.add_reaction('') await response.add_reaction('')
self.db.update_vote( self.db.update_vote(
gid, gid,
response.id, response.id,
@@ -346,23 +400,32 @@ class Voice(Cog, VoiceExtension):
} }
) )
else: else:
logging.info(f"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)
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15) await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
@track.command(description="Добавить трек в избранное или убрать, если он уже там.") @track.command(description="Добавить трек в избранное или убрать, если он уже там.")
async def like(self, ctx: discord.ApplicationContext) -> None: async def like(self, ctx: discord.ApplicationContext) -> None:
logging.debug(f"Like command invoked by user {ctx.author.id} in guild {ctx.guild.id}") logging.info(f"Like command invoked by user {ctx.author.id} in guild {ctx.guild.id}")
if await self.voice_check(ctx):
vc = await self.get_voice_client(ctx) if not await self.voice_check(ctx):
if not vc or not vc.is_playing: return
logging.debug(f"No current track in {ctx.guild.id}")
await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True) vc = await self.get_voice_client(ctx)
return if not vc or not vc.is_playing:
result = await self.like_track(ctx) logging.info(f"No current track in {ctx.guild.id}")
if not result: await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
await ctx.respond("❌ Операция не удалась.", delete_after=15, ephemeral=True) return
elif result == 'TRACK REMOVED':
await ctx.respond("Трек был удалён из избранного.", delete_after=15, ephemeral=True) result = await self.like_track(ctx)
else: if not result:
await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True) logging.warning(f"Like command failed for user {ctx.author.id} in guild {ctx.guild.id}")
await ctx.respond("❌ Операция не удалась.", delete_after=15, ephemeral=True)
elif result == 'TRACK REMOVED':
logging.info(f"Track removed from favorites for user {ctx.author.id} in guild {ctx.guild.id}")
await ctx.respond("Трек был удалён из избранного.", delete_after=15, ephemeral=True)
else:
logging.info(f"Track added to favorites for user {ctx.author.id} in guild {ctx.guild.id}")
await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True)

View File

@@ -6,14 +6,6 @@ from MusicBot.database import BaseGuildsDatabase
class VoiceGuildsDatabase(BaseGuildsDatabase): class VoiceGuildsDatabase(BaseGuildsDatabase):
def clear_history(self, gid: int) -> None:
"""Clear previous and next tracks list.
Args:
gid (int): _description_
"""
self.update(gid, {'previous_tracks': [], 'next_tracks': []})
def get_tracks_list(self, gid: int, type: Literal['next', 'previous']) -> list[dict[str, Any]]: def get_tracks_list(self, gid: int, type: Literal['next', 'previous']) -> list[dict[str, Any]]:
"""Get tracks list with given type. """Get tracks list with given type.