import logging from typing import Literal, cast from asyncio import gather import discord from discord.ext.commands import Cog import yandex_music import yandex_music.exceptions from yandex_music import ClientAsync as YMClient 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.cogs.utils.embeds import generate_item_embed 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: return [] users_db = BaseUsersDatabase() token = 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 [] content_type = ctx.options['тип'] search = await client.search(ctx.value) if not search: logging.warning(f"[GENERAL] Failed to search for '{ctx.value}' for user {ctx.interaction.user.id}") return [] res = [] logging.debug(f"[GENERAL] 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}") else: 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): def __init__(self, bot: discord.Bot): self.bot = bot self.db = BaseGuildsDatabase() self.users_db = BaseUsersDatabase() account = discord.SlashCommandGroup("account", "Команды, связанные с аккаунтом.") @discord.slash_command(description="Получить информацию о командах YandexMusic.") @discord.option( "command", description="Название команды.", type=discord.SlashCommandOptionType.string, default='all' ) async def help(self, ctx: discord.ApplicationContext, command: str) -> None: logging.info(f"[GENERAL] Help command invoked by {ctx.user.id} for command '{command}'") response_message = None embed = discord.Embed( title='Помощь', color=0xfed42b ) embed.set_author(name='YandexMusic') embed.description = '__Использование__\n' if command == 'all': embed.description = ( "Этот бот позволяет слушать музыку из вашего аккаунта Yandex Music.\n" "Зарегистрируйте свой токен с помощью /login. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n" "Для получения помощи по конкретной команде, введите /help <команда>.\n" "Для изменения настроек необходимо иметь права управления каналами на сервере.\n\n" "**Для дополнительной помощи, присоединяйтесь к [серверу любителей Яндекс Музыки](https://discord.gg/gkmFDaPMeC).**" ) embed.add_field( name='__Основные команды__', value="""`account` `find` `help` `like` `queue` `settings` `track` `voice`""" ) embed.set_footer(text='©️ Bananchiki') elif command == 'account': embed.description += ( "Ввести токен от Яндекс Музыки. Его можно получить [здесь](https://github.com/MarshalX/yandex-music-api/discussions/513).\n" "```/account login ```\n" "Удалить токен из базы данных бота.\n```/account remove```\n" "Получить ваши плейлисты. Чтобы добавить плейлист в очередь, используйте команду /find.\n```/account playlists```\n" "Получить плейлист «Мне нравится».\n```/account likes```\n" ) elif command == 'find': embed.description += ( "Вывести информацию о треке (по умолчанию), альбоме, авторе или плейлисте. Позволяет добавить музыку в очередь. " "В названии можно уточнить автора или версию. Возвращается лучшее совпадение.\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" "Очистить очередь треков и историю прослушивания. Доступно только если вы единственный в голосовом канале " "или имеете разрешение управления каналом.\n```/queue clear```\n" ) elif command == 'settings': embed.description += ( "Получить текущие настройки.\n```/settings show```\n" "Разрешить или запретить воспроизведение Explicit треков и альбомов. Если автор или плейлист содержат Explicit треки, убираются кнопки для доступа к ним.\n```/settings explicit```\n" "Разрешить или запретить создание меню проигрывателя, когда в канале больше одного человека.\n```/settings menu```\n" "Разрешить или запретить голосование.\n```/settings vote <тип голосования>```\n" "`Примечание`: Только пользователи с разрешением управления каналом могут менять настройки." ) elif command == 'track': embed.description += ( "`Примечание`: Если вы один в голосовом канале или имеете разрешение управления каналом, голосование не начинается.\n\n" "Переключиться на следующий трек в очереди. \n```/track next```\n" "Приостановить текущий трек.\n```/track pause```\n" "Возобновить текущий трек.\n```/track resume```\n" "Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```\n" "Запустить Мою Волну по текущему треку.\n```/track vibe```" ) elif command == 'voice': embed.description += ( "`Примечание`: Доступность меню и Моей Волны зависит от настроек сервера.\n\n" "Присоединить бота в голосовой канал. Требует разрешения управления каналом.\n```/voice join```\n" "Заставить бота покинуть голосовой канал. Требует разрешения управления каналом.\n ```/voice leave```\n" "Создать меню проигрывателя. По умолчанию работает только когда в канале один человек.\n```/voice menu```\n" "Запустить Мою Волну. По умолчанию работает только когда в канале один человек.\n```/vibe```" ) else: response_message = '❌ Неизвестная команда.' embed = None await ctx.respond(response_message, embed=embed, ephemeral=True) @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}") try: client = await YMClient(token).init() except yandex_music.exceptions.UnauthorizedError: logging.info(f"[GENERAL] Invalid token provided by user {ctx.author.id}") await ctx.respond('❌ Недействительный токен.', delete_after=15, ephemeral=True) return about = cast(yandex_music.Status, client.me).to_dict() uid = ctx.author.id self.users_db.update(uid, {'ym_token': token}) 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="Удалить токен из датабазы бота.") 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}") self.users_db.update(ctx.user.id, {'ym_token': None}) await ctx.respond(f'Токен был удалён.', delete_after=15, ephemeral=True) @account.command(description="Получить плейлист «Мне нравится»") async def likes(self, ctx: discord.ApplicationContext) -> None: logging.info(f"[GENERAL] Likes command invoked by user {ctx.author.id} in guild {ctx.guild.id}") token = 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() if not client.me or not client.me.account or not client.me.account.uid: logging.warning(f"Failed to fetch user info for user {ctx.user.id}") await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) return likes = await client.users_likes_tracks() if likes is None: logging.info(f"[GENERAL] Failed to fetch likes for user {ctx.user.id}") await ctx.respond('❌ Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) return elif not likes: logging.info(f"[GENERAL] Empty likes for user {ctx.user.id}") await ctx.respond('❌ У вас нет треков в плейлисте «Мне нравится».', delete_after=15, ephemeral=True) return real_tracks = await gather(*[track_short.fetch_track_async() for track_short in likes.tracks], return_exceptions=True) tracks = [track for track in real_tracks if not isinstance(track, BaseException)] # Can't fetch user tracks embed = await generate_item_embed(tracks) logging.info(f"[GENERAL] Successfully fetched likes for user {ctx.user.id}") await ctx.respond(embed=embed, view=ListenView(tracks)) @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}") token = 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() 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[tuple[str, int]] = [ (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}) embed = generate_playlists_embed(0, playlists) logging.info(f"[GENERAL] Successfully fetched playlists for user {ctx.user.id}") await ctx.respond(embed=embed, view=MyPlaylists(ctx), ephemeral=True) @discord.slash_command(description="Найти контент и отправить информацию о нём. Возвращается лучшее совпадение.") @discord.option( "тип", parameter_name='content_type', description="Тип контента для поиска.", type=discord.SlashCommandOptionType.string, choices=['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'], ) @discord.option( "запрос", parameter_name='name', description="Название контента для поиска (По умолчанию трек).", type=discord.SlashCommandOptionType.string, autocomplete=discord.utils.basic_autocomplete(get_search_suggestions) ) async def find( self, ctx: discord.ApplicationContext, content_type: Literal['Трек', 'Альбом', 'Артист', 'Плейлист', 'Свой плейлист'], name: str ) -> None: logging.info(f"[GENERAL] 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) token = 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 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 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) 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 not tracks: logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' is empty") await ctx.respond("❌ Плейлист пуст.", delete_after=15, ephemeral=True) return for track_short in 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] Successfully generated '{content_type}' message for user {ctx.author.id}") await ctx.respond(embed=embed, view=view)