diff --git a/MusicBot/cogs/general.py b/MusicBot/cogs/general.py index 88a7edb..b287d44 100644 --- a/MusicBot/cogs/general.py +++ b/MusicBot/cogs/general.py @@ -25,22 +25,23 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]: users_db = BaseUsersDatabase() token = users_db.get_ym_token(ctx.interaction.user.id) if not token: - return ['❌ Укажите токен через /account login.'] + logging.info(f"[GENERAL] User {ctx.interaction.user.id} has no token") + return [] try: client = await YMClient(token).init() except yandex_music.exceptions.UnauthorizedError: logging.info(f"[GENERAL] User {ctx.interaction.user.id} provided invalid token") - return ['❌ Недействительный токен.'] + return [] content_type = ctx.options['тип'] search = await client.search(ctx.value) if not search: - logging.warning(f"Failed to search for '{ctx.value}' for user {ctx.interaction.user.id}") - return ["❌ Что-то пошло не так. Повторите попытку позже"] + logging.warning(f"[GENERAL] Failed to search for '{ctx.value}' for user {ctx.interaction.user.id}") + return [] res = [] - logging.debug(f"Searching for '{ctx.value}' for user {ctx.interaction.user.id}") + logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {ctx.interaction.user.id}") if content_type == 'Трек' and search.tracks: for item in search.tracks.results: @@ -57,10 +58,9 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]: elif content_type == "Свой плейлист": if not client.me or not client.me.account or not client.me.account.uid: logging.warning(f"Failed to get playlists for user {ctx.interaction.user.id}") - return ["❌ Что-то пошло не так. Повторите попытку позже"] - - playlists_list = await client.users_playlists_list(client.me.account.uid) - res = [playlist.title if playlist.title else 'Без названия' for playlist in playlists_list] + else: + playlists_list = await client.users_playlists_list(client.me.account.uid) + res = [playlist.title if playlist.title else 'Без названия' for playlist in playlists_list] return res @@ -95,7 +95,8 @@ class General(Cog): embed.description = ( "Этот бот позволяет слушать музыку из вашего аккаунта Yandex Music.\n" "Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n" - "Для получения помощи по конкретной команде, введите /help <команда>.\n\n" + "Для получения помощи по конкретной команде, введите /help <команда>.\n" + "Для изменения настроек необходимо иметь права управления каналами на сервере.\n\n" "**Для дополнительной помощи, присоединяйтесь к [серверу любителей Яндекс Музыки](https://discord.gg/gkmFDaPMeC).**" ) @@ -152,15 +153,18 @@ class General(Cog): embed.description += ( "`Примечание`: Если вы один в голосовом канале или имеете разрешение управления каналом, голосование не начинается.\n\n" "Переключиться на следующий трек в очереди. \n```/track next```\n" - "Приостановить текущий трек.\n ```/track pause```\n" - "Возобновить текущий трек.\n ```/track resume```\n" - "Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```" + "Приостановить текущий трек.\n```/track pause```\n" + "Возобновить текущий трек.\n```/track resume```\n" + "Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```\n" + "Запустить Мою Волну по текущему треку.\n```/track vibe```" ) elif command == 'voice': embed.description += ( - "Присоединить бота в голосовой канал. Требует разрешения управления каналом.\n ```/voice join```\n" + "`Примечание`: Доступность меню и Моей Волны зависит от настроек сервера.\n\n" + "Присоединить бота в голосовой канал. Требует разрешения управления каналом.\n```/voice join```\n" "Заставить бота покинуть голосовой канал. Требует разрешения управления каналом.\n ```/voice leave```\n" - "Создать меню проигрывателя. Доступность зависит от настроек сервера. По умолчанию работает только когда в канале один человек.\n```/voice menu```" + "Создать меню проигрывателя. По умолчанию работает только когда в канале один человек.\n```/voice menu```\n" + "Запустить Мою Волну. По умолчанию работает только когда в канале один человек.\n```/vibe```" ) else: response_message = '❌ Неизвестная команда.' @@ -194,16 +198,19 @@ class General(Cog): @account.command(description="Получить плейлист «Мне нравится»") async def likes(self, ctx: discord.ApplicationContext) -> None: logging.info(f"[GENERAL] Likes command invoked by user {ctx.author.id} in guild {ctx.guild.id}") + token = self.users_db.get_ym_token(ctx.user.id) if not token: logging.info(f"[GENERAL] No token found for user {ctx.user.id}") - await ctx.respond('❌ Необходимо указать свой токен доступа с помощью команды /login.', delete_after=15, ephemeral=True) + await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True) return + client = await YMClient(token).init() if not client.me or not client.me.account or not client.me.account.uid: logging.warning(f"Failed to fetch user info for user {ctx.user.id}") await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) return + likes = await client.users_likes_tracks() if likes is None: logging.info(f"[GENERAL] Failed to fetch likes for user {ctx.user.id}") @@ -226,7 +233,7 @@ class General(Cog): token = self.users_db.get_ym_token(ctx.user.id) if not token: - await ctx.respond('❌ Необходимо указать свой токен доступа с помощью команды /login.', delete_after=15, ephemeral=True) + await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True) return client = await YMClient(token).init() @@ -272,7 +279,7 @@ class General(Cog): token = self.users_db.get_ym_token(ctx.user.id) if not token: logging.info(f"[GENERAL] No token found for user {ctx.user.id}") - await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True) + await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True) return try: @@ -311,7 +318,7 @@ class General(Cog): embed = await generate_item_embed(result) view = ListenView(result) else: - result = await client.search(name, True) + result = await client.search(name, nocorrect=True) if not result: logging.warning(f"Failed to search for '{name}' for user {ctx.user.id}") @@ -368,4 +375,3 @@ class General(Cog): logging.info(f"[GENERAL] Successfully generated '{content_type}' message for user {ctx.author.id}") await ctx.respond(embed=embed, view=view) - diff --git a/MusicBot/cogs/utils/embeds.py b/MusicBot/cogs/utils/embeds.py index 617a532..bc791e6 100644 --- a/MusicBot/cogs/utils/embeds.py +++ b/MusicBot/cogs/utils/embeds.py @@ -11,7 +11,7 @@ from yandex_music import Track, Album, Artist, Playlist, Label from discord import Embed async def generate_item_embed(item: Track | Album | Artist | Playlist | list[Track], vibing: bool = False) -> Embed: - """Generate item embed. + """Generate item embed. list[Track] is used for likes. Args: item (yandex_music.Track | yandex_music.Album | yandex_music.Artist | yandex_music.Playlist): Item to be processed. diff --git a/MusicBot/cogs/utils/voice_extension.py b/MusicBot/cogs/utils/voice_extension.py index 7f9ea7d..75ec370 100644 --- a/MusicBot/cogs/utils/voice_extension.py +++ b/MusicBot/cogs/utils/voice_extension.py @@ -11,7 +11,7 @@ from discord.ui import View from discord import Interaction, ApplicationContext, RawReactionActionEvent from MusicBot.cogs.utils import generate_item_embed -from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase +from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase, ExplicitGuild # TODO: RawReactionActionEvent is poorly supported. @@ -277,7 +277,7 @@ class VoiceExtension: token = self.users_db.get_ym_token(ctx.user.id) if not token: logging.debug(f"[VC_EXT] No token found for user {ctx.user.id}") - await ctx.respond("❌ Необходимо указать свой токен доступа с помощью команды /login.", delete_after=15, ephemeral=True) + await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True) return False if not isinstance(ctx.channel, discord.VoiceChannel): @@ -389,32 +389,22 @@ class VoiceExtension: self.db.update(gid, {'current_track': track.to_dict()}) guild = self.db.get_guild(gid) - if guild['current_menu'] and not isinstance(ctx, RawReactionActionEvent): - if menu_message: - try: - if gid in menu_views: - menu_views[gid].stop() - menu_views[gid] = await MenuView(ctx).init() - await menu_message.edit(embed=await generate_item_embed(track, guild['vibing']), view=menu_views[gid]) - except discord.errors.NotFound: - logging.warning("[VC_EXT] Menu message not found. Using 'update_menu_embed' instead.") - await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback) - else: - await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback) try: - await track.download_async(f'music/{gid}.mp3') - song = discord.FFmpegPCMAudio(f'music/{gid}.mp3', options='-vn -filter:a "volume=0.15"') - except yandex_music.exceptions.TimedOutError: # sometimes track takes too long to download. + await asyncio.gather( + track.download_async(f'music/{gid}.mp3'), + self._update_menu(ctx, guild, track, menu_message, button_callback) + ) + except yandex_music.exceptions.TimedOutError: logging.warning(f"[VC_EXT] Timed out while downloading track '{track.title}'") if not isinstance(ctx, RawReactionActionEvent) and ctx.user and ctx.channel: channel = cast(discord.VoiceChannel, ctx.channel) if not retry: - channel = cast(discord.VoiceChannel, ctx.channel) return await self.play_track(ctx, track, vc=vc, button_callback=button_callback, retry=True) await channel.send(f"😔 Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15) return None + 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.info(f"[VC_EXT] Playing track '{track.title}'") @@ -718,6 +708,30 @@ class VoiceExtension: break await asyncio.sleep(0.25) update = await self.update_menu_embed(ctx, menu_mid, button_callback) + + async def _update_menu( + self, + ctx: ApplicationContext | Interaction | RawReactionActionEvent, + guild: ExplicitGuild, + track: Track, + menu_message: discord.Message | None, + button_callback: bool + ) -> None: + from MusicBot.ui import MenuView + gid = cast(int, ctx.guild_id) + + if guild['current_menu'] and not isinstance(ctx, RawReactionActionEvent): + if menu_message: + try: + if gid in menu_views: + menu_views[gid].stop() + menu_views[gid] = await MenuView(ctx).init() + await menu_message.edit(embed=await generate_item_embed(track, guild['vibing']), view=menu_views[gid]) + except discord.errors.NotFound: + logging.warning("[VC_EXT] Menu message not found. Using 'update_menu_embed' instead.") + await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback) + else: + await self._retry_update_menu_embed(ctx, guild['current_menu'], button_callback) async def init_ym_client(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, token: str | None = None) -> YMClient | None: """Initialize Yandex Music client. Return client on success. Return None if no token found and respond to the context. @@ -737,7 +751,7 @@ class VoiceExtension: if not token: logging.debug("No token found in 'init_ym_client'") if not isinstance(ctx, discord.RawReactionActionEvent): - await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True) + await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True) return None try: @@ -747,4 +761,4 @@ class VoiceExtension: if not isinstance(ctx, discord.RawReactionActionEvent): await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True) return None - return client \ No newline at end of file + return client diff --git a/MusicBot/cogs/voice.py b/MusicBot/cogs/voice.py index 8018eb5..7423720 100644 --- a/MusicBot/cogs/voice.py +++ b/MusicBot/cogs/voice.py @@ -356,15 +356,19 @@ class Voice(Cog, VoiceExtension): token = user['ym_token'] if not token: logging.info(f"[VOICE] User {ctx.user.id} has no YM token") - await ctx.respond("❌ Укажите токен через /account login.", ephemeral=True) + await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True) return client = await self.init_ym_client(ctx, user['ym_token']) if not client: + logging.info(f"[VOICE] Failed to init YM client for user {ctx.user.id}") + await ctx.respond("❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True) return track = guild['current_track'] if not track: + logging.info(f"[VOICE] No current track in guild {ctx.guild.id}") + await ctx.respond("❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True) return res = await client.rotor_station_feedback_track_finished( @@ -470,7 +474,7 @@ class Voice(Cog, VoiceExtension): await self.send_menu_message(ctx) await self.update_vibe(ctx, 'track', guild['current_track']['id']) - @discord.slash_command(name='vibe', description="Запустить Мою Волну.") + @voice.command(name='vibe', description="Запустить Мою Волну.") async def user_vibe(self, ctx: discord.ApplicationContext) -> None: logging.info(f"[VOICE] Vibe (user) command invoked by user {ctx.user.id} in guild {ctx.guild_id}") if not await self.voice_check(ctx): diff --git a/MusicBot/ui/find.py b/MusicBot/ui/find.py index 9fb9b94..39af63d 100644 --- a/MusicBot/ui/find.py +++ b/MusicBot/ui/find.py @@ -165,7 +165,7 @@ class MyVibeButton(Button, VoiceExtension): ) class ListenView(View): - def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False): + def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True): super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout) logging.debug(f"Creating view for type: '{type(item).__name__}'") diff --git a/MusicBot/ui/menu.py b/MusicBot/ui/menu.py index cc89dad..5a748fe 100644 --- a/MusicBot/ui/menu.py +++ b/MusicBot/ui/menu.py @@ -234,7 +234,7 @@ class MyVibeSelect(Select, VoiceExtension): await interaction.edit(view=view) class MyVibeSettingsView(View, VoiceExtension): - def __init__(self, interaction: Interaction, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = False): + def __init__(self, interaction: Interaction, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True): View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout) VoiceExtension.__init__(self, None) diff --git a/MusicBot/ui/other.py b/MusicBot/ui/other.py index 42b48fe..61769a6 100644 --- a/MusicBot/ui/other.py +++ b/MusicBot/ui/other.py @@ -2,12 +2,7 @@ from math import ceil from typing import Any from discord.ui import View, Button, Item -from discord import ButtonStyle, Interaction, Embed - -from MusicBot.cogs.utils.voice_extension import VoiceExtension - -from discord.ui import View, Button, Item -from discord import ButtonStyle, Interaction, ApplicationContext +from discord import ApplicationContext, ButtonStyle, Interaction, Embed from MusicBot.cogs.utils.voice_extension import VoiceExtension @@ -71,7 +66,7 @@ class MPPrevButton(Button, VoiceExtension): await interaction.edit(embed=embed, view=MyPlaylists(interaction)) class MyPlaylists(View, VoiceExtension): - def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = True): + def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True): View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout) VoiceExtension.__init__(self, None) if not ctx.user: @@ -121,7 +116,7 @@ class QueuePrevButton(Button, VoiceExtension): await interaction.edit(embed=embed, view=QueueView(interaction)) class QueueView(View, VoiceExtension): - def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = True): + def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True): View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout) VoiceExtension.__init__(self, None) if not ctx.user or not ctx.guild: diff --git a/requirements.txt b/requirements.txt index 6a34b71..f6ceb33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,4 @@ PyNaCl pymongo yandex-music pillow -python-dotenv -wavelink \ No newline at end of file +python-dotenv \ No newline at end of file