diff --git a/MusicBot/cogs/general.py b/MusicBot/cogs/general.py index ae63ff5..1282053 100644 --- a/MusicBot/cogs/general.py +++ b/MusicBot/cogs/general.py @@ -35,7 +35,7 @@ class General(Cog): default='all' ) 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 embed = discord.Embed( color=0xfed42b @@ -109,32 +109,32 @@ class General(Cog): @account.command(description="Ввести токен от Яндекс Музыки.") @discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.") 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: client = await YMClient(token).init() 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) return about = cast(yandex_music.Status, client.me).to_dict() uid = ctx.author.id 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) @account.command(description="Удалить токен из датабазы бота.") 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}) await ctx.respond(f'Токен был удалён.', delete_after=15, ephemeral=True) @account.command(description="Получить плейлист «Мне нравится»") 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) 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) return client = await YMClient(token).init() @@ -144,22 +144,23 @@ class General(Cog): return likes = await client.users_likes_tracks() 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) return 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) return 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 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)) @account.command(description="Получить ваши плейлисты.") 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) if not token: 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}) 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) @discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.") @@ -196,18 +197,18 @@ class General(Cog): name: str, content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'] = 'Трек' ) -> 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) token = self.users_db.get_ym_token(ctx.user.id) 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) return try: client = await YMClient(token).init() 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) return @@ -220,20 +221,20 @@ class General(Cog): playlists = await client.users_playlists_list(client.me.account.uid) result = next((playlist for playlist in playlists if playlist.title == name), None) 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) return tracks = await result.fetch_tracks_async() 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) return for track_short in tracks: track = cast(Track, track_short.track) 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) return @@ -257,7 +258,7 @@ class General(Cog): content = result.playlists 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) return content = content.results[0] @@ -266,35 +267,35 @@ class General(Cog): view = ListenView(content) 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) return elif isinstance(content, Artist): tracks = await content.get_tracks_async() 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) return for track in tracks: 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 embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки") break elif isinstance(content, Playlist): tracks = await content.fetch_tracks_async() 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) return for track_short in content.tracks: track = cast(Track, track_short.track) 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 embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки") 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) diff --git a/MusicBot/cogs/utils/find.py b/MusicBot/cogs/utils/find.py index 9d4dd18..6523b83 100644 --- a/MusicBot/cogs/utils/find.py +++ b/MusicBot/cogs/utils/find.py @@ -1,5 +1,5 @@ import logging -from typing import cast +from typing import Literal, cast import discord from yandex_music import Track, Album, Artist, Playlist @@ -31,6 +31,7 @@ class PlayButton(Button, VoiceExtension): guild = self.db.get_guild(gid) channel = cast(discord.VoiceChannel, interaction.channel) member = cast(discord.Member, interaction.user) + action: Literal['add_track', 'add_album', 'add_artist', 'add_playlist'] if isinstance(self.item, Track): tracks = [self.item] @@ -38,39 +39,46 @@ class PlayButton(Button, VoiceExtension): vote_message = f"{member.mention} хочет добавить трек **{self.item.title}** в очередь.\n\n Голосуйте за добавление." response_message = f"Трек **{self.item.title}** был добавлен в очередь." play_message = f"Сейчас играет: **{self.item.title}**!" + elif isinstance(self.item, Album): album = await self.item.with_tracks_async() if not album or not album.volumes: logging.debug("Failed to fetch album tracks") await interaction.respond("Не удалось получить треки альбома.", ephemeral=True) return + tracks = [track for volume in album.volumes for track in volume] action = 'add_album' vote_message = f"{member.mention} хочет добавить альбом **{self.item.title}** в очередь.\n\n Голосуйте за добавление." response_message = f"Альбом **{self.item.title}** был добавлен в очередь." play_message = f"Сейчас играет: **{self.item.title}**!" + elif isinstance(self.item, Artist): artist_tracks = await self.item.get_tracks_async() if not artist_tracks: logging.debug("Failed to fetch artist tracks") await interaction.respond("Не удалось получить треки артиста.", ephemeral=True) return + tracks = artist_tracks.tracks.copy() action = 'add_artist' vote_message = f"{member.mention} хочет добавить треки от **{self.item.name}** в очередь.\n\n Голосуйте за добавление." response_message = f"Песни артиста **{self.item.name}** были добавлены в очередь." play_message = f"Сейчас играет: **{self.item.name}**!" + elif isinstance(self.item, Playlist): short_tracks = await self.item.fetch_tracks_async() if not short_tracks: logging.debug("Failed to fetch playlist tracks") await interaction.respond("❌ Не удалось получить треки из плейлиста.", delete_after=15) return + tracks = [cast(Track, short_track.track) for short_track in short_tracks] action = 'add_playlist' vote_message = f"{member.mention} хочет добавить плейлист **{self.item.title}** в очередь.\n\n Голосуйте за добавление." response_message = f"Плейлист **{self.item.title}** был добавлен в очередь." play_message = f"Сейчас играет: **{self.item.title}**!" + elif isinstance(self.item, list): tracks = self.item.copy() if not tracks: @@ -81,16 +89,19 @@ class PlayButton(Button, VoiceExtension): action = 'add_playlist' vote_message = f"{member.mention} хочет добавить плейлист **** в очередь.\n\n Голосуйте за добавление." response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь." - play_message = f"Сейчас играет: **{tracks[0].title}**!" + else: 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: logging.debug(f"Starting vote for '{action}'") + message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=30)) response = await message.original_response() + await response.add_reaction('✅') await response.add_reaction('❌') + self.db.update_vote( gid, response.id, @@ -104,27 +115,30 @@ class PlayButton(Button, VoiceExtension): ) else: logging.debug(f"Skipping vote for '{action}'") + if guild['current_track'] is not None: self.db.modify_track(gid, tracks, 'next', 'extend') - response_message = response_message else: track = tracks.pop(0) self.db.modify_track(gid, tracks, 'next', 'extend') await self.play_track(interaction, track) - response_message = play_message + response_message = f"Сейчас играет: **{tracks[0].title}**!" + current_player = None if 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): 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) logging.debug(f"Creating view for type: '{type(item).__name__}'") + if isinstance(item, Track): 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}" @@ -140,9 +154,11 @@ class ListenView(View): elif isinstance(item, list): # Can't open other person's likes self.add_item(PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray)) return + self.button1: Button = Button(label="Слушать в приложении", style=ButtonStyle.gray, url=link_app) self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web) self.button3: PlayButton = PlayButton(item, label="Слушать в голосовом канале", style=ButtonStyle.gray) + if item.available: # self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason. self.add_item(self.button2) @@ -158,6 +174,7 @@ async def generate_item_embed(item: Track | Album | Artist | Playlist) -> Embed: discord.Embed: Item embed. """ logging.debug(f"Generating embed for type: '{type(item).__name__}'") + if isinstance(item, Track): return await generate_track_embed(item) elif isinstance(item, Album): diff --git a/MusicBot/cogs/utils/voice_extension.py b/MusicBot/cogs/utils/voice_extension.py index 04ddb1f..3ed20ba 100644 --- a/MusicBot/cogs/utils/voice_extension.py +++ b/MusicBot/cogs/utils/voice_extension.py @@ -1,6 +1,6 @@ import asyncio import logging -from typing import Literal, cast +from typing import Any, Literal, cast from yandex_music import Track, ClientAsync @@ -23,7 +23,7 @@ class VoiceExtension: Args: ctx (ApplicationContext | Interaction): Context. player_mid (int): Id of the player message. There can only be only one player in the guild. - + Returns: bool: True if updated, False if not. """ @@ -31,18 +31,18 @@ class VoiceExtension: f"Updating player embed using " + "interaction context" if isinstance(ctx, Interaction) 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 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") + 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 token = self.users_db.get_ym_token(uid) @@ -120,9 +120,8 @@ class VoiceExtension: Returns: bool: Check result. """ - logging.debug("Checking voice requirements...") if not ctx.user: - logging.warning("User not found in context.") + logging.warning("User not found in context inside 'voice_check'") return False token = self.users_db.get_ym_token(ctx.user.id) @@ -159,14 +158,13 @@ class VoiceExtension: Returns: discord.VoiceClient | None: Voice client or None. """ - logging.debug("Getting voice client...") if isinstance(ctx, Interaction): voice_chat = discord.utils.get(ctx.client.voice_clients, guild=ctx.guild) elif isinstance(ctx, RawReactionActionEvent): if not self.bot: raise ValueError("Bot instance is not set.") 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 voice_chat = discord.utils.get(self.bot.voice_clients, guild=await self.bot.fetch_guild(ctx.guild_id)) elif isinstance(ctx, ApplicationContext): @@ -175,19 +173,25 @@ class VoiceExtension: raise ValueError(f"Invalid context type: '{type(ctx).__name__}'.") if voice_chat: - logging.debug(f"Voice client found") + logging.debug("Voice client found") else: logging.debug("Voice client not found") 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. If sound is already playing, add track id to the queue. There's no response to the context. Args: ctx (ApplicationContext | Interaction): Context track (Track): Track to play. + vc (discord.VoiceClient | None): Voice client. Returns: 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 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") + logging.warning("Guild ID or User ID not found in context inside 'play_track'") return None - vc = await self.get_voice_client(ctx) if not vc: - return None + vc = await self.get_voice_client(ctx) + if not vc: + return None if isinstance(ctx, Interaction): loop = ctx.client.loop @@ -218,7 +223,7 @@ class VoiceExtension: 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)) - logging.debug(f"Playing track '{track.title}'") + logging.info(f"Playing track '{track.title}'") self.db.set_current_track(gid, track) self.db.update(gid, {'is_stopped': False}) @@ -229,36 +234,44 @@ class VoiceExtension: return track.title - async def stop_playing(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> None: - logging.debug("Stopping playback...") + async def stop_playing(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, vc: discord.VoiceClient | None = None) -> None: + gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None if not gid: logging.warning("Guild ID not found in context") return - vc = await self.get_voice_client(ctx) + if not vc: + vc = await self.get_voice_client(ctx) if vc: + logging.debug("Stopping playback") self.db.update(gid, {'current_track': None, 'is_stopped': True}) 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. Doesn't change track if stopped. Stop playing if tracks list is empty. Args: 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. Returns: 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 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 + logging.warning("Guild ID or User ID not found in context inside 'next_track'") + return None guild = self.db.get_guild(gid) token = self.users_db.get_ym_token(uid) @@ -270,11 +283,13 @@ class VoiceExtension: logging.debug("Playback is stopped, skipping...") return None - if not await self.get_voice_client(ctx): # Silently return if bot got kicked - logging.debug("Voice client not found") - return None + if not vc: + vc = await self.get_voice_client(ctx) + if not vc: # Silently return if bot got kicked + return None if guild['repeat'] and after: + logging.debug("Repeating current track") next_track = guild['current_track'] elif guild['shuffle']: logging.debug("Shuffling tracks") @@ -283,7 +298,8 @@ class VoiceExtension: logging.debug("Getting next track") 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') if next_track: @@ -291,19 +307,20 @@ class VoiceExtension: next_track, 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( 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): await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15) 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 async def prev_track(self, ctx: ApplicationContext | Interaction) -> str | None: @@ -316,9 +333,8 @@ class VoiceExtension: Returns: str | None: Track title or None. """ - logging.debug("Switching to the previous track") 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 gid = ctx.guild.id @@ -328,11 +344,11 @@ class VoiceExtension: if not token: logging.debug(f"No token found for user {ctx.user.id}") - return - + return None + if prev_track: logging.debug("Previous track found") - track = prev_track + track: dict[str, Any] | None = prev_track elif current_track: logging.debug("No previous track found. Repeating current track") track = self.db.get_track(gid, 'current') @@ -363,7 +379,7 @@ class VoiceExtension: str | None: Track title or None. """ 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 current_track = self.db.get_track(ctx.guild.id, 'current') diff --git a/MusicBot/cogs/voice.py b/MusicBot/cogs/voice.py index d511771..a198c72 100644 --- a/MusicBot/cogs/voice.py +++ b/MusicBot/cogs/voice.py @@ -22,50 +22,50 @@ class Voice(Cog, VoiceExtension): def __init__(self, bot: discord.Bot): VoiceExtension.__init__(self, bot) - self.bot = bot + self.typed_bot: discord.Bot = bot @Cog.listener() 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 guild = self.db.get_guild(gid) + discord_guild = await self.typed_bot.fetch_guild(gid) channel = after.channel or before.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 - discord_guild = await self.bot.fetch_guild(gid) - vc = cast(discord.VoiceClient | None, discord.utils.get(self.bot.voice_clients, guild=discord_guild)) + vc = cast(discord.VoiceClient | None, discord.utils.get(self.typed_bot.voice_clients, guild=discord_guild)) if len(channel.members) == 1 and vc: - logging.debug(f"Clearing history and stopping playback for guild {gid}") - self.db.clear_history(gid) - self.db.update(gid, {'current_track': None, 'is_stopped': True}) + logging.info(f"Clearing history and stopping playback for guild {gid}") + self.db.update(gid, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True}) vc.stop() elif len(channel.members) > 2 and not guild['always_allow_menu']: current_player = self.db.get_current_player(gid) 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}) try: message = await channel.fetch_message(current_player) await message.delete() except (discord.NotFound, discord.Forbidden): pass - await channel.send("Текущий плеер отключён, так как в канале больше одного человека.", delete_after=15) @Cog.listener() 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}") - if not self.bot.user or not payload.member: + logging.info(f"Reaction added by user {payload.user_id} in channel {payload.channel_id}") + if not self.typed_bot.user or not payload.member: return - bot_id = self.bot.user.id + bot_id = self.typed_bot.user.id if payload.user_id == bot_id: 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: return @@ -86,55 +86,69 @@ class Voice(Cog, VoiceExtension): vote_data = votes[str(payload.message_id)] 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) 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) 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 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': - 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}) title = await self.next_track(payload) await message.clear_reactions() await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15) del votes[str(payload.message_id)] + 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() + track = vote_data['vote_content'] 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 + self.db.update(guild_id, {'is_stopped': False}) self.db.modify_track(guild_id, track, 'next', 'append') + if guild['current_track']: await message.edit(content=f"Трек был добавлен в очередь!", delete_after=15) else: title = await self.next_track(payload) await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15) + del votes[str(payload.message_id)] + elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'): - logging.debug(f"Performing '{vote_data['action']}' action for message {payload.message_id}") - tracks = vote_data['vote_content'] + logging.info(f"Performing '{vote_data['action']}' action for message {payload.message_id}") + await message.clear_reactions() + + tracks = vote_data['vote_content'] 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 + self.db.update(guild_id, {'is_stopped': False}) self.db.modify_track(guild_id, tracks, 'next', 'extend') + if guild['current_track']: await message.edit(content=f"Контент был добавлен в очередь!", delete_after=15) else: title = await self.next_track(payload) await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15) + del votes[str(payload.message_id)] + 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.edit(content='Запрос был отклонён.', delete_after=15) del votes[str(payload.message_id)] @@ -143,8 +157,8 @@ class Voice(Cog, VoiceExtension): @Cog.listener() 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}") - if not self.bot.user: + logging.info(f"Reaction removed by user {payload.user_id} in channel {payload.channel_id}") + if not self.typed_bot.user: return guild_id = payload.guild_id @@ -153,27 +167,27 @@ class Voice(Cog, VoiceExtension): guild = self.db.get_guild(guild_id) 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: return 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 vote_data = votes[str(payload.message_id)] 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] 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] self.db.update(guild_id, {'votes': votes}) @voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.") 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): return @@ -182,7 +196,7 @@ class Voice(Cog, VoiceExtension): embed = None 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) return @@ -200,7 +214,7 @@ class Voice(Cog, VoiceExtension): embed.remove_footer() 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']) await message.delete() @@ -208,14 +222,16 @@ class Voice(Cog, VoiceExtension): response = await interaction.original_response() 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="Подключиться к голосовому каналу, в котором вы сейчас находитесь.") 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) - vc = await self.get_voice_client(ctx) if not member.guild_permissions.manage_channels: response_message = "❌ У вас нет прав для выполнения этой команды." - elif vc and vc.is_connected(): + elif (vc := await self.get_voice_client(ctx)) and vc.is_connected(): response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave." elif isinstance(ctx.channel, discord.VoiceChannel): await ctx.channel.connect(timeout=15) @@ -223,117 +239,155 @@ class Voice(Cog, VoiceExtension): else: response_message = "❌ Вы должны отправить команду в голосовом канале." + logging.info(f"Join command response: {response_message}") await ctx.respond(response_message, delete_after=15, ephemeral=True) @voice.command(description="Заставить бота покинуть голосовой канал.") 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) 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) return vc = await self.get_voice_client(ctx) if await self.voice_check(ctx) and vc: - self.db.update(ctx.guild.id, {'current_track': None, 'is_stopped': True}) - self.db.clear_history(ctx.guild.id) + self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': [], 'current_track': None, 'is_stopped': True}) vc.stop() await vc.disconnect(force=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="Очистить очередь треков и историю прослушивания.") 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) channel = cast(discord.VoiceChannel, ctx.channel) + 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) - elif await self.voice_check(ctx) and (len(channel.members) == 2 or member.guild_permissions.manage_channels): - self.db.clear_history(ctx.guild.id) + elif await self.voice_check(ctx): + self.db.update(ctx.guild.id, {'previous_tracks': [], 'next_tracks': []}) await ctx.respond("Очередь и история сброшены.", delete_after=15, ephemeral=True) + logging.info(f"Queue and history cleared in guild {ctx.guild.id}") @queue.command(description="Получить очередь треков.") 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): return - tracks = self.db.get_tracks_list(ctx.guild.id, 'next') 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) 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="Приостановить текущий трек.") 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) channel = cast(discord.VoiceChannel, ctx.channel) + 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) + elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)) is not None: if not vc.is_paused(): vc.pause() + player = self.db.get_current_player(ctx.guild.id) if 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) else: + logging.info(f"Track already paused in guild {ctx.guild.id}") await ctx.respond("Воспроизведение уже приостановлено.", delete_after=15, ephemeral=True) @track.command(description="Возобновить текущий трек.") 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) channel = cast(discord.VoiceChannel, ctx.channel) + 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) - 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(): vc.resume() player = self.db.get_current_player(ctx.guild.id) if 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) else: + logging.info(f"Track is not paused in guild {ctx.guild.id}") await ctx.respond("Воспроизведение не на паузе.", delete_after=15, ephemeral=True) @track.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.") 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) channel = cast(discord.VoiceChannel, ctx.channel) + 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) + 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) + current_player = self.db.get_current_player(ctx.guild.id) - if current_player is not None: - try: - message = await ctx.fetch_message(current_player) - await message.delete() - except discord.DiscordException: - pass + if current_player: + player = await self.get_player_message(ctx, current_player) + if player: + await player.delete() + + logging.info(f"Playback stopped in guild {ctx.guild.id}") + self.db.update(ctx.guild.id, {'current_player': None, 'repeat': False, 'shuffle': False}) await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True) @track.command(description="Переключиться на следующую песню в очереди.") 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): return + gid = ctx.guild.id guild = self.db.get_guild(gid) 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) return member = cast(discord.Member, ctx.author) channel = cast(discord.VoiceChannel, ctx.channel) + 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)) response = await message.original_response() + await response.add_reaction('✅') await response.add_reaction('❌') + self.db.update_vote( gid, response.id, @@ -346,23 +400,32 @@ class Voice(Cog, VoiceExtension): } ) else: + logging.info(f"Skipping vote for user {ctx.author.id} in guild {ctx.guild.id}") + self.db.update(gid, {'is_stopped': False}) title = await self.next_track(ctx) await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15) @track.command(description="Добавить трек в избранное или убрать, если он уже там.") async def like(self, ctx: discord.ApplicationContext) -> None: - logging.debug(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 vc or not vc.is_playing: - logging.debug(f"No current track in {ctx.guild.id}") - await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True) - return - result = await self.like_track(ctx) - if not result: - await ctx.respond("❌ Операция не удалась.", delete_after=15, ephemeral=True) - elif result == 'TRACK REMOVED': - await ctx.respond("Трек был удалён из избранного.", delete_after=15, ephemeral=True) - else: - await ctx.respond(f"Трек **{result}** был добавлен в избранное.", delete_after=15, ephemeral=True) + logging.info(f"Like command invoked by user {ctx.author.id} in guild {ctx.guild.id}") + + if not await self.voice_check(ctx): + return + + vc = await self.get_voice_client(ctx) + if not vc or not vc.is_playing: + logging.info(f"No current track in {ctx.guild.id}") + await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True) + return + + result = await self.like_track(ctx) + if not result: + 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) diff --git a/MusicBot/database/extensions.py b/MusicBot/database/extensions.py index 27cc0ce..81a3e78 100644 --- a/MusicBot/database/extensions.py +++ b/MusicBot/database/extensions.py @@ -6,14 +6,6 @@ from MusicBot.database import 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]]: """Get tracks list with given type.