diff --git a/MusicBot/cogs/general.py b/MusicBot/cogs/general.py index 4867aec..bd7db3c 100644 --- a/MusicBot/cogs/general.py +++ b/MusicBot/cogs/general.py @@ -18,7 +18,7 @@ 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: + if not ctx.interaction.user or not ctx.value or not (100 > len(ctx.value) > 2): return [] uid = ctx.interaction.user.id @@ -41,6 +41,10 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]: logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {uid}") + if content_type not in ('Трек', 'Альбом', 'Артист', 'Плейлист'): + logging.error(f"[GENERAL] Invalid content type '{content_type}' for user {uid}") + return [] + if content_type == 'Трек' and search.tracks is not None: res = [f"{item.title} {f"({item.version})" if item.version else ''} - {", ".join(item.artists_name())}" for item in search.tracks.results] elif content_type == 'Альбом' and search.albums is not None: @@ -50,13 +54,13 @@ async def get_search_suggestions(ctx: discord.AutocompleteContext) -> list[str]: elif content_type == 'Плейлист' and search.playlists is not None: res = [f"{item.title}" for item in search.playlists.results] else: - logging.warning(f"[GENERAL] Invalid content type '{content_type}' for user {uid}") + logging.info(f"[GENERAL] Failed to get content type '{content_type}' with name '{ctx.value}' for user {uid}") return [] 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: + if not ctx.interaction.user or not ctx.value or not (100 > len(ctx.value) > 2): return [] uid = ctx.interaction.user.id @@ -70,21 +74,25 @@ async def get_user_playlists_suggestions(ctx: discord.AutocompleteContext) -> li except UnauthorizedError: logging.info(f"[GENERAL] User {uid} provided invalid token") return [] - - logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {uid}") - playlists_list = await client.users_playlists_list() + logging.debug(f"[GENERAL] Searching for '{ctx.value}' for user {uid}") + try: + playlists_list = await client.users_playlists_list() + except Exception as e: + logging.error(f"[GENERAL] Failed to get playlists for user {uid}: {e}") + return [] + return [playlist.title for playlist in playlists_list if playlist.title and ctx.value in playlist.title][:100] class General(Cog): - + def __init__(self, bot: discord.Bot): self.bot = bot self.db = BaseGuildsDatabase() self.users_db = users_db - + account = discord.SlashCommandGroup("account", "Команды, связанные с аккаунтом.") - + @discord.slash_command(description="Получить информацию о командах YandexMusic.") @discord.option( "command", @@ -208,10 +216,11 @@ class General(Cog): 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) + try: + client = await YMClient(token).init() + except UnauthorizedError: + logging.info(f"[GENERAL] Invalid token for user {ctx.user.id}") + await ctx.respond('❌ Неверный токен. Укажите новый через /account login.', delete_after=15, ephemeral=True) return likes = await client.users_likes_tracks() @@ -256,7 +265,7 @@ class General(Cog): client = await YMClient(token).init() except UnauthorizedError: logging.info(f"[GENERAL] User {ctx.user.id} provided invalid token") - await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True) + await ctx.respond('❌ Неверный токен. Укажите новый через /account login.', delete_after=15, ephemeral=True) return search = await client.search(content_type, type_='playlist') @@ -287,7 +296,7 @@ class General(Cog): 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}") + logging.info(f"[GENERAL] Playlist command invoked by user {ctx.user.id} in guild {ctx.guild_id}") token = await self.users_db.get_ym_token(ctx.user.id) if not token: @@ -299,10 +308,15 @@ class General(Cog): client = await YMClient(token).init() except UnauthorizedError: logging.info(f"[GENERAL] User {ctx.user.id} provided invalid token") - await ctx.respond("❌ Недействительный токен. Если это не так, попробуйте ещё раз.", delete_after=15, ephemeral=True) + await ctx.respond('❌ Неверный токен. Укажите новый через /account login.', delete_after=15, ephemeral=True) return - playlists = await client.users_playlists_list() + try: + playlists = await client.users_playlists_list() + except UnauthorizedError: + logging.warning(f"[GENERAL] Unknown token error for user {ctx.user.id}") + await ctx.respond("❌ Произошла неизвестная ошибка при попытке получения плейлистов. Пожалуйста, сообщите об этом разработчику.", delete_after=15, ephemeral=True) + return playlist = next((playlist for playlist in playlists if playlist.title == name), None) if not playlist: diff --git a/MusicBot/cogs/utils/embeds.py b/MusicBot/cogs/utils/embeds.py index c737242..89e7e97 100644 --- a/MusicBot/cogs/utils/embeds.py +++ b/MusicBot/cogs/utils/embeds.py @@ -1,5 +1,6 @@ import logging -from typing import cast +from functools import lru_cache +from typing import cast, Final from math import ceil from os import getenv @@ -10,29 +11,35 @@ from PIL import Image from yandex_music import Track, Album, Artist, Playlist, Label from discord import Embed +explicit_eid: Final[str | None] = getenv('EXPLICIT_EID') +if not explicit_eid: + raise ValueError('You must specify explicit emoji id in your enviroment (EXPLICIT_EID).') + async def generate_item_embed(item: Track | Album | Artist | Playlist | list[Track], vibing: bool = False) -> Embed: """Generate item embed. list[Track] is used for likes. If vibing is True, add vibing image. Args: - item (yandex_music.Track | yandex_music.Album | yandex_music.Artist | yandex_music.Playlist): Item to be processed. + item (Track | Album | Artist | Playlist | list[Track]): Item to be processed. + vibing (bool, optional): Add vibing image. Defaults to False. Returns: discord.Embed: Item embed. """ logging.debug(f"[EMBEDS] Generating embed for type: '{type(item).__name__}'") - if isinstance(item, Track): - embed = await _generate_track_embed(item) - elif isinstance(item, Album): - embed = await _generate_album_embed(item) - elif isinstance(item, Artist): - embed = await _generate_artist_embed(item) - elif isinstance(item, Playlist): - embed = await _generate_playlist_embed(item) - elif isinstance(item, list): - embed = _generate_likes_embed(item) - else: - raise ValueError(f"Unknown item type: {type(item).__name__}") + match item: + case Track(): + embed = await _generate_track_embed(item) + case Album(): + embed = await _generate_album_embed(item) + case Artist(): + embed = await _generate_artist_embed(item) + case Playlist(): + embed = await _generate_playlist_embed(item) + case list(): + embed = _generate_likes_embed(item) + case _: + raise ValueError(f"Unknown item type: {type(item).__name__}") if vibing: embed.set_image( @@ -41,13 +48,12 @@ async def generate_item_embed(item: Track | Album | Artist | Playlist | list[Tra return embed def _generate_likes_embed(tracks: list[Track]) -> Embed: - track_count = len(tracks) cover_url = "https://avatars.yandex.net/get-music-user-playlist/11418140/favorit-playlist-cover.bb48fdb9b9f4/300x300" embed = Embed( title="Мне нравится", description="Треки, которые вам понравились.", - color=0xce3a26, + color=0xce3a26 ) embed.set_thumbnail(url=cover_url) @@ -56,203 +62,143 @@ def _generate_likes_embed(tracks: list[Track]) -> Embed: if track.duration_ms: duration += track.duration_ms - duration_m = duration // 60000 - duration_s = ceil(duration / 1000) - duration_m * 60 - if duration_s == 60: - duration_m += 1 - duration_s = 0 - - embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}") - - if track_count is not None: - embed.add_field(name="Треки", value=str(track_count)) + embed.add_field(name="Длительность", value=_format_duration(duration)) + embed.add_field(name="Треки", value=str(len(tracks))) return embed async def _generate_track_embed(track: Track) -> Embed: - title = cast(str, track.title) - avail = cast(bool, track.available) - artists = track.artists_name() + title = track.title albums = [cast(str, album.title) for album in track.albums] - lyrics = cast(bool, track.lyrics_available) - duration = cast(int, track.duration_ms) explicit = track.explicit or track.content_warning - bg_video = track.background_video_uri - metadata = track.meta_data - year = track.albums[0].year - artist = track.artists[0] + year = track.albums[0].year if track.albums else None + artist = track.artists[0] if track.artists else None - cover_url = track.get_cover_url('400x400') - color = await _get_average_color_from_url(cover_url) + if track.cover_uri: + cover_url = track.get_cover_url('400x400') + color = await _get_average_color_from_url(cover_url) + else: + cover_url = None + color = 0x000 - if explicit: - explicit_eid = getenv('EXPLICIT_EID') - if not explicit_eid: - raise ValueError('You must specify explicit emoji id in your enviroment (EXPLICIT_EID).') + if explicit and title: title += ' <:explicit:' + explicit_eid + '>' - duration_m = duration // 60000 - duration_s = ceil(duration / 1000) - duration_m * 60 - if duration_s == 60: - duration_m += 1 - duration_s = 0 + if artist: + artist_url = f"https://music.yandex.ru/artist/{artist.id}" + artist_cover = artist.cover - artist_url = f"https://music.yandex.ru/artist/{artist.id}" - artist_cover = artist.cover - - if not artist_cover and artist.op_image: - artist_cover_url = artist.get_op_image_url() - elif artist_cover: - artist_cover_url = artist_cover.get_url() + if not artist_cover and artist.op_image: + artist_cover_url = artist.get_op_image_url() + elif artist_cover: + artist_cover_url = artist_cover.get_url() + else: + artist_cover_url = None else: + artist_url = None artist_cover_url = None embed = Embed( title=title, description=", ".join(albums), - color=color, + color=color ) embed.set_thumbnail(url=cover_url) - embed.set_author(name=", ".join(artists), url=artist_url, icon_url=artist_cover_url) + embed.set_author(name=", ".join(track.artists_name()), url=artist_url, icon_url=artist_cover_url) - embed.add_field(name="Текст песни", value="Есть" if lyrics else "Нет") - embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}") + embed.add_field(name="Текст песни", value="Есть" if track.lyrics_available else "Нет") + + if isinstance(track.duration_ms, int): + embed.add_field(name="Длительность", value=_format_duration(track.duration_ms)) if year: embed.add_field(name="Год выпуска", value=str(year)) - if metadata: - if metadata.year: - embed.add_field(name="Год выхода", value=str(metadata.year)) + if track.background_video_uri: + embed.add_field(name="Видеофон", value=f"[Ссылка]({track.background_video_uri})") - if metadata.number: - embed.add_field(name="Позиция", value=str(metadata.number)) - - if metadata.composer: - embed.add_field(name="Композитор", value=metadata.composer) - - if metadata.version: - embed.add_field(name="Версия", value=metadata.version) - - if bg_video: - embed.add_field(name="Видеофон", value=f"[Ссылка]({bg_video})") - - if not avail: + if not (track.available or track.available_for_premium_users): embed.set_footer(text=f"Трек в данный момент недоступен.") return embed async def _generate_album_embed(album: Album) -> Embed: - title = cast(str, album.title) - track_count = album.track_count - artists = album.artists_name() - avail = cast(bool, album.available) - description = album.short_description - year = album.year - version = album.version - bests = album.bests - duration = album.duration_ms + title = album.title explicit = album.explicit or album.content_warning - likes_count = album.likes_count artist = album.artists[0] - cover_url = album.get_cover_url('400x400') - color = await _get_average_color_from_url(cover_url) if isinstance(album.labels[0], Label): labels = [cast(Label, label).name for label in album.labels] else: labels = [cast(str, label) for label in album.labels] - if version: - title += f' *{version}*' + if album.version and title: + title += f' *{album.version}*' - if explicit: - explicit_eid = getenv('EXPLICIT_EID') - if not explicit_eid: - raise ValueError('You must specify explicit emoji id in your enviroment.') + if explicit and title: title += ' <:explicit:' + explicit_eid + '>' artist_url = f"https://music.yandex.ru/artist/{artist.id}" artist_cover = 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('400x400') elif artist_cover: - artist_cover_url = artist_cover.get_url() + artist_cover_url = artist_cover.get_url(size='400x400') else: artist_cover_url = None embed = Embed( title=title, - description=description, - color=color, + description=album.short_description, + color=await _get_average_color_from_url(cover_url) ) embed.set_thumbnail(url=cover_url) - embed.set_author(name=", ".join(artists), url=artist_url, icon_url=artist_cover_url) + embed.set_author(name=", ".join(album.artists_name()), url=artist_url, icon_url=artist_cover_url) - if year: - embed.add_field(name="Год выпуска", value=str(year)) + if album.year: + embed.add_field(name="Год выпуска", value=str(album.year)) - if duration: - duration_m = duration // 60000 - duration_s = ceil(duration / 1000) - duration_m * 60 - if duration_s == 60: - duration_m += 1 - duration_s = 0 - embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}") + if isinstance(album.duration_ms, int): + embed.add_field(name="Длительность", value=_format_duration(album.duration_ms)) - if track_count is not None: - if track_count > 1: - embed.add_field(name="Треки", value=str(track_count)) - else: - embed.add_field(name="Треки", value="Сингл") + if album.track_count is not None: + embed.add_field(name="Треки", value=str(album.track_count) if album.track_count > 1 else "Сингл") - if likes_count: - embed.add_field(name="Лайки", value=str(likes_count)) + if album.likes_count is not None: + embed.add_field(name="Лайки", value=str(album.likes_count)) - if len(labels) > 1: - embed.add_field(name="Лейблы", value=", ".join(labels)) - else: - embed.add_field(name="Лейбл", value=", ".join(labels)) + embed.add_field(name="Лейблы" if len(labels) > 1 else "Лейбл", value=", ".join(labels)) - if not avail: + if not (album.available or album.available_for_premium_users): embed.set_footer(text=f"Альбом в данный момент недоступен.") return embed async def _generate_artist_embed(artist: Artist) -> Embed: - name = cast(str, artist.name) - likes_count = artist.likes_count - avail = cast(bool, artist.available) - counts = artist.counts - description = artist.description - ratings = artist.ratings - popular_tracks = artist.popular_tracks - if not artist.cover: cover_url = artist.get_op_image_url('400x400') else: cover_url = artist.cover.get_url(size='400x400') - color = await _get_average_color_from_url(cover_url) embed = Embed( - title=name, - description=description.text if description else None, - color=color, + title=artist.name, + description=artist.description.text if artist.description else None, + color=await _get_average_color_from_url(cover_url) ) embed.set_thumbnail(url=cover_url) - if likes_count: - embed.add_field(name="Лайки", value=str(likes_count)) + if artist.likes_count: + embed.add_field(name="Лайки", value=str(artist.likes_count)) # if ratings: - # embed.add_field(name="Слушателей за месяц", value=str(ratings.month)) # Wrong numbers? + # embed.add_field(name="Слушателей за месяц", value=str(ratings.month)) # Wrong numbers - if counts: - embed.add_field(name="Треки", value=str(counts.tracks)) + if artist.counts: + embed.add_field(name="Треки", value=str(artist.counts.tracks)) - embed.add_field(name="Альбомы", value=str(counts.direct_albums)) + embed.add_field(name="Альбомы", value=str(artist.counts.direct_albums)) if artist.genres: genres = [genre.capitalize() for genre in artist.genres] @@ -261,23 +207,12 @@ async def _generate_artist_embed(artist: Artist) -> Embed: else: embed.add_field(name="Жанр", value=", ".join(genres)) - if not avail: + if not artist.available or artist.reason: embed.set_footer(text=f"Артист в данный момент недоступен.") return embed 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 - year = playlist.created - modified = playlist.modified - duration = playlist.duration_ms - likes_count = playlist.likes_count - - cover_url = None - if playlist.cover and playlist.cover.uri: cover_url = f"https://{playlist.cover.uri.replace('%%', '400x400')}" else: @@ -287,6 +222,8 @@ async def _generate_playlist_embed(playlist: Playlist) -> Embed: if track and track.albums and track.albums[0].cover_uri: cover_url = f"https://{track.albums[0].cover_uri.replace('%%', '400x400')}" break + else: + cover_url = None if cover_url: color = await _get_average_color_from_url(cover_url) @@ -294,37 +231,33 @@ async def _generate_playlist_embed(playlist: Playlist) -> Embed: color = 0x000 embed = Embed( - title=title, - description=description, - color=color, + title=playlist.title, + description=playlist.description, + color=color ) embed.set_thumbnail(url=cover_url) - if year: - embed.add_field(name="Год создания", value=str(year).split('-')[0]) + if playlist.created: + embed.add_field(name="Год создания", value=str(playlist.created).split('-')[0]) - if modified: - embed.add_field(name="Изменён", value=str(modified).split('-')[0]) + if playlist.modified: + embed.add_field(name="Изменён", value=str(playlist.modified).split('-')[0]) - if duration: - duration_m = duration // 60000 - duration_s = ceil(duration / 1000) - duration_m * 60 - if duration_s == 60: - duration_m += 1 - duration_s = 0 - embed.add_field(name="Длительность", value=f"{duration_m}:{duration_s:02}") + if playlist.duration_ms: + embed.add_field(name="Длительность", value=_format_duration(playlist.duration_ms)) - if track_count is not None: - embed.add_field(name="Треки", value=str(track_count)) + if playlist.track_count is not None: + embed.add_field(name="Треки", value=str(playlist.track_count)) - if likes_count: - embed.add_field(name="Лайки", value=str(likes_count)) + if playlist.likes_count: + embed.add_field(name="Лайки", value=str(playlist.likes_count)) - if not avail: + if not playlist.available: embed.set_footer(text=f"Плейлист в данный момент недоступен.") return embed +@lru_cache() async def _get_average_color_from_url(url: str) -> int: """Get image from url and calculate its average color to use in embeds. @@ -358,5 +291,13 @@ async def _get_average_color_from_url(url: str) -> int: b = b_total // count return (r << 16) + (g << 8) + b - except Exception: + except (aiohttp.ClientError, IOError, ValueError): return 0x000 + +def _format_duration(duration_ms: int) -> str: + duration_m = duration_ms // 60000 + duration_s = ceil(duration_ms / 1000) - duration_m * 60 + if duration_s == 60: + duration_m += 1 + duration_s = 0 + return f"{duration_m}:{duration_s:02}" \ No newline at end of file diff --git a/MusicBot/cogs/utils/voice_extension.py b/MusicBot/cogs/utils/voice_extension.py index 62abdda..fb2c801 100644 --- a/MusicBot/cogs/utils/voice_extension.py +++ b/MusicBot/cogs/utils/voice_extension.py @@ -163,6 +163,7 @@ class VoiceExtension: guild = await self.db.get_guild(gid, projection={'vibing': 1, 'current_menu': 1, 'current_track': 1}) if not guild['current_menu']: + logging.debug("[VC_EXT] No current menu found") return False menu_message = await self.get_menu_message(ctx, guild['current_menu']) if not menu_message else menu_message @@ -281,7 +282,7 @@ class VoiceExtension: uid = viber_id if viber_id else ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None if not uid or not gid: - logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'vibe_update'") + logging.warning("[VC_EXT] Guild ID or User ID not found in context") return False user = await self.users_db.get_user(uid, projection={'ym_token': 1, 'vibe_settings': 1}) @@ -516,6 +517,7 @@ class VoiceExtension: vc: discord.VoiceClient | None = None, *, after: bool = False, + client: YMClient | None = None, menu_message: discord.Message | None = None, button_callback: bool = False ) -> str | None: @@ -526,6 +528,7 @@ class VoiceExtension: ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context vc (discord.VoiceClient, optional): Voice client. after (bool, optional): Whether the function is being called by the after callback. Defaults to False. + client (YMClient | None, optional): Yandex Music client. Defaults to None. menu_message (discord.Message | None): Menu message. If None, fetches menu from channel using message id from database. Defaults to None. button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False. @@ -548,13 +551,6 @@ class VoiceExtension: logging.debug("[VC_EXT] Playback is stopped, skipping after callback.") return None - if not (client := await self.init_ym_client(ctx, user['ym_token'])): - return None - - if not (vc := await self.get_voice_client(ctx) if not vc else vc): - logging.debug("[VC_EXT] Voice client not found in 'next_track'") - return None - if guild['current_track'] and guild['current_menu'] and not guild['repeat']: logging.debug("[VC_EXT] Adding current track to history") await self.db.modify_track(gid, guild['current_track'], 'previous', 'insert') diff --git a/MusicBot/cogs/voice.py b/MusicBot/cogs/voice.py index d00a488..d052ae7 100644 --- a/MusicBot/cogs/voice.py +++ b/MusicBot/cogs/voice.py @@ -204,8 +204,8 @@ class Voice(Cog, VoiceExtension): @voice.command(name="menu", description="Создать или обновить меню проигрывателя.") async def menu(self, ctx: discord.ApplicationContext) -> None: logging.info(f"[VOICE] Menu command invoked by user {ctx.author.id} in guild {ctx.guild.id}") - if await self.voice_check(ctx): - await self.send_menu_message(ctx) + if await self.voice_check(ctx) and not await self.send_menu_message(ctx): + await ctx.respond("❌ Не удалось создать меню.", ephemeral=True) @voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.") async def join(self, ctx: discord.ApplicationContext) -> None: @@ -213,17 +213,17 @@ class Voice(Cog, VoiceExtension): member = cast(discord.Member, ctx.author) guild = await self.db.get_guild(ctx.guild.id, projection={'allow_change_connect': 1}) - vc = await self.get_voice_client(ctx) + await ctx.defer(ephemeral=True) if not member.guild_permissions.manage_channels and not guild['allow_change_connect']: response_message = "❌ У вас нет прав для выполнения этой команды." - elif vc and vc.is_connected(): - response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave." elif isinstance(ctx.channel, discord.VoiceChannel): try: await ctx.channel.connect() except TimeoutError: response_message = "❌ Не удалось подключиться к голосовому каналу." + except discord.ClientException: + response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave." else: response_message = "✅ Подключение успешно!" else: @@ -302,6 +302,10 @@ class Voice(Cog, VoiceExtension): await self.users_db.update(ctx.user.id, {'queue_page': 0}) tracks = await self.db.get_tracks_list(ctx.guild.id, 'next') + if len(tracks) == 0: + await ctx.respond("❌ Очередь пуста.", ephemeral=True) + return + embed = generate_queue_embed(0, tracks) await ctx.respond(embed=embed, view=await QueueView(ctx).init(), ephemeral=True) @@ -340,6 +344,7 @@ class Voice(Cog, VoiceExtension): ) return + await ctx.defer(ephemeral=True) res = await self.stop_playing(ctx, full=True) if res: await ctx.respond("✅ Воспроизведение остановлено.", delete_after=15, ephemeral=True) @@ -435,18 +440,16 @@ class Voice(Cog, VoiceExtension): } ) return - - feedback = await self.update_vibe(ctx, _type, _id) - if not feedback: + if not await self.update_vibe(ctx, _type, _id): await ctx.respond("❌ Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", delete_after=15, ephemeral=True) return if guild['current_menu']: await ctx.respond("✅ Моя Волна включена.", delete_after=15, ephemeral=True) - else: - await self.send_menu_message(ctx, disable=True) + elif not await self.send_menu_message(ctx, disable=True): + await ctx.respond("❌ Не удалось отправить меню. Попробуйте позже.", delete_after=15, ephemeral=True) next_track = await self.db.get_track(ctx.guild_id, 'next') if next_track: - await self._play_track(ctx, next_track) + await self.play_track(ctx, next_track) diff --git a/MusicBot/main.py b/MusicBot/main.py index bdff7cc..2b7fc88 100644 --- a/MusicBot/main.py +++ b/MusicBot/main.py @@ -5,7 +5,6 @@ import discord from discord.ext.commands import Bot intents = discord.Intents.default() -intents.message_content = True bot = Bot(intents=intents) cogs_list = [ diff --git a/MusicBot/ui/find.py b/MusicBot/ui/find.py index 8a10b5a..b5784e1 100644 --- a/MusicBot/ui/find.py +++ b/MusicBot/ui/find.py @@ -19,11 +19,11 @@ class PlayButton(Button, VoiceExtension): logging.debug(f"[FIND] Callback triggered for type: '{type(self.item).__name__}'") if not interaction.guild: - logging.warning("[FIND] No guild found in PlayButton callback") + logging.info("[FIND] No guild found in PlayButton callback") + await interaction.respond("❌ Эта команда доступна только на серверах.", ephemeral=True, delete_after=15) return if not await self.voice_check(interaction): - logging.debug("[FIND] Voice check failed in PlayButton callback") return gid = interaction.guild.id @@ -41,7 +41,7 @@ class PlayButton(Button, VoiceExtension): album = await self.item.with_tracks_async() if not album or not album.volumes: logging.debug("[FIND] Failed to fetch album tracks in PlayButton callback") - await interaction.respond("❌ Не удалось получить треки альбома.", ephemeral=True) + await interaction.respond("❌ Не удалось получить треки альбома.", ephemeral=True, delete_after=15) return tracks = [track for volume in album.volumes for track in volume] @@ -53,7 +53,7 @@ class PlayButton(Button, VoiceExtension): artist_tracks = await self.item.get_tracks_async() if not artist_tracks: logging.debug("[FIND] Failed to fetch artist tracks in PlayButton callback") - await interaction.respond("❌ Не удалось получить треки артиста.", ephemeral=True) + await interaction.respond("❌ Не удалось получить треки артиста.", ephemeral=True, delete_after=15) return tracks = artist_tracks.tracks.copy() @@ -65,7 +65,7 @@ class PlayButton(Button, VoiceExtension): short_tracks = await self.item.fetch_tracks_async() if not short_tracks: logging.debug("[FIND] Failed to fetch playlist tracks in PlayButton callback") - await interaction.respond("❌ Не удалось получить треки из плейлиста.", delete_after=15) + await interaction.respond("❌ Не удалось получить треки из плейлиста.", ephemeral=True, delete_after=15) return tracks = [cast(Track, short_track.track) for short_track in short_tracks] @@ -77,7 +77,7 @@ class PlayButton(Button, VoiceExtension): tracks = self.item.copy() if not tracks: logging.debug("[FIND] Empty tracks list in PlayButton callback") - await interaction.respond("❌ Не удалось получить треки.", delete_after=15) + await interaction.respond("❌ Не удалось получить треки.", ephemeral=True, delete_after=15) return action = 'add_playlist' @@ -109,21 +109,20 @@ class PlayButton(Button, VoiceExtension): ) return - logging.debug(f"[FIND] Skipping vote for '{action}'") - if guild['current_menu']: await interaction.respond(response_message, delete_after=15) - else: - await self.send_menu_message(interaction, disable=True) + elif not await self.send_menu_message(interaction, disable=True): + await interaction.respond('❌ Не удалось отправить сообщение.', ephemeral=True, delete_after=15) - if guild['current_track'] is not None: + if guild['current_track']: logging.debug(f"[FIND] Adding tracks to queue") await self.db.modify_track(gid, tracks, 'next', 'extend') else: logging.debug(f"[FIND] Playing track") track = tracks.pop(0) await self.db.modify_track(gid, tracks, 'next', 'extend') - await self.play_track(interaction, track) + if not await self.play_track(interaction, track): + await interaction.respond('❌ Не удалось воспроизвести трек.', ephemeral=True, delete_after=15) if interaction.message: await interaction.message.delete() @@ -146,6 +145,7 @@ class MyVibeButton(Button, VoiceExtension): logging.warning(f"[VIBE] Guild ID is None in button callback") return + track_type_map = { Track: 'track', Album: 'album', Artist: 'artist', Playlist: 'playlist', list: 'user' } @@ -153,7 +153,7 @@ class MyVibeButton(Button, VoiceExtension): if isinstance(self.item, Playlist): if not self.item.owner: logging.warning(f"[VIBE] Playlist owner is None") - await interaction.respond("❌ Не удалось получить информацию о плейлисте.", ephemeral=True) + await interaction.respond("❌ Не удалось получить информацию о плейлисте. Отсутствует владелец.", ephemeral=True, delete_after=15) return _id = self.item.owner.login + '_' + str(self.item.kind) @@ -162,12 +162,11 @@ class MyVibeButton(Button, VoiceExtension): else: _id = 'onyourwave' - await self.send_menu_message(interaction, disable=True) - await self.update_vibe( - interaction, - track_type_map[type(self.item)], - _id - ) + guild = await self.db.get_guild(gid, projection={'current_menu': 1}) + if not guild['current_menu'] and not await self.send_menu_message(interaction, disable=True): + await interaction.respond('❌ Не удалось отправить сообщение.', ephemeral=True, delete_after=15) + + await self.update_vibe(interaction, track_type_map[type(self.item)], _id) next_track = await self.db.get_track(gid, 'next') if next_track: @@ -208,6 +207,6 @@ class ListenView(View): async def on_timeout(self) -> None: try: return await super().on_timeout() - except discord.NotFound: + except discord.HTTPException: pass self.stop() diff --git a/MusicBot/ui/menu.py b/MusicBot/ui/menu.py index ff87d85..b5c2e64 100644 --- a/MusicBot/ui/menu.py +++ b/MusicBot/ui/menu.py @@ -2,7 +2,7 @@ import logging from typing import Self, cast from discord.ui import View, Button, Item, Select -from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption, Member +from discord import VoiceChannel, ButtonStyle, Interaction, ApplicationContext, RawReactionActionEvent, Embed, ComponentType, SelectOption, Member, HTTPException import yandex_music.exceptions from yandex_music import TrackLyrics, Playlist, ClientAsync as YMClient @@ -348,7 +348,7 @@ class MyVibeSelect(Select, VoiceExtension): await interaction.edit(view=view) class MyVibeSettingsView(View, VoiceExtension): - def __init__(self, interaction: Interaction, *items: Item, timeout: float | None = None, disable_on_timeout: bool = True): + def __init__(self, interaction: 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.interaction = interaction @@ -410,6 +410,13 @@ class MyVibeSettingsView(View, VoiceExtension): self.add_item(select) return self + + async def on_timeout(self) -> None: + try: + return await super().on_timeout() + except HTTPException: + pass + self.stop() class MyVibeSettingsButton(Button, VoiceExtension): def __init__(self, **kwargs): @@ -530,7 +537,7 @@ class MenuView(View, VoiceExtension): if not self.ctx.guild_id: return self - self.guild = await self.db.get_guild(self.ctx.guild_id) + self.guild = await self.db.get_guild(self.ctx.guild_id, projection={'repeat': 1, 'shuffle': 1, 'current_track': 1, 'vibing': 1}) if self.guild['repeat']: self.repeat_button.style = ButtonStyle.success diff --git a/MusicBot/ui/other.py b/MusicBot/ui/other.py index 5e45197..11a6f87 100644 --- a/MusicBot/ui/other.py +++ b/MusicBot/ui/other.py @@ -2,7 +2,7 @@ from math import ceil from typing import Self, Any from discord.ui import View, Button, Item -from discord import ApplicationContext, ButtonStyle, Interaction, Embed +from discord import ApplicationContext, ButtonStyle, Interaction, Embed, HTTPException from MusicBot.cogs.utils.voice_extension import VoiceExtension @@ -27,10 +27,11 @@ class QueueNextButton(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 or not interaction.guild: return + user = await self.users_db.get_user(interaction.user.id) page = user['queue_page'] + 1 await self.users_db.update(interaction.user.id, {'queue_page': page}) @@ -42,10 +43,11 @@ class QueuePrevButton(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 or not interaction.guild: return + user = await self.users_db.get_user(interaction.user.id) page = user['queue_page'] - 1 await self.users_db.update(interaction.user.id, {'queue_page': page}) @@ -54,30 +56,36 @@ class QueuePrevButton(Button, VoiceExtension): await interaction.edit(embed=embed, view=await QueueView(interaction).init()) class QueueView(View, VoiceExtension): - def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = True): + def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 360, disable_on_timeout: bool = False): View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout) VoiceExtension.__init__(self, None) self.ctx = ctx self.next_button = QueueNextButton(style=ButtonStyle.primary, emoji='▶️') self.prev_button = QueuePrevButton(style=ButtonStyle.primary, emoji='◀️') - + async def init(self) -> Self: if not self.ctx.user or not self.ctx.guild: return self tracks = await self.db.get_tracks_list(self.ctx.guild.id, 'next') user = await self.users_db.get_user(self.ctx.user.id) - + count = 15 * user['queue_page'] - + if not tracks[count + 15:]: self.next_button.disabled = True if not tracks[:count]: self.prev_button.disabled = True - + self.add_item(self.prev_button) self.add_item(self.next_button) - + return self - \ No newline at end of file + + async def on_timeout(self) -> None: + try: + await super().on_timeout() + except HTTPException: + pass + self.stop() \ No newline at end of file