impr: Changed /account playlists functionality

This commit is contained in:
Lemon4ksan
2025-02-12 13:53:38 +03:00
parent 41d5319836
commit c0bb10cbf8
5 changed files with 132 additions and 179 deletions

View File

@@ -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 <token>```\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)

View File

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

View File

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

View File

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

View File

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