diff --git a/MusicBot/cogs/general.py b/MusicBot/cogs/general.py index 312006c..7e52c44 100644 --- a/MusicBot/cogs/general.py +++ b/MusicBot/cogs/general.py @@ -173,7 +173,7 @@ class General(Cog): await ctx.respond(response_message, embed=embed, ephemeral=True) - @account.command(description="Ввести токен от Яндекс Музыки.") + @account.command(description="Ввести токен Яндекс Музыки.") @discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.") async def login(self, ctx: discord.ApplicationContext, token: str) -> None: logging.info(f"[GENERAL] Login command invoked by user {ctx.author.id} in guild {ctx.guild.id}") @@ -190,7 +190,7 @@ class General(Cog): logging.info(f"[GENERAL] Token saved for user {ctx.author.id}") await ctx.respond(f'Привет, {about['account']['first_name']}!', delete_after=15, ephemeral=True) - @account.command(description="Удалить токен из датабазы бота.") + @account.command(description="Удалить токен из базы данных бота.") async def remove(self, ctx: discord.ApplicationContext) -> None: logging.info(f"[GENERAL] Remove command invoked by user {ctx.author.id} in guild {ctx.guild.id}") await self.users_db.update(ctx.user.id, {'ym_token': None}) @@ -228,6 +228,60 @@ class General(Cog): logging.info(f"[GENERAL] Successfully fetched likes for user {ctx.user.id}") await ctx.respond(embed=embed, view=ListenView(tracks)) + @account.command(description="Получить ваши рекомендации.") + @discord.option( + 'тип', + parameter_name='content_type', + description="Вид рекомендаций.", + type=discord.SlashCommandOptionType.string, + choices=['Премьера', 'Плейлист дня', 'Дежавю'] + ) + async def recommendations( + self, + ctx: discord.ApplicationContext, + content_type: Literal['Премьера', 'Плейлист дня', 'Дежавю'] + )-> None: + # NOTE: Recommendations can be accessed by using /find, but it's more convenient to have it in separate command. + logging.debug(f"[GENERAL] Recommendations command invoked by user {ctx.user.id} in guild {ctx.guild_id} for type '{content_type}'") + + guild = await self.db.get_guild(ctx.guild_id) + token = await self.users_db.get_ym_token(ctx.user.id) + if not token: + await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True) + return + + client = await YMClient(token).init() + + search = await client.search(content_type, False, 'playlist') + if not search or not search.playlists: + logging.info(f"[GENERAL] Failed to fetch recommendations for user {ctx.user.id}") + await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) + return + + playlist = search.playlists.results[0] + if playlist is None: + logging.info(f"[GENERAL] Failed to fetch recommendations for user {ctx.user.id}") + await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) + + tracks = await playlist.fetch_tracks_async() + if not tracks: + logging.info(f"[GENERAL] User {ctx.user.id} search for '{content_type}' returned no tracks") + 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 '{content_type}' 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) + @account.command(description="Получить ваши плейлисты.") async def playlists(self, ctx: discord.ApplicationContext) -> None: logging.info(f"[GENERAL] Playlists command invoked by user {ctx.user.id} in guild {ctx.guild_id}") @@ -238,11 +292,8 @@ class General(Cog): return client = await YMClient(token).init() - if not client.me or not client.me.account or not client.me.account.uid: - await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) - return - playlists_list = await client.users_playlists_list(client.me.account.uid) + 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 ] @@ -274,6 +325,9 @@ class General(Cog): 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}'") guild = await self.db.get_guild(ctx.guild_id, projection={'allow_explicit': 1}) @@ -291,12 +345,8 @@ class General(Cog): return if content_type == 'Свой плейлист': - if not client.me or not client.me.account or not client.me.account.uid: - logging.warning(f"Failed to get user info for user {ctx.user.id}") - await ctx.respond("❌ Не удалось получить информацию о пользователе.", delete_after=15, ephemeral=True) - return - playlists = await client.users_playlists_list(client.me.account.uid) + 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") @@ -371,7 +421,7 @@ class General(Cog): 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 треки") + embed.set_footer(text="Воспроизведение недоступно, так как в плейлисте присутствуют Explicit треки") break logging.info(f"[GENERAL] Successfully generated '{content_type}' message for user {ctx.author.id}") diff --git a/MusicBot/cogs/utils/embeds.py b/MusicBot/cogs/utils/embeds.py index 96d2aeb..13b8c6e 100644 --- a/MusicBot/cogs/utils/embeds.py +++ b/MusicBot/cogs/utils/embeds.py @@ -99,10 +99,13 @@ async def _generate_track_embed(track: Track) -> Embed: artist_url = f"https://music.yandex.ru/artist/{artist.id}" artist_cover = artist.cover - if not artist_cover: + + if not artist_cover and artist.op_image: artist_cover_url = artist.get_op_image_url() - else: + elif artist_cover: artist_cover_url = artist_cover.get_url() + else: + artist_cover_url = None embed = Embed( title=title, @@ -172,10 +175,13 @@ async def _generate_album_embed(album: Album) -> Embed: artist_url = f"https://music.yandex.ru/artist/{artist.id}" artist_cover = artist.cover - if not artist_cover: + + if not artist_cover and artist.op_image: artist_cover_url = artist.get_op_image_url() - else: + elif artist_cover: artist_cover_url = artist_cover.get_url() + else: + artist_cover_url = None embed = Embed( title=title, @@ -264,7 +270,7 @@ async def _generate_playlist_embed(playlist: Playlist) -> Embed: title = cast(str, playlist.title) track_count = playlist.track_count avail = cast(bool, playlist.available) - description = playlist.description_formatted + description = playlist.description year = playlist.created modified = playlist.modified duration = playlist.duration_ms diff --git a/MusicBot/ui/find.py b/MusicBot/ui/find.py index a991c5f..80ca18b 100644 --- a/MusicBot/ui/find.py +++ b/MusicBot/ui/find.py @@ -155,15 +155,31 @@ class MyVibeButton(Button, VoiceExtension): track_type_map: dict[Any, Literal['track', 'album', 'artist', 'playlist', 'user']] = { Track: 'track', Album: 'album', Artist: 'artist', Playlist: 'playlist', list: 'user' - } # NOTE: Likes playlist should have its own entry instead of 'user:onyourwave' + } + + if isinstance(self.item, Playlist): + if not self.item.owner: + logging.warning(f"[VIBE] Playlist owner is None") + await interaction.respond("❌ Не удалось получить информацию о плейлисте.", ephemeral=True) + return + + _id = self.item.owner.login + '_' + str(self.item.kind) + elif not isinstance(self.item, list): + _id = cast(int | str, self.item.id) + else: + _id = 'onyourwave' await self.send_menu_message(interaction) await self.update_vibe( interaction, track_type_map[type(self.item)], - cast(int, self.item.uid) if isinstance(self.item, Playlist) else cast(int | str, self.item.id) if not isinstance(self.item, list) else 'onyourwave' + _id ) + next_track = await self.db.get_track(gid, 'next') + if next_track: + await self._play_next_track(interaction, next_track) + class ListenView(View): 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)