diff --git a/cogs/music.py b/cogs/music.py index c9d3524..74f419f 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -1,8 +1,12 @@ import asyncio +import datetime import re from typing import Optional import discord +from discord.channel import TextChannel +from discord.ext.commands.context import Context +from discord.ext.commands.errors import CommandError import lavalink from discord import Embed from discord.channel import TextChannel @@ -16,6 +20,7 @@ from utils.classes import BaseCog from context import CustomContext from utils.database import AutoJoin from utils.database import Playlist +from utils.exceptions import EmbeddedCommandException from utils.EmbedGenerator import EmbedGenerator url_rx = re.compile(r"https?://(?:www\.)?.+") @@ -119,6 +124,26 @@ class Music(BaseCog): ) player.add(requester=self.bot.user.id, track=track) + async def create_track_embed(self, track: AudioTrack) -> Embed: + embed_color = self.bot.colors["embed"] + embed = discord.Embed( + title=f"Now playing...", + colour=embed_color, + ) + embed.description = f"[{track.title}]({track.uri})" + embed.set_thumbnail( + url=f"https://i3.ytimg.com/vi/{track.identifier}/mqdefault.jpg" + ) + + try: + duration = str(datetime.timedelta(milliseconds=int(track.duration))) + except OverflowError: + duration = "🔴 LIVE" + + embed.add_field(name="Duration", value=duration) + embed.add_field(name="Author", value=track.author) + return embed + def cog_unload(self): """Cog unload handler. This removes any event hooks that were registered.""" self.bot.lavalink._event_hooks.clear() @@ -139,13 +164,15 @@ class Music(BaseCog): return guild_check - async def cog_command_error(self, ctx, error): + async def cog_command_error(self, ctx: CustomContext, error: CommandError): if isinstance(error, commands.CommandInvokeError): await ctx.send(error.original) # The above handles errors thrown in this cog and shows them to the user. # This shouldn't be a problem as the only errors thrown in this cog are from `ensure_voice` # which contain a reason string, such as "Join a voicechannel" etc. You can modify the above # if you want to do things differently. + elif isinstance(error, EmbeddedCommandException): + await error.send(ctx) async def ensure_voice(self, ctx): """This check ensures that the bot and command author are in the same voicechannel.""" @@ -170,7 +197,14 @@ class Music(BaseCog): if not player.is_connected: if not should_connect: - raise commands.CommandInvokeError("Not connected.") + bot_name = self.bot.config["info"]["name"] + embed = await EmbedGenerator.Message( + ctx, + f"{bot_name} is not connected", + "However you can start playing music using `/connect`", + no_send=True, + ) + raise EmbeddedCommandException(embed) permissions = ctx.author.voice.channel.permissions_for(ctx.me) @@ -198,14 +232,7 @@ class Music(BaseCog): elif isinstance(event, lavalink.events.TrackStartEvent): channel_id = int(event.player.fetch("channel")) channel: TextChannel = self.bot.get_channel(channel_id) - - color = self.bot.colors["embed"] - current_track: AudioTrack = event.player.current - embed = Embed( - title="Now playing:", - description=f"[{current_track.title}]({current_track.uri})", - color=color, - ) + embed = await self.create_track_embed(event.player.current) await channel.send(embed=embed) elif isinstance(event, lavalink.events.TrackEndEvent): await self.fill_player_queue(event.player, 1) @@ -215,17 +242,17 @@ class Music(BaseCog): """Start the radio""" # Get the player for this guild from cache. player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) - if player.is_connected: await ctx.send("Already connected") return - await self.fill_player_queue(player, self.bot.config["queue_buffer_size"]) + await self.fill_player_queue(player, self.bot.config["queue_buffer_size"] if not player.is_playing: await player.play() - await ctx.send("Started playing") + await EmbedGenerator.Title(ctx, "*⃣ | Connected.") + return @commands.command(name="skip", aliases=["next"]) async def skip(self, ctx: Context): @@ -238,23 +265,33 @@ class Music(BaseCog): async def queue(self, ctx: Context): """Display the current radio queue""" player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) - await EmbedGenerator.Message(ctx, "Queue:", player.queue) + + embed_color = self.bot.colors["embed"] + embed = Embed(title="Coming Up...", colour=embed_color) + + if len(player.queue) > 0: + embed.description = "\n".join( + f"{index}. [{track.title}]({track.uri})" + for index, track in enumerate(player.queue, 1) + ) + else: + embed.description = "We are still determining a playlist" + + await ctx.send(embed=embed) @commands.command(name="disconnect", aliases=["dc", "stop"]) async def disconnect(self, ctx: Context): """Disconnects the radio from the channel""" player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) - if not player.is_connected: - return await EmbedGenerator.Title(ctx, "Not connected.") - if not ctx.author.voice or ( player.is_connected and ctx.author.voice.channel.id != int(player.channel_id) ): # Abuse prevention. Users not in voice channels, or not in the same voice channel as the bot # may not disconnect the bot. - return await EmbedGenerator.Title(ctx, "You're not in my voicechannel!") + await EmbedGenerator.Title(ctx, "You're not in my voicechannel!") + return # Clear the queue to ensure old tracks don't start playing # when someone else queues something. @@ -265,6 +302,13 @@ class Music(BaseCog): await ctx.voice_client.disconnect(force=True) await EmbedGenerator.Title(ctx, "*⃣ | Disconnected.") + @commands.command(name="now") + async def now_playing(self, ctx: CustomContext): + player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + track = player.current + embed = await self.create_track_embed(track) + await ctx.send(embed=embed) + def setup(bot: ChristmasBot): bot.add_cog(Music(bot)) diff --git a/utils/EmbedGenerator.py b/utils/EmbedGenerator.py index 0d66ff8..95cf8c0 100644 --- a/utils/EmbedGenerator.py +++ b/utils/EmbedGenerator.py @@ -1,8 +1,8 @@ -from typing import Optional +from typing import Optional, Union import discord -from discord import Embed from discord.ext.commands import Context +from discord import Embed class EmbedGenerator: @@ -13,7 +13,9 @@ class EmbedGenerator: return await EmbedGenerator.SendWithFooter(ctx, em, **kwargs) @staticmethod - async def Message(ctx: Context, title: str, message: Optional[str] = "", **kwargs): + async def Message( + ctx: Context, title: str, message: Optional[str] = "", **kwargs + ) -> Embed: color = ctx.bot.colors["embed"] em = Embed(title=title, description=message, color=color) return await EmbedGenerator.SendWithFooter(ctx, em, **kwargs) @@ -21,20 +23,22 @@ class EmbedGenerator: @staticmethod async def Image( ctx: Context, title: str, url: str, message: Optional[str] = "", **kwargs - ): + ) -> Embed: color = ctx.bot.colors["embed"] em = Embed(title=title, description=message, url=url, color=color) em.set_image(url=url) return await EmbedGenerator.SendWithFooter(ctx, em, **kwargs) @staticmethod - async def Title(ctx: Context, title: str, **kwargs): + async def Title(ctx: Context, title: str, **kwargs) -> Embed: color = ctx.bot.colors["embed"] em = Embed(title=title, color=color) return await EmbedGenerator.SendWithFooter(ctx, em, **kwargs) @staticmethod - async def SendWithFooter(ctx: Context, em: Embed, **kwargs) -> discord.Message: + async def SendWithFooter( + ctx: Context, em: Embed, **kwargs + ) -> Union[discord.Message, Embed]: avatar = ctx.author.avatar.with_static_format("jpeg") em.set_footer(text=f"Requested by: {ctx.author}", icon_url=avatar) if kwargs.get("no_send", False): diff --git a/utils/exceptions.py b/utils/exceptions.py new file mode 100644 index 0000000..d807e17 --- /dev/null +++ b/utils/exceptions.py @@ -0,0 +1,11 @@ +from discord.embeds import Embed +from context import CustomContext +from discord.ext.commands import CommandError + + +class EmbeddedCommandException(CommandError): + def __init__(self, embed: Embed) -> None: + self.embed = embed + + async def send(self, ctx: CustomContext): + await ctx.send(embed=self.embed)