feat: Add find autocomplete.

This commit is contained in:
Lemon4ksan
2025-01-25 19:57:55 +03:00
parent 6a20ab11d1
commit 85f7ee6c6c
5 changed files with 157 additions and 64 deletions

View File

@@ -18,9 +18,55 @@ from MusicBot.cogs.utils.embeds import generate_item_embed
def setup(bot): def setup(bot):
bot.add_cog(General(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:
return []
users_db = BaseUsersDatabase()
token = users_db.get_ym_token(ctx.interaction.user.id)
if not token:
return ['❌ Укажите токен через /account login.']
try:
client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError:
logging.info(f"User {ctx.interaction.user.id} provided invalid token")
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 ["❌ Что-то пошло не так. Повторите попытку позже"]
res = []
logging.debug(f"Searching for '{ctx.value}' for user {ctx.interaction.user.id}")
if content_type == 'Трек' and search.tracks:
for item in search.tracks.results:
res.append(f"{item.title} {f"({item.version})" if item.version else ''} - {", ".join(item.artists_name())}")
elif content_type == 'Альбом' and search.albums:
for item in search.albums.results:
res.append(f"{item.title} - {", ".join(item.artists_name())}")
elif content_type == 'Артист' and search.artists:
for item in search.artists.results:
res.append(f"{item.name}")
elif content_type == 'Плейлист' and search.playlists:
for item in search.playlists.results:
res.append(f"{item.title}")
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]
return res
class General(Cog): class General(Cog):
def __init__(self, bot): def __init__(self, bot: discord.Bot):
self.bot = bot self.bot = bot
self.db = BaseGuildsDatabase() self.db = BaseGuildsDatabase()
self.users_db = BaseUsersDatabase() self.users_db = BaseUsersDatabase()
@@ -36,6 +82,7 @@ class General(Cog):
) )
async def help(self, ctx: discord.ApplicationContext, command: str) -> None: async def help(self, ctx: discord.ApplicationContext, command: str) -> None:
logging.info(f"Help command invoked by {ctx.user.id} for command '{command}'") logging.info(f"Help command invoked by {ctx.user.id} for command '{command}'")
response_message = None response_message = None
embed = discord.Embed( embed = discord.Embed(
title='Помощь', title='Помощь',
@@ -45,60 +92,76 @@ class General(Cog):
embed.description = '__Использование__\n' embed.description = '__Использование__\n'
if command == 'all': if command == 'all':
embed.description = ("Данный бот позволяет вам слушать музыку из вашего аккаунта Yandex Music.\n" embed.description = (
"Этот бот позволяет слушать музыку из вашего аккаунта Yandex Music.\n"
"Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n" "Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
"Для получения помощи для конкретной команды, введите /help <команда>.\n\n" "Для получения помощи по конкретной команде, введите /help <команда>.\n\n"
"**Для доп. помощи, зайдите на [сервер любителей Яндекс Музыки](https://discord.gg/gkmFDaPMeC).**") "**Для дополнительной помощи, присоединяйтесь к [серверу любителей Яндекс Музыки](https://discord.gg/gkmFDaPMeC).**"
)
embed.add_field( embed.add_field(
name='__Основные команды__', name='__Основные команды__',
value=""" value="""`account`
`account`
`find` `find`
`help` `help`
`like` `like`
`queue` `queue`
`settings` `settings`
`track` `track`
`voice` `voice`"""
"""
) )
embed.set_footer(text='©️ Bananchiki') embed.set_footer(text='©️ Bananchiki')
elif command == 'account': elif command == 'account':
embed.description += ("Ввести токен от Яндекс Музыки. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n" embed.description += (
"Ввести токен от Яндекс Музыки. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n"
"```/account login <token>```\n" "```/account login <token>```\n"
"Удалить токен из датабазы бота.\n```/account remove```\n" "Удалить токен из базы данных бота.\n```/account remove```\n"
"Получить ваши плейлисты. Чтобы добавить плейлист в очередь, используйте команду /find.\n```/account playlists```\n" "Получить ваши плейлисты. Чтобы добавить плейлист в очередь, используйте команду /find.\n```/account playlists```\n"
"Получить плейлист «Мне нравится». \n```/account likes```\n") "Получить плейлист «Мне нравится».\n```/account likes```\n"
)
elif command == 'find': elif command == 'find':
embed.description += ("Вывести информацию о треке (по умолчанию), альбоме, авторе или плейлисте. Позволяет добавить музыку в очередь. " embed.description += (
"В названии можно уточнить автора или версию. Возвращается лучшее совпадение.\n```/find <название> <тип>```") "Вывести информацию о треке (по умолчанию), альбоме, авторе или плейлисте. Позволяет добавить музыку в очередь. "
"В названии можно уточнить автора или версию. Возвращается лучшее совпадение.\n```/find <название> <тип>```"
)
elif command == 'help': elif command == 'help':
embed.description += ("Вывести список всех команд.\n```/help```\n" embed.description += (
"Получить информацию о конкретной команде.\n```/help <команда>```") "Вывести список всех команд.\n```/help```\n"
"Получить информацию о конкретной команде.\n```/help <команда>```"
)
elif command == 'like': elif command == 'like':
embed.description += "Добавить трек в плейлист «Мне нравится». Пользовательские треки из этого плейлиста игнорируются.\n```/like```" embed.description += (
"Добавить трек в плейлист «Мне нравится». Пользовательские треки из этого плейлиста игнорируются.\n```/like```"
)
elif command == 'queue': elif command == 'queue':
embed.description += ("Получить очередь треков. По 15 элементов на страницу.\n```/queue get```\n" embed.description += (
"Очистить очередь треков и историю прослушивания. Доступно только если вы единственный в голосовом канале" "Получить очередь треков. По 15 элементов на страницу.\n```/queue get```\n"
"или имеете разрешение управления каналом.\n```/queue clear```\n") "Очистить очередь треков и историю прослушивания. Доступно только если вы единственный в голосовом канале "
"или имеете разрешение управления каналом.\n```/queue clear```\n"
)
elif command == 'settings': elif command == 'settings':
embed.description += ("Получить текущие настройки.\n```/settings show```\n" embed.description += (
"Получить текущие настройки.\n```/settings show```\n"
"Разрешить или запретить воспроизведение Explicit треков и альбомов. Если автор или плейлист содержат Explicit треки, убираются кнопки для доступа к ним.\n```/settings explicit```\n" "Разрешить или запретить воспроизведение Explicit треков и альбомов. Если автор или плейлист содержат Explicit треки, убираются кнопки для доступа к ним.\n```/settings explicit```\n"
"Разрешить или запретить создание меню проигрывателя, когда в канале больше одного человека.\n```/settings menu```\n" "Разрешить или запретить создание меню проигрывателя, когда в канале больше одного человека.\n```/settings menu```\n"
"Разрешить или запретить голосование.\n```/settings vote <тип голосования>```\n" "Разрешить или запретить голосование.\n```/settings vote <тип голосования>```\n"
"`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки.") "`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки."
)
elif command == 'track': elif command == 'track':
embed.description += ("`Примечание`: Если вы один в голосовом канале или имеете разрешение управления каналом, голосование не начинается.\n\n" embed.description += (
"`Примечание`: Если вы один в голосовом канале или имеете разрешение управления каналом, голосование не начинается.\n\n"
"Переключиться на следующий трек в очереди. \n```/track next```\n" "Переключиться на следующий трек в очереди. \n```/track next```\n"
"Приостановить текущий трек.\n ```/track pause```\n" "Приостановить текущий трек.\n ```/track pause```\n"
"Возобновить текущий трек.\n ```/track resume```\n" "Возобновить текущий трек.\n ```/track resume```\n"
"Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```") "Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```"
)
elif command == 'voice': elif command == 'voice':
embed.description += ("Присоединить бота в голосовой канал. Требует разрешения управления каналом.\n ```/voice join```\n" embed.description += (
"Присоединить бота в голосовой канал. Требует разрешения управления каналом.\n ```/voice join```\n"
"Заставить бота покинуть голосовой канал. Требует разрешения управления каналом.\n ```/voice leave```\n" "Заставить бота покинуть голосовой канал. Требует разрешения управления каналом.\n ```/voice leave```\n"
"Создать меню проигрывателя. Доступность зависит от настроек сервера. По умолчанию работает только когда в канале один человек.\n```/voice menu```") "Создать меню проигрывателя. Доступность зависит от настроек сервера. По умолчанию работает только когда в канале один человек.\n```/voice menu```"
)
else: else:
response_message = '❌ Неизвестная команда.' response_message = '❌ Неизвестная команда.'
embed = None embed = None
@@ -160,43 +223,52 @@ class General(Cog):
@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"Playlists command invoked by user {ctx.user.id} in guild {ctx.guild_id}") logging.info(f"Playlists command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
token = self.users_db.get_ym_token(ctx.user.id) token = self.users_db.get_ym_token(ctx.user.id)
if not token: if not token:
await ctx.respond('❌ Необходимо указать свой токен доступа с помощью команды /login.', delete_after=15, ephemeral=True) await ctx.respond('❌ Необходимо указать свой токен доступа с помощью команды /login.', delete_after=15, ephemeral=True)
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: if not client.me or not client.me.account or not client.me.account.uid:
await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True)
return return
playlists_list = await client.users_playlists_list(client.me.account.uid) playlists_list = await client.users_playlists_list(client.me.account.uid)
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
] ]
self.users_db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0}) self.users_db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0})
embed = generate_playlists_embed(0, playlists) embed = generate_playlists_embed(0, playlists)
logging.info(f"Successfully fetched playlists for user {ctx.user.id}") logging.info(f"Successfully fetched playlists for user {ctx.user.id}")
await ctx.respond(embed=embed, view=MyPlaylists(ctx), ephemeral=True) await ctx.respond(embed=embed, view=MyPlaylists(ctx), ephemeral=True)
discord.Option
@discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.") @discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.")
@discord.option( @discord.option(
"name", "тип",
description="Название контента для поиска (По умолчанию трек).", parameter_name='content_type',
type=discord.SlashCommandOptionType.string description="Тип контента для поиска.",
)
@discord.option(
"content_type",
description="Тип искомого контента.",
type=discord.SlashCommandOptionType.string, type=discord.SlashCommandOptionType.string,
choices=['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'], choices=['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'],
default='Трек' )
@discord.option(
"запрос",
parameter_name='name',
description="Название контента для поиска (По умолчанию трек).",
type=discord.SlashCommandOptionType.string,
autocomplete=discord.utils.basic_autocomplete(get_search_suggestions)
) )
async def find( async def find(
self, self,
ctx: discord.ApplicationContext, ctx: discord.ApplicationContext,
name: str, content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'],
content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'] = 'Трек' name: str
) -> None: ) -> None:
logging.info(f"Find command invoked by user {ctx.user.id} in guild {ctx.guild_id} for '{content_type}' with name '{name}'") logging.info(f"Find command invoked by user {ctx.user.id} in guild {ctx.guild_id} for '{content_type}' with name '{name}'")
guild = self.db.get_guild(ctx.guild_id) guild = self.db.get_guild(ctx.guild_id)
token = self.users_db.get_ym_token(ctx.user.id) token = self.users_db.get_ym_token(ctx.user.id)
if not token: if not token:

View File

@@ -27,6 +27,7 @@ class VoiceExtension:
Returns: Returns:
bool: True if updated, False if not. bool: True if updated, False if not.
""" """
from MusicBot.ui import MenuView
logging.debug( logging.debug(
f"Updating player embed using " + f"Updating player embed using " +
"interaction context" if isinstance(ctx, Interaction) else "interaction context" if isinstance(ctx, Interaction) else
@@ -41,7 +42,7 @@ class VoiceExtension:
logging.warning("Guild ID or User ID not found in context inside 'update_player_embed'") logging.warning("Guild ID or User ID not found in context inside 'update_player_embed'")
return False return False
player = await self.get_player_message(ctx, player_mid) player = await self.get_menu_message(ctx, player_mid)
if not player: if not player:
return False return False
@@ -63,14 +64,14 @@ class VoiceExtension:
if isinstance(ctx, Interaction) and ctx.message and ctx.message.id == player_mid: if isinstance(ctx, Interaction) and ctx.message and ctx.message.id == player_mid:
# If interaction from player buttons # If interaction from player buttons
await ctx.edit(embed=embed) await ctx.edit(embed=embed, view=await MenuView(ctx).init())
else: else:
# If interaction from other buttons or commands. They should have their own response. # If interaction from other buttons or commands. They should have their own response.
await player.edit(embed=embed) await player.edit(embed=embed, view=await MenuView(ctx).init())
return True return True
async def get_player_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, player_mid: int) -> discord.Message | None: async def get_menu_message(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, player_mid: int) -> discord.Message | None:
"""Fetch the player message by its id. Return the message if found, None if not. """Fetch the player message by its id. Return the message if found, None if not.
Reset `current_player` field in the database if not found. Reset `current_player` field in the database if not found.
@@ -369,7 +370,7 @@ class VoiceExtension:
return None return None
async def get_likes(self, ctx: ApplicationContext | Interaction) -> list[TrackShort] | None: async def get_likes(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> list[TrackShort] | None:
"""Get liked tracks. Return list of tracks on success. """Get liked tracks. Return list of tracks on success.
Return None if no token found. Return None if no token found.
@@ -380,12 +381,14 @@ class VoiceExtension:
list[Track] | None: List of tracks or None. list[Track] | None: List of tracks or None.
""" """
if not ctx.guild or not ctx.user: gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
logging.warning("Guild or User not found in context inside 'like_track'") uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid:
logging.warning("Guild ID or User ID not found in context inside 'play_track'")
return None return None
current_track = self.db.get_track(ctx.guild.id, 'current') current_track = self.db.get_track(gid, 'current')
token = self.users_db.get_ym_token(ctx.user.id) token = self.users_db.get_ym_token(uid)
if not current_track or not token: if not current_track or not token:
logging.debug("Current track or token not found") logging.debug("Current track or token not found")
return None return None

View File

@@ -83,6 +83,10 @@ class Voice(Cog, VoiceExtension):
guild = self.db.get_guild(guild_id) guild = self.db.get_guild(guild_id)
votes = guild['votes'] votes = guild['votes']
if payload.message_id not in votes:
logging.info(f"Message {payload.message_id} not found in votes")
return
vote_data = votes[str(payload.message_id)] vote_data = votes[str(payload.message_id)]
if payload.emoji.name == '': if payload.emoji.name == '':
logging.info(f"User {payload.user_id} voted positively for message {payload.message_id}") logging.info(f"User {payload.user_id} voted positively for message {payload.message_id}")
@@ -214,7 +218,7 @@ class Voice(Cog, VoiceExtension):
if guild['current_player']: if guild['current_player']:
logging.info(f"Deleteing old player menu {guild['current_player']} in guild {ctx.guild.id}") logging.info(f"Deleteing old player menu {guild['current_player']} in guild {ctx.guild.id}")
message = await self.get_player_message(ctx, guild['current_player']) message = await self.get_menu_message(ctx, guild['current_player'])
if message: if message:
await message.delete() await message.delete()
@@ -354,7 +358,7 @@ class Voice(Cog, VoiceExtension):
current_player = self.db.get_current_player(ctx.guild.id) current_player = self.db.get_current_player(ctx.guild.id)
if current_player: if current_player:
player = await self.get_player_message(ctx, current_player) player = await self.get_menu_message(ctx, current_player)
if player: if player:
await player.delete() await player.delete()

View File

@@ -121,7 +121,7 @@ class PlayButton(Button, VoiceExtension):
current_player = None current_player = None
if guild['current_player']: if guild['current_player']:
current_player = await self.get_player_message(interaction, guild['current_player']) current_player = await self.get_menu_message(interaction, guild['current_player'])
if current_player and interaction.message: if current_player and interaction.message:
logging.debug(f"Deleting interaction message {interaction.message.id}: current player {current_player.id} found") logging.debug(f"Deleting interaction message {interaction.message.id}: current player {current_player.id} found")
@@ -130,7 +130,7 @@ class PlayButton(Button, VoiceExtension):
await interaction.respond(response_message, delete_after=15) await interaction.respond(response_message, delete_after=15)
class ListenView(View): class ListenView(View):
def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = None, disable_on_timeout: bool = False): def __init__(self, item: Track | Album | Artist | Playlist | list[Track], *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False):
super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout) super().__init__(*items, timeout=timeout, disable_on_timeout=disable_on_timeout)
logging.debug(f"Creating view for type: '{type(item).__name__}'") logging.debug(f"Creating view for type: '{type(item).__name__}'")

View File

@@ -140,7 +140,7 @@ class LyricsButton(Button, VoiceExtension):
class MenuView(View, VoiceExtension): class MenuView(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 | RawReactionActionEvent, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = False):
View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout) View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout)
VoiceExtension.__init__(self, None) VoiceExtension.__init__(self, None)
if not ctx.guild_id: if not ctx.guild_id:
@@ -167,7 +167,7 @@ class MenuView(View, VoiceExtension):
self.add_item(self.next_button) self.add_item(self.next_button)
self.add_item(self.shuffle_button) self.add_item(self.shuffle_button)
if len(cast(VoiceChannel, self.ctx.channel).members) > 2: if isinstance(self.ctx, RawReactionActionEvent) or len(cast(VoiceChannel, self.ctx.channel).members) > 2:
self.like_button.disabled = True self.like_button.disabled = True
elif likes and current_track and str(current_track['id']) in [str(like.id) for like in likes]: elif likes and current_track and str(current_track['id']) in [str(like.id) for like in likes]:
self.like_button.style = ButtonStyle.success self.like_button.style = ButtonStyle.success
@@ -179,3 +179,17 @@ class MenuView(View, VoiceExtension):
self.add_item(self.lyrics_button) self.add_item(self.lyrics_button)
return self return self
async def on_timeout(self) -> None:
logging.debug('Menu timed out...')
if not self.ctx.guild_id:
return
if self.guild['current_player']:
self.db.update(self.ctx.guild_id, {'current_player': None, 'previous_tracks': []})
message = await self.get_menu_message(self.ctx, self.guild['current_player'])
if message:
await message.delete()
logging.debug('Successfully deleted menu message')
else:
logging.debug('No menu message found')