mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-10 09:41:46 +03:00
feat: Add account recommendations.
This commit is contained in:
@@ -173,7 +173,7 @@ class General(Cog):
|
|||||||
|
|
||||||
await ctx.respond(response_message, embed=embed, ephemeral=True)
|
await ctx.respond(response_message, embed=embed, ephemeral=True)
|
||||||
|
|
||||||
@account.command(description="Ввести токен от Яндекс Музыки.")
|
@account.command(description="Ввести токен Яндекс Музыки.")
|
||||||
@discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.")
|
@discord.option("token", type=discord.SlashCommandOptionType.string, description="Токен.")
|
||||||
async def login(self, ctx: discord.ApplicationContext, token: str) -> None:
|
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}")
|
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}")
|
logging.info(f"[GENERAL] Token saved for user {ctx.author.id}")
|
||||||
await ctx.respond(f'Привет, {about['account']['first_name']}!', delete_after=15, ephemeral=True)
|
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:
|
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}")
|
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})
|
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}")
|
logging.info(f"[GENERAL] Successfully fetched likes for user {ctx.user.id}")
|
||||||
await ctx.respond(embed=embed, view=ListenView(tracks))
|
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="Получить ваши плейлисты.")
|
@account.command(description="Получить ваши плейлисты.")
|
||||||
async def playlists(self, ctx: discord.ApplicationContext) -> None:
|
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}")
|
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
|
return
|
||||||
|
|
||||||
client = await YMClient(token).init()
|
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]] = [
|
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
|
(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['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'],
|
content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'],
|
||||||
name: str
|
name: str
|
||||||
) -> None:
|
) -> 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}'")
|
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})
|
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_explicit': 1})
|
||||||
@@ -291,12 +345,8 @@ class General(Cog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if content_type == 'Свой плейлист':
|
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)
|
result = next((playlist for playlist in playlists if playlist.title == name), None)
|
||||||
if not result:
|
if not result:
|
||||||
logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' not found")
|
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']:
|
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")
|
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned explicit content and is not allowed on this server")
|
||||||
view = None
|
view = None
|
||||||
embed.set_footer(text="Воспроизведение недоступно, так как у автора присутствуют Explicit треки")
|
embed.set_footer(text="Воспроизведение недоступно, так как в плейлисте присутствуют Explicit треки")
|
||||||
break
|
break
|
||||||
|
|
||||||
logging.info(f"[GENERAL] Successfully generated '{content_type}' message for user {ctx.author.id}")
|
logging.info(f"[GENERAL] Successfully generated '{content_type}' message for user {ctx.author.id}")
|
||||||
|
|||||||
@@ -99,10 +99,13 @@ async def _generate_track_embed(track: Track) -> Embed:
|
|||||||
|
|
||||||
artist_url = f"https://music.yandex.ru/artist/{artist.id}"
|
artist_url = f"https://music.yandex.ru/artist/{artist.id}"
|
||||||
artist_cover = artist.cover
|
artist_cover = artist.cover
|
||||||
if not artist_cover:
|
|
||||||
|
if not artist_cover and artist.op_image:
|
||||||
artist_cover_url = artist.get_op_image_url()
|
artist_cover_url = artist.get_op_image_url()
|
||||||
else:
|
elif artist_cover:
|
||||||
artist_cover_url = artist_cover.get_url()
|
artist_cover_url = artist_cover.get_url()
|
||||||
|
else:
|
||||||
|
artist_cover_url = None
|
||||||
|
|
||||||
embed = Embed(
|
embed = Embed(
|
||||||
title=title,
|
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_url = f"https://music.yandex.ru/artist/{artist.id}"
|
||||||
artist_cover = artist.cover
|
artist_cover = artist.cover
|
||||||
if not artist_cover:
|
|
||||||
|
if not artist_cover and artist.op_image:
|
||||||
artist_cover_url = artist.get_op_image_url()
|
artist_cover_url = artist.get_op_image_url()
|
||||||
else:
|
elif artist_cover:
|
||||||
artist_cover_url = artist_cover.get_url()
|
artist_cover_url = artist_cover.get_url()
|
||||||
|
else:
|
||||||
|
artist_cover_url = None
|
||||||
|
|
||||||
embed = Embed(
|
embed = Embed(
|
||||||
title=title,
|
title=title,
|
||||||
@@ -264,7 +270,7 @@ async def _generate_playlist_embed(playlist: Playlist) -> Embed:
|
|||||||
title = cast(str, playlist.title)
|
title = cast(str, playlist.title)
|
||||||
track_count = playlist.track_count
|
track_count = playlist.track_count
|
||||||
avail = cast(bool, playlist.available)
|
avail = cast(bool, playlist.available)
|
||||||
description = playlist.description_formatted
|
description = playlist.description
|
||||||
year = playlist.created
|
year = playlist.created
|
||||||
modified = playlist.modified
|
modified = playlist.modified
|
||||||
duration = playlist.duration_ms
|
duration = playlist.duration_ms
|
||||||
|
|||||||
@@ -155,15 +155,31 @@ class MyVibeButton(Button, VoiceExtension):
|
|||||||
|
|
||||||
track_type_map: dict[Any, Literal['track', 'album', 'artist', 'playlist', 'user']] = {
|
track_type_map: dict[Any, Literal['track', 'album', 'artist', 'playlist', 'user']] = {
|
||||||
Track: 'track', Album: 'album', Artist: 'artist', Playlist: 'playlist', list: '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.send_menu_message(interaction)
|
||||||
await self.update_vibe(
|
await self.update_vibe(
|
||||||
interaction,
|
interaction,
|
||||||
track_type_map[type(self.item)],
|
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):
|
class ListenView(View):
|
||||||
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True):
|
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)
|
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
|
||||||
|
|||||||
Reference in New Issue
Block a user