From c0bb10cbf803c1fde4a3a4c0dd4af9174232903e Mon Sep 17 00:00:00 2001 From: Lemon4ksan Date: Wed, 12 Feb 2025 13:53:38 +0300 Subject: [PATCH] impr: Changed /account playlists functionality --- MusicBot/cogs/general.py | 223 +++++++++++++++++++--------------- MusicBot/cogs/utils/embeds.py | 12 +- MusicBot/ui/__init__.py | 6 +- MusicBot/ui/find.py | 2 +- MusicBot/ui/other.py | 68 ----------- 5 files changed, 132 insertions(+), 179 deletions(-) diff --git a/MusicBot/cogs/general.py b/MusicBot/cogs/general.py index 7e52c44..9989a73 100644 --- a/MusicBot/cogs/general.py +++ b/MusicBot/cogs/general.py @@ -12,17 +12,18 @@ from yandex_music import Track, Album, Artist, Playlist from MusicBot.database import BaseUsersDatabase, BaseGuildsDatabase -from MusicBot.ui import ListenView, MyPlaylists, generate_playlists_embed +from MusicBot.ui import ListenView from MusicBot.cogs.utils.embeds import generate_item_embed +users_db = BaseUsersDatabase() + def setup(bot): bot.add_cog(General(bot)) async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]: if not ctx.interaction.user or not ctx.value or len(ctx.value) < 2: return [] - - users_db = BaseUsersDatabase() + token = await users_db.get_ym_token(ctx.interaction.user.id) if not token: logging.info(f"[GENERAL] User {ctx.interaction.user.id} has no token") @@ -64,12 +65,30 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]: return res[:100] +async def get_user_playlists_suggestions(ctx: discord.AutocompleteContext) -> list[str]: + if not ctx.interaction.user or not ctx.value or len(ctx.value) < 2: + return [] + + token = await users_db.get_ym_token(ctx.interaction.user.id) + if not token: + logging.info(f"[GENERAL] User {ctx.interaction.user.id} has no token") + return [] + + try: + client = await YMClient(token).init() + except yandex_music.exceptions.UnauthorizedError: + logging.info(f"[GENERAL] User {ctx.interaction.user.id} provided invalid token") + return [] + + playlists_list = await client.users_playlists_list() + return [playlist.title if playlist.title else 'Без названия' for playlist in playlists_list] + class General(Cog): def __init__(self, bot: discord.Bot): self.bot = bot self.db = BaseGuildsDatabase() - self.users_db = BaseUsersDatabase() + self.users_db = users_db account = discord.SlashCommandGroup("account", "Команды, связанные с аккаунтом.") @@ -106,7 +125,6 @@ class General(Cog): value="""`account` `find` `help` - `like` `queue` `settings` `track` @@ -116,26 +134,23 @@ class General(Cog): embed.set_footer(text='©️ Bananchiki') elif command == 'account': embed.description += ( - "Ввести токен от Яндекс Музыки. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n" + "Ввести токен Яндекс Музыки. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n" "```/account login ```\n" "Удалить токен из базы данных бота.\n```/account remove```\n" - "Получить ваши плейлисты. Чтобы добавить плейлист в очередь, используйте команду /find.\n```/account playlists```\n" + "Получить ваш плейлист.\n```/account playlist <название>```\n" "Получить плейлист «Мне нравится».\n```/account likes```\n" + "Получить ваши рекомендации.\n```/account recommendations <тип>```\n" ) elif command == 'find': embed.description += ( "Вывести информацию о треке (по умолчанию), альбоме, авторе или плейлисте. Позволяет добавить музыку в очередь. " - "В названии можно уточнить автора или версию. Возвращается лучшее совпадение.\n```/find <название> <тип>```" + "В названии можно уточнить автора через «-». Возвращается лучшее совпадение.\n```/find <тип> <название>```" ) elif command == 'help': embed.description += ( "Вывести список всех команд.\n```/help```\n" "Получить информацию о конкретной команде.\n```/help <команда>```" ) - elif command == 'like': - embed.description += ( - "Добавить трек в плейлист «Мне нравится». Пользовательские треки из этого плейлиста игнорируются.\n```/like```" - ) elif command == 'queue': embed.description += ( "Получить очередь треков. По 15 элементов на страницу.\n```/queue get```\n" @@ -147,7 +162,8 @@ class General(Cog): "Получить текущие настройки.\n```/settings show```\n" "Разрешить или запретить воспроизведение Explicit треков и альбомов. Если автор или плейлист содержат Explicit треки, убираются кнопки для доступа к ним.\n```/settings explicit```\n" "Разрешить или запретить создание меню проигрывателя, когда в канале больше одного человека.\n```/settings menu```\n" - "Разрешить или запретить голосование.\n```/settings vote <тип голосования>```\n" + "Разрешить или запретить голосование.\n```/settings vote <тип>```\n" + "Разрешить или запретить отключение/подключение бота к каналу участникам без прав управления каналом.\n```/settings connect```\n" "`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки." ) elif command == 'track': @@ -157,6 +173,7 @@ class General(Cog): "Приостановить текущий трек.\n```/track pause```\n" "Возобновить текущий трек.\n```/track resume```\n" "Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```\n" + "Добавить трек в плейлист «Мне нравится» или удалить его, если он уже там.\n```/track like```" "Запустить Мою Волну по текущему треку.\n```/track vibe```" ) elif command == 'voice': @@ -282,27 +299,57 @@ class General(Cog): await ctx.respond(embed=embed, view=view) - @account.command(description="Получить ваши плейлисты.") - async def playlists(self, ctx: discord.ApplicationContext) -> None: + @account.command(description="Получить ваш плейлист.") + @discord.option( + "запрос", + parameter_name='name', + description="Название плейлиста.", + type=discord.SlashCommandOptionType.string, + autocomplete=discord.utils.basic_autocomplete(get_user_playlists_suggestions) + ) + async def playlist(self, ctx: discord.ApplicationContext, name: str) -> None: logging.info(f"[GENERAL] Playlists command invoked by user {ctx.user.id} in guild {ctx.guild_id}") + guild = await self.db.get_guild(ctx.guild_id, projection={'allow_explicit': 1}) token = await self.users_db.get_ym_token(ctx.user.id) if not token: + logging.info(f"[GENERAL] No token found for user {ctx.user.id}") await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True) return - client = await YMClient(token).init() + try: + client = await YMClient(token).init() + except yandex_music.exceptions.UnauthorizedError: + logging.info(f"[GENERAL] User {ctx.user.id} provided invalid token") + await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True) + return - playlists_list = await client.users_playlists_list() - playlists: list[tuple[str, int]] = [ - (playlist.title if playlist.title else 'Без названия', playlist.track_count if playlist.track_count else 0) for playlist in playlists_list - ] + playlists = await client.users_playlists_list() - await self.users_db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0}) - embed = generate_playlists_embed(0, playlists) + playlist = next((playlist for playlist in playlists if playlist.title == name), None) + if not playlist: + logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' not found") + await ctx.respond("❌ Плейлист не найден.", delete_after=15, ephemeral=True) + return - logging.info(f"[GENERAL] Successfully fetched playlists for user {ctx.user.id}") - await ctx.respond(embed=embed, view=await MyPlaylists(ctx).init(), ephemeral=True) + tracks = await playlist.fetch_tracks_async() + if not tracks: + logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' is empty") + await ctx.respond("❌ Плейлист пуст.", delete_after=15, ephemeral=True) + return + + embed = await generate_item_embed(playlist) + view = ListenView(playlist) + + for track_short in playlist.tracks: + track = cast(Track, track_short.track) + if (track.explicit or track.content_warning) and not guild['allow_explicit']: + logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server") + embed.set_footer(text="Воспроизведение недоступно, так как в плейлисте присутствуют Explicit треки") + view = None + break + + await ctx.respond(embed=embed, view=view) @discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.") @discord.option( @@ -310,7 +357,7 @@ class General(Cog): parameter_name='content_type', description="Тип контента для поиска.", type=discord.SlashCommandOptionType.string, - choices=['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'], + choices=['Трек', 'Альбом', 'Артист', 'Плейлист'], ) @discord.option( "запрос", @@ -322,11 +369,10 @@ class General(Cog): async def find( self, ctx: discord.ApplicationContext, - content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'], + content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист'], name: str ) -> None: # TODO: Improve explicit check by excluding bad tracks from the queue and not fully discard the artist/album/playlist. - # TODO: Move 'Свой плейлист' search to /account playlists command by using select menu. logging.info(f"[GENERAL] Find command invoked by user {ctx.user.id} in guild {ctx.guild_id} for '{content_type}' with name '{name}'") @@ -344,85 +390,60 @@ class General(Cog): await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True) return - if content_type == 'Свой плейлист': + result = await client.search(name, nocorrect=True) + + if not result: + logging.warning(f"Failed to search for '{name}' for user {ctx.user.id}") + await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True) + return - playlists = await client.users_playlists_list() - result = next((playlist for playlist in playlists if playlist.title == name), None) - if not result: - logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' not found") - await ctx.respond("❌ Плейлист не найден.", delete_after=15, ephemeral=True) - return - - tracks = await result.fetch_tracks_async() + if content_type == 'Трек': + content = result.tracks + elif content_type == 'Альбом': + content = result.albums + elif content_type == 'Артист': + content = result.artists + elif content_type == 'Плейлист': + content = result.playlists + + if not content: + logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no results") + await ctx.respond("❌ По запросу ничего не найдено.", delete_after=15, ephemeral=True) + return + content = content.results[0] + + embed = await generate_item_embed(content) + view = ListenView(content) + + if isinstance(content, (Track, Album)) and (content.explicit or content.content_warning) and not guild['allow_explicit']: + logging.info(f"[GENERAL] 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.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' is empty") - await ctx.respond("❌ Плейлист пуст.", delete_after=15, ephemeral=True) + logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no tracks") + await ctx.respond("❌ Треки от этого исполнителя не найдены.", delete_after=15, ephemeral=True) return - - for track_short in tracks: + for track in tracks: + if (track.explicit or track.content_warning) and not guild['allow_explicit']: + logging.info(f"[GENERAL] 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.info(f"[GENERAL] 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.info(f"[GENERAL] 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 - - embed = await generate_item_embed(result) - view = ListenView(result) - else: - result = await client.search(name, nocorrect=True) - - if not result: - logging.warning(f"Failed to search for '{name}' for user {ctx.user.id}") - await ctx.respond("❌ Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True) - return - - if content_type == 'Трек': - content = result.tracks - elif content_type == 'Альбом': - content = result.albums - elif content_type == 'Артист': - content = result.artists - elif content_type == 'Плейлист': - content = result.playlists - - if not content: - logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no results") - await ctx.respond("❌ По запросу ничего не найдено.", delete_after=15, ephemeral=True) - return - content = content.results[0] - - embed = await generate_item_embed(content) - view = ListenView(content) - - if isinstance(content, (Track, Album)) and (content.explicit or content.content_warning) and not guild['allow_explicit']: - logging.info(f"[GENERAL] 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.info(f"[GENERAL] 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.info(f"[GENERAL] 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.info(f"[GENERAL] 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.info(f"[GENERAL] 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.info(f"[GENERAL] 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.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 13b8c6e..c737242 100644 --- a/MusicBot/cogs/utils/embeds.py +++ b/MusicBot/cogs/utils/embeds.py @@ -276,20 +276,22 @@ async def _generate_playlist_embed(playlist: Playlist) -> Embed: duration = playlist.duration_ms likes_count = playlist.likes_count - color = 0x000 cover_url = None if playlist.cover and playlist.cover.uri: cover_url = f"https://{playlist.cover.uri.replace('%%', '400x400')}" else: tracks = await playlist.fetch_tracks_async() - for i in range(len(tracks)): - track = tracks[i].track - if not track or not track.albums or not track.albums[0].cover_uri: - continue + for track_short in tracks: + track = track_short.track + if track and track.albums and track.albums[0].cover_uri: + cover_url = f"https://{track.albums[0].cover_uri.replace('%%', '400x400')}" + break if cover_url: color = await _get_average_color_from_url(cover_url) + else: + color = 0x000 embed = Embed( title=title, diff --git a/MusicBot/ui/__init__.py b/MusicBot/ui/__init__.py index 3f33174..2295689 100644 --- a/MusicBot/ui/__init__.py +++ b/MusicBot/ui/__init__.py @@ -1,12 +1,10 @@ -from .other import MyPlaylists, QueueView, generate_queue_embed, generate_playlists_embed +from .other import QueueView, generate_queue_embed from .menu import MenuView from .find import ListenView __all__ = [ - 'MyPlaylists', 'QueueView', 'MenuView', 'ListenView', - 'generate_queue_embed', - 'generate_playlists_embed' + 'generate_queue_embed' ] diff --git a/MusicBot/ui/find.py b/MusicBot/ui/find.py index 80ca18b..5cc6dd9 100644 --- a/MusicBot/ui/find.py +++ b/MusicBot/ui/find.py @@ -169,7 +169,7 @@ class MyVibeButton(Button, VoiceExtension): else: _id = 'onyourwave' - await self.send_menu_message(interaction) + await self.send_menu_message(interaction, disable=True) await self.update_vibe( interaction, track_type_map[type(self.item)], diff --git a/MusicBot/ui/other.py b/MusicBot/ui/other.py index 4f08a8c..5e45197 100644 --- a/MusicBot/ui/other.py +++ b/MusicBot/ui/other.py @@ -6,19 +6,6 @@ from discord import ApplicationContext, ButtonStyle, Interaction, Embed from MusicBot.cogs.utils.voice_extension import VoiceExtension -def generate_playlists_embed(page: int, playlists: list[tuple[str, int]]) -> Embed: - count = 15 * page - length = len(playlists) - embed = Embed( - title=f"Всего плейлистов: {length}", - color=0xfed42b - ) - embed.set_author(name="Ваши плейлисты") - embed.set_footer(text=f"Страница {page + 1} из {ceil(length / 10)}") - for playlist in playlists[count:count + 10]: - embed.add_field(name=playlist[0], value=f"{playlist[1]} треков", inline=False) - return embed - def generate_queue_embed(page: int, tracks_list: list[dict[str, Any]]) -> Embed: count = 15 * page length = len(tracks_list) @@ -36,61 +23,6 @@ def generate_queue_embed(page: int, tracks_list: list[dict[str, Any]]) -> Embed: embed.add_field(name=f"{i} - {track['title']} - {duration_m}:{duration_s:02d}", value="", inline=False) return embed - -class MPNextButton(Button, VoiceExtension): - def __init__(self, **kwargs): - Button.__init__(self, **kwargs) - VoiceExtension.__init__(self, None) - - async def callback(self, interaction: Interaction) -> None: - if not interaction.user: - return - user = await self.users_db.get_user(interaction.user.id) - page = user['playlists_page'] + 1 - await self.users_db.update(interaction.user.id, {'playlists_page': page}) - embed = generate_playlists_embed(page, user['playlists']) - await interaction.edit(embed=embed, view=await MyPlaylists(interaction).init()) - -class MPPrevButton(Button, VoiceExtension): - def __init__(self, **kwargs): - Button.__init__(self, **kwargs) - VoiceExtension.__init__(self, None) - - async def callback(self, interaction: Interaction) -> None: - if not interaction.user: - return - user = await self.users_db.get_user(interaction.user.id) - page = user['playlists_page'] - 1 - await self.users_db.update(interaction.user.id, {'playlists_page': page}) - embed = generate_playlists_embed(page, user['playlists']) - await interaction.edit(embed=embed, view=await MyPlaylists(interaction).init()) - -class MyPlaylists(View, VoiceExtension): - 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) - - self.ctx = ctx - self.next_button = MPNextButton(style=ButtonStyle.primary, emoji='▶️') - self.prev_button = MPPrevButton(style=ButtonStyle.primary, emoji='◀️') - - async def init(self) -> Self: - if not self.ctx.user: - return self - - user = await self.users_db.get_user(self.ctx.user.id) - count = 10 * user['playlists_page'] - - if not user['playlists'][count + 10:]: - self.next_button.disabled = True - if not user['playlists'][:count]: - self.prev_button.disabled = True - - self.add_item(self.prev_button) - self.add_item(self.next_button) - - return self - class QueueNextButton(Button, VoiceExtension): def __init__(self, **kwargs): Button.__init__(self, **kwargs)