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'
)
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)

View File

@@ -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):

View File

@@ -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')

View File

@@ -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)

View File

@@ -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.