Add album playback features

Queue a whole album with one single command.
This commit is contained in:
Joshua Arulsamy 2020-08-13 02:25:15 -06:00
parent 56fd4aa5ab
commit f95d5c1fd2
2 changed files with 141 additions and 31 deletions

View File

@ -12,7 +12,8 @@ from discord.ext.commands import command
from plexapi.exceptions import Unauthorized from plexapi.exceptions import Unauthorized
from plexapi.server import PlexServer from plexapi.server import PlexServer
from .exceptions import TrackNotFoundError from .exceptions import MediaNotFoundError
from .exceptions import VoiceChannelError
root_log = logging.getLogger() root_log = logging.getLogger()
plex_log = logging.getLogger("Plex") plex_log = logging.getLogger("Plex")
@ -159,16 +160,34 @@ class Plex(commands.Cog):
title: str title of song to search for title: str title of song to search for
Returns: Returns:
plexapi.audio.Track pointing to closest matching title plexapi.audio.Track pointing to best matching title
Raises: 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) results = self.music.searchTracks(title=title, maxresults=1)
try: try:
return results[0] return results[0]
except IndexError: 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): async def _play(self):
"""Heavy lifting of playing songs """Heavy lifting of playing songs
@ -195,7 +214,7 @@ class Plex(commands.Cog):
plex_log.debug("%s - URL: %s", self.current_track, track_url) 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) self.np_message_id = await self.ctx.send(embed=embed, file=img)
async def _audio_player_task(self): async def _audio_player_task(self):
@ -250,11 +269,11 @@ class Plex(commands.Cog):
self.current_track = None self.current_track = None
self.bot.loop.call_soon_threadsafe(self.play_next_event.set) self.bot.loop.call_soon_threadsafe(self.play_next_event.set)
def _build_embed(self, track, type_="play"): def _build_embed_track(self, track, type_="play"):
"""Creates a pretty embed card. """Creates a pretty embed card for tracks
Builds a helpful status embed with the following info: 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. pertitent information is grabbed dynamically from the Plex db.
Args: Args:
@ -262,7 +281,7 @@ class Plex(commands.Cog):
type_: Type of card to make (play, queue). type_: Type of card to make (play, queue).
Returns: Returns:
embed: discord.embed fully constructed img payload. embed: discord.embed fully constructed payload.
thumb_art: io.BytesIO of album thumbnail img. thumb_art: io.BytesIO of album thumbnail img.
Raises: Raises:
@ -293,13 +312,71 @@ class Plex(commands.Cog):
# Point to file attached with ctx object. # Point to file attached with ctx object.
embed.set_thumbnail(url="attachment://image0.png") 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 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() @command()
async def play(self, ctx, *args): async def play(self, ctx, *args):
"""User command to start playback """User command to play song
Searchs plex db and either, initiates playback, or Searchs plex db and either, initiates playback, or
adds to queue. Handles invalid usage from the user. 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 # Save the context to use with async callbacks
self.ctx = ctx 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) title = " ".join(args)
try: try:
track = self._search_tracks(title) track = self._search_tracks(title)
except TrackNotFoundError: except MediaNotFoundError:
await ctx.send(f"Can't find song: {title}") await ctx.send(f"Can't find song: {title}")
bot_log.debug("Failed to play, can't find song - %s", title) bot_log.debug("Failed to play, can't find song - %s", title)
return return
# Fail if user not in vc try:
if not ctx.author.voice: await self._validate(ctx)
await ctx.send("Join a voice channel first!") except VoiceChannelError:
bot_log.debug("Failed to play, requester not in voice channel") pass
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.")
# Specific add to queue message # Specific add to queue message
if self.voice_channel.is_playing(): if self.voice_channel.is_playing():
bot_log.debug("Added to queue - %s", title) 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) await ctx.send(embed=embed, file=img)
# Add the song to the async queue # Add the song to the async queue
await self.play_queue.put(track) 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() @command()
async def stop(self, ctx): async def stop(self, ctx):
"""User command to stop playback """User command to stop playback
@ -451,7 +557,7 @@ class Plex(commands.Cog):
None None
""" """
if self.current_track: 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") bot_log.debug("Now playing")
if self.np_message_id: if self.np_message_id:
await self.np_message_id.delete() await self.np_message_id.delete()

View File

@ -1,2 +1,6 @@
class TrackNotFoundError(Exception): class MediaNotFoundError(Exception):
pass
class VoiceChannelError(Exception):
pass pass