From f95d5c1fd23e9545418d90f53249e798df94ec21 Mon Sep 17 00:00:00 2001 From: Joshua Arulsamy Date: Thu, 13 Aug 2020 02:25:15 -0600 Subject: [PATCH] :sparkles: Add album playback features Queue a whole album with one single command. --- PlexBot/bot.py | 166 ++++++++++++++++++++++++++++++++++-------- PlexBot/exceptions.py | 6 +- 2 files changed, 141 insertions(+), 31 deletions(-) diff --git a/PlexBot/bot.py b/PlexBot/bot.py index 19b29b5..2a50782 100644 --- a/PlexBot/bot.py +++ b/PlexBot/bot.py @@ -12,7 +12,8 @@ from discord.ext.commands import command from plexapi.exceptions import Unauthorized from plexapi.server import PlexServer -from .exceptions import TrackNotFoundError +from .exceptions import MediaNotFoundError +from .exceptions import VoiceChannelError root_log = logging.getLogger() plex_log = logging.getLogger("Plex") @@ -159,16 +160,34 @@ class Plex(commands.Cog): title: str title of song to search for Returns: - plexapi.audio.Track pointing to closest matching title + plexapi.audio.Track pointing to best matching title Raises: - TrackNotFoundError: Title of track can't be found in plex db. + MediaNotFoundError: Title of track can't be found in plex db """ results = self.music.searchTracks(title=title, maxresults=1) try: return results[0] except IndexError: - raise TrackNotFoundError + raise MediaNotFoundError("Track cannot be found") + + def _search_albums(self, title: str): + """Search the Plex music db for album + + Args: + title: str title of album to search for + + Returns: + plexapi.audio.Album pointing to best matching title + + Raises: + MediaNotFoundError: Title of album can't be found in plex db + """ + results = self.music.searchAlbums(title=title, maxresults=1) + try: + return results[0] + except IndexError: + raise MediaNotFoundError("Album cannot be found") async def _play(self): """Heavy lifting of playing songs @@ -195,7 +214,7 @@ class Plex(commands.Cog): plex_log.debug("%s - URL: %s", self.current_track, track_url) - embed, img = self._build_embed(self.current_track) + embed, img = self._build_embed_track(self.current_track) self.np_message_id = await self.ctx.send(embed=embed, file=img) async def _audio_player_task(self): @@ -250,11 +269,11 @@ class Plex(commands.Cog): self.current_track = None self.bot.loop.call_soon_threadsafe(self.play_next_event.set) - def _build_embed(self, track, type_="play"): - """Creates a pretty embed card. + def _build_embed_track(self, track, type_="play"): + """Creates a pretty embed card for tracks Builds a helpful status embed with the following info: - Status, song title, album, artistm and album art. All + Status, song title, album, artist and album art. All pertitent information is grabbed dynamically from the Plex db. Args: @@ -262,7 +281,7 @@ class Plex(commands.Cog): type_: Type of card to make (play, queue). Returns: - embed: discord.embed fully constructed img payload. + embed: discord.embed fully constructed payload. thumb_art: io.BytesIO of album thumbnail img. Raises: @@ -293,13 +312,71 @@ class Plex(commands.Cog): # Point to file attached with ctx object. embed.set_thumbnail(url="attachment://image0.png") - bot_log.debug("Built embed for %s", track.title) + bot_log.debug("Built embed for track - %s", track.title) return embed, art_file + def _build_embed_album(self, album): + """Creates a pretty embed card for albums + + Builds a helpful status embed with the following info: + album, artist, and album art. All pertitent information + is grabbed dynamically from the Plex db. + + Args: + album: plexapi.audio.Album object of album + + Returns: + embed: discord.embed fully constructed payload. + thumb_art: io.BytesIO of album thumbnail img. + + Raises: + None + """ + # Grab the relevant thumbnail + img_stream = urlopen(album.thumbUrl) + img = io.BytesIO(img_stream.read()) + + # Attach to discord embed + art_file = discord.File(img, filename="image0.png") + title = "Added album to queue" + descrip = f"{album.title} - {album.artist().title}" + + embed = discord.Embed( + title=title, description=descrip, colour=discord.Color.red() + ) + embed.set_author(name="Plex") + embed.set_thumbnail(url="attachment://image0.png") + bot_log.debug("Built embed for album - %s", album.title) + + return embed, art_file + + async def _validate(self, ctx): + """Ensures user is in a vc + + Args: + ctx: discord.ext.commands.Context message context from command + + Returns: + None + + Raises: + VoiceChannelError: Author not in voice channel + """ + # Fail if user not in vc + if not ctx.author.voice: + await ctx.send("Join a voice channel first!") + bot_log.debug("Failed to play, requester not in voice channel") + raise VoiceChannelError + + # Connect to voice if not already + if not self.voice_channel: + self.voice_channel = await ctx.author.voice.channel.connect() + bot_log.debug("Connected to vc.") + @command() async def play(self, ctx, *args): - """User command to start playback + """User command to play song Searchs plex db and either, initiates playback, or adds to queue. Handles invalid usage from the user. @@ -316,40 +393,69 @@ class Plex(commands.Cog): """ # Save the context to use with async callbacks self.ctx = ctx - - if not args: - await ctx.send(f"Usage: {self.bot_prefix}play TITLE_OF_SONG") - bot_log.debug("Failed to play, invalid usage") - return - title = " ".join(args) + try: track = self._search_tracks(title) - except TrackNotFoundError: + except MediaNotFoundError: await ctx.send(f"Can't find song: {title}") bot_log.debug("Failed to play, can't find song - %s", title) return - # Fail if user not in vc - if not ctx.author.voice: - await ctx.send("Join a voice channel first!") - bot_log.debug("Failed to play, requester not in voice channel") - return - - # Connect to voice if not already - if not self.voice_channel: - self.voice_channel = await ctx.author.voice.channel.connect() - bot_log.debug("Connected to vc.") + try: + await self._validate(ctx) + except VoiceChannelError: + pass # Specific add to queue message if self.voice_channel.is_playing(): bot_log.debug("Added to queue - %s", title) - embed, img = self._build_embed(track, type_="queue") + embed, img = self._build_embed_track(track, type_="queue") await ctx.send(embed=embed, file=img) # Add the song to the async queue await self.play_queue.put(track) + @command() + async def album(self, ctx, *args): + """User command to play song + + Searchs plex db and either, initiates playback, or + adds to queue. Handles invalid usage from the user. + + Args: + ctx: discord.ext.commands.Context message context from command + *args: Title of song to play + + Returns: + None + + Raises: + None + """ + # Save the context to use with async callbacks + self.ctx = ctx + title = " ".join(args) + + try: + album = self._search_albums(title) + except MediaNotFoundError: + await ctx.send(f"Can't find album: {title}") + bot_log.debug("Failed to queue album, can't find - %s", title) + return + + try: + await self._validate(ctx) + except VoiceChannelError: + pass + + bot_log.debug("Added to queue - %s", title) + embed, img = self._build_embed_album(album) + await ctx.send(embed=embed, file=img) + + for track in album.tracks(): + await self.play_queue.put(track) + @command() async def stop(self, ctx): """User command to stop playback @@ -451,7 +557,7 @@ class Plex(commands.Cog): None """ if self.current_track: - embed, img = self._build_embed(self.current_track) + embed, img = self._build_embed_track(self.current_track) bot_log.debug("Now playing") if self.np_message_id: await self.np_message_id.delete() diff --git a/PlexBot/exceptions.py b/PlexBot/exceptions.py index 782496a..193aac7 100644 --- a/PlexBot/exceptions.py +++ b/PlexBot/exceptions.py @@ -1,2 +1,6 @@ -class TrackNotFoundError(Exception): +class MediaNotFoundError(Exception): + pass + + +class VoiceChannelError(Exception): pass