diff --git a/MusicBot/cogs/general.py b/MusicBot/cogs/general.py index 6f96411..6fd1f9a 100644 --- a/MusicBot/cogs/general.py +++ b/MusicBot/cogs/general.py @@ -147,7 +147,9 @@ class General(Cog): 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, playlist.track_count) for playlist in playlists_list] # type: ignore + 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.db.update(ctx.user.id, {'playlists': playlists, 'playlists_page': 0}) embed = generate_playlist_embed(0, playlists) await ctx.respond(embed=embed, view=MyPlalists(ctx), ephemeral=True) diff --git a/MusicBot/cogs/utils/find.py b/MusicBot/cogs/utils/find.py index 9530660..e75b2f4 100644 --- a/MusicBot/cogs/utils/find.py +++ b/MusicBot/cogs/utils/find.py @@ -1,4 +1,3 @@ -from asyncio import gather from os import getenv from math import ceil from typing import cast @@ -64,7 +63,7 @@ class PlayAlbumButton(Button, VoiceExtension): track = tracks.pop(0) self.db.modify_track(gid, tracks, 'next', 'extend') await self.play_track(interaction, track) - response_message = f"Сейчас играет: **{album.title}**!" + response_message = f"Сейчас играет: **{track.title}**!" if guild['current_player'] is not None and interaction.message: await interaction.message.delete() @@ -97,7 +96,7 @@ class PlayArtistButton(Button, VoiceExtension): track = tracks.pop(0) self.db.modify_track(gid, tracks, 'next', 'extend') await self.play_track(interaction, track) - response_message = f"Сейчас играет: **{self.artist.name}**!" + response_message = f"Сейчас играет: **{track.title}**!" if guild['current_player'] is not None and interaction.message: await interaction.message.delete() @@ -116,13 +115,13 @@ class PlayPlaylistButton(Button, VoiceExtension): short_tracks = await self.playlist.fetch_tracks_async() if not short_tracks: + await interaction.respond("Не удалось получить треки из плейлиста.", delete_after=15) return gid = interaction.guild.id guild = self.db.get_guild(gid) - real_tracks = await gather(*[track_short.fetch_track_async() for track_short in self.playlist.tracks], return_exceptions=True) - tracks = [track for track in real_tracks if not isinstance(track, BaseException)] # Can't fetch user tracks + tracks: list[Track] = [cast(Track, short_track.track) for short_track in short_tracks] if guild['current_track'] is not None: self.db.modify_track(gid, tracks, 'next', 'extend') @@ -148,8 +147,9 @@ class ListenTrack(View): self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web) self.button3: PlayTrackButton = PlayTrackButton(track, label="Слушать в голосовом канале", style=ButtonStyle.gray) # self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason. - self.add_item(self.button2) - self.add_item(self.button3) + if track.available: + self.add_item(self.button2) + self.add_item(self.button3) class ListenAlbum(View): @@ -161,8 +161,9 @@ class ListenAlbum(View): self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web) self.button3: PlayAlbumButton = PlayAlbumButton(album, label="Слушать в голосовом канале", style=ButtonStyle.gray) # self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason. - self.add_item(self.button2) - self.add_item(self.button3) + if album.available: + self.add_item(self.button2) + self.add_item(self.button3) class ListenArtist(View): @@ -174,8 +175,9 @@ class ListenArtist(View): self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web) self.button3: PlayArtistButton = PlayArtistButton(artist, label="Слушать в голосовом канале", style=ButtonStyle.gray) # self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason. - self.add_item(self.button2) - self.add_item(self.button3) + if artist.available: + self.add_item(self.button2) + self.add_item(self.button3) class ListenPlaylist(View): def __init__(self, playlist: Playlist, *items: Item, timeout: float | None = None, disable_on_timeout: bool = False): @@ -186,8 +188,9 @@ class ListenPlaylist(View): self.button2: Button = Button(label="Слушать в браузере", style=ButtonStyle.gray, url=link_web) self.button3: PlayPlaylistButton = PlayPlaylistButton(playlist, label="Слушать в голосовом канале", style=ButtonStyle.gray) # self.add_item(self.button1) # Discord doesn't allow well formed URLs in buttons for some reason. - self.add_item(self.button2) - self.add_item(self.button3) + if playlist.available: + self.add_item(self.button2) + self.add_item(self.button3) async def process_track(track: Track) -> Embed: """Generate track embed. @@ -430,10 +433,10 @@ async def process_playlist(playlist: Playlist) -> Embed: for i in range(len(tracks)): track = tracks[i].track try: - cover_url = f"https://{track.albums[0].cover_uri.replace('%%', '400x400')}" # type: ignore + cover_url = f"https://{track.albums[0].cover_uri.replace('%%', '400x400')}" # type: ignore # Errors are being caught below. break - except TypeError: - continue + except (TypeError, IndexError): + continue if cover_url: color = await get_average_color_from_url(cover_url) diff --git a/MusicBot/cogs/utils/misc.py b/MusicBot/cogs/utils/misc.py index f9acbe4..27b54eb 100644 --- a/MusicBot/cogs/utils/misc.py +++ b/MusicBot/cogs/utils/misc.py @@ -56,9 +56,10 @@ def generate_queue_embed(page: int, tracks_list: list[dict[str, Any]]) -> Embed: embed.set_footer(text=f"Страница {page + 1} из {ceil(length / 15)}") for i, track in enumerate(tracks_list[count:count + 15], start=1 + count): duration = track['duration_ms'] - duration_m = duration // 60000 - duration_s = ceil(duration / 1000) - duration_m * 60 - embed.add_field(name=f"{i} - {track['title']} - {duration_m}:{duration_s:02d}", value="", inline=False) + if duration: + duration_m = duration // 60000 + duration_s = ceil(duration / 1000) - duration_m * 60 + embed.add_field(name=f"{i} - {track['title']} - {duration_m}:{duration_s:02d}", value="", inline=False) return embed class PlayLikesButton(Button, VoiceExtension): @@ -82,7 +83,7 @@ class PlayLikesButton(Button, VoiceExtension): track = playlist.pop(0) self.db.modify_track(gid, playlist, 'next', 'extend') await self.play_track(interaction, track) - response_message = f"Сейчас играет плейлист **«Мне нравится»**!" + response_message = f"Сейчас играет плейлист **{track.title}**!" if guild['current_player'] is not None and interaction.message: await interaction.message.delete() diff --git a/MusicBot/cogs/utils/voice_extension.py b/MusicBot/cogs/utils/voice_extension.py index 9263ac1..32a036b 100644 --- a/MusicBot/cogs/utils/voice_extension.py +++ b/MusicBot/cogs/utils/voice_extension.py @@ -33,11 +33,18 @@ async def generate_player_embed(track: Track) -> discord.Embed: 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 = f"https://{track.cover_uri.replace('%%', '400x400')}" + else: + cover_url = None + + if cover_url: + color = await get_average_color_from_url(cover_url) + else: + color = None if explicit: explicit_eid = getenv('EXPLICIT_EID') @@ -48,12 +55,18 @@ async def generate_player_embed(track: Track) -> discord.Embed: duration_m = duration // 60000 duration_s = ceil(duration / 1000) - duration_m * 60 - artist_url = f"https://music.yandex.ru/artist/{artist.id}" - artist_cover = artist.cover - if not artist_cover: - artist_cover_url = artist.get_op_image_url() + if artist: + artist_url = f"https://music.yandex.ru/artist/{artist.id}" + artist_cover = artist.cover if artist else None + if artist and not artist_cover: + 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_cover_url = artist_cover.get_url() + artist_url = None + artist_cover_url = None embed = discord.Embed( title=title, @@ -134,7 +147,7 @@ class VoiceExtension: self.db = VoiceGuildsDatabase() self.users_db = BaseUsersDatabase() - async def update_player_embed(self, ctx: ApplicationContext | Interaction, player_mid: int) -> None: + async def update_player_embed(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, player_mid: int) -> None: """Update current player message by its id. Args: @@ -145,20 +158,31 @@ class VoiceExtension: try: if isinstance(ctx, Interaction): player = ctx.client.get_message(player_mid) - else: + elif isinstance(ctx, RawReactionActionEvent) and self.bot: + player = self.bot.get_message(player_mid) + elif isinstance(ctx, ApplicationContext): player = await ctx.fetch_message(player_mid) + else: + player = None except discord.DiscordException: return if not player: return - guild = ctx.guild - user = ctx.user - if guild and user: - token = self.users_db.get_ym_token(user.id) - current_track = self.db.get_track(guild.id, 'current') - track = cast(Track, Track.de_json(current_track, client=ClientAsync(token))) # type: ignore + gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None + uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None + + if gid and uid: + token = self.users_db.get_ym_token(uid) + current_track = self.db.get_track(gid, 'current') + if not current_track: + return + track = cast(Track, Track.de_json( + current_track, + client=ClientAsync(token) # type: ignore # Async client can be used here. + ) + ) embed = await generate_player_embed(track) if isinstance(ctx, Interaction) and ctx.message and ctx.message.id == player_mid: @@ -263,7 +287,7 @@ class VoiceExtension: self.db.update(gid, {'is_stopped': False}) player = guild['current_player'] - if player is not None and not isinstance(ctx, discord.RawReactionActionEvent): + if player is not None: await self.update_player_embed(ctx, player) return track.title @@ -318,9 +342,15 @@ class VoiceExtension: self.db.modify_track(gid, current_track, 'previous', 'insert') if next_track: - ym_track = Track.de_json(next_track, client=ClientAsync(token)) # type: ignore + ym_track = Track.de_json( + next_track, + client=ClientAsync(token) # type: ignore # Async client can be used here. + ) await self.stop_playing(ctx) - title = await self.play_track(ctx, ym_track) # type: ignore + title = await self.play_track( + ctx, + ym_track # type: ignore # de_json should always work here. + ) if after and not guild['current_player'] and not isinstance(ctx, discord.RawReactionActionEvent): await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15) @@ -348,9 +378,15 @@ class VoiceExtension: title = None if prev_track: - ym_track = Track.de_json(prev_track, client=ClientAsync(token)) # type: ignore + ym_track = Track.de_json( + prev_track, + client=ClientAsync(token) # type: ignore # Async client can be used here. + ) await self.stop_playing(ctx) - title = await self.play_track(ctx, ym_track) # type: ignore + title = await self.play_track( + ctx, + ym_track # type: ignore # de_json should always work here. + ) elif current_track: title = await self.repeat_current_track(ctx) @@ -375,9 +411,15 @@ class VoiceExtension: current_track = self.db.get_track(gid, 'current') if current_track: - ym_track = Track.de_json(current_track, client=ClientAsync(token)) # type: ignore + ym_track = Track.de_json( + current_track, + client=ClientAsync(token) # type: ignore # Async client can be used here. + ) await self.stop_playing(ctx) - return await self.play_track(ctx, ym_track) # type: ignore + return await self.play_track( + ctx, + ym_track # type: ignore # de_json should always work here. + ) return None @@ -403,11 +445,17 @@ class VoiceExtension: if not likes: return None - ym_track = cast(Track, Track.de_json(current_track, client=client)) # type: ignore + ym_track = cast(Track, Track.de_json( + current_track, + client=client # type: ignore # Async client can be used here. + ) + ) if ym_track.id not in [track.id for track in likes.tracks]: await ym_track.like_async() return ym_track.title else: - await client.users_likes_tracks_remove(ym_track.id, client.me.account.uid) # type: ignore + if not client.me or not client.me.account or not client.me.account.uid: + return None + await client.users_likes_tracks_remove(ym_track.id, client.me.account.uid) return 'TRACK REMOVED' \ No newline at end of file diff --git a/MusicBot/cogs/voice.py b/MusicBot/cogs/voice.py index c1b5a62..9f17b56 100644 --- a/MusicBot/cogs/voice.py +++ b/MusicBot/cogs/voice.py @@ -57,7 +57,10 @@ class Voice(Cog, VoiceExtension): @Cog.listener() async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None: - bot_id = self.bot.user.id # type: ignore + if not self.bot.user or not payload.member: + return + + bot_id = self.bot.user.id if payload.user_id == bot_id: return @@ -70,7 +73,7 @@ class Voice(Cog, VoiceExtension): return if not self.users_db.get_ym_token(payload.user_id): - await message.remove_reaction(payload.emoji, payload.member) # type: ignore + await message.remove_reaction(payload.emoji, payload.member) await channel.send("Для участия в голосовании необходимо авторизоваться через /account login.", delete_after=15) return @@ -114,6 +117,9 @@ class Voice(Cog, VoiceExtension): @Cog.listener() async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent) -> None: + if not self.bot.user: + return + guild_id = payload.guild_id if guild_id not in self.vote_messages: return @@ -126,7 +132,7 @@ class Voice(Cog, VoiceExtension): return message = await channel.fetch_message(payload.message_id) - if not message or message.author.id != self.bot.user.id: # type: ignore + if not message or message.author.id != self.bot.user.id: return vote_data = self.vote_messages[guild_id][payload.message_id] @@ -149,7 +155,12 @@ class Voice(Cog, VoiceExtension): return if guild['current_track']: - embed = await generate_player_embed(Track.de_json(guild['current_track'], client=ClientAsync())) # type: ignore + embed = await generate_player_embed( + Track.de_json( + guild['current_track'], + client=ClientAsync() # type: ignore # Async client can be used here. + ) + ) vc = await self.get_voice_client(ctx) if vc and vc.is_paused(): embed.set_footer(text='Приостановлено') diff --git a/MusicBot/database/extensions.py b/MusicBot/database/extensions.py index d0495a9..27cc0ce 100644 --- a/MusicBot/database/extensions.py +++ b/MusicBot/database/extensions.py @@ -121,7 +121,7 @@ class VoiceGuildsDatabase(BaseGuildsDatabase): else: raise ValueError(f"Unknown operation '{operation}'") - self.update(gid, {explicit_type: tracks}) # type: ignore + self.update(gid, {explicit_type: tracks}) # type: ignore return pop_track