import discord import asyncio from discord.ext import commands from . import utils class Playlist: """Used to manage user playlists""" def __init__(self, bot): self.bot = bot async def get_response(self, ctx, question): # Save our simple variables channel = ctx.message.channel author = ctx.message.author # Create our check function used to ensure the author and channel are the only possible message we get check = lambda m: m.author == author and m.channel == channel try: # Ask our question, wait 60 seconds for a response my_msg = await ctx.send(question) response = await self.bot.wait_for('message', check=check, timeout=60) except asyncio.TimeoutError: # If we timeout, let them know and return None await ctx.send("You took too long. I'm impatient, don't make me wait") return None else: # If succesful try to delete the message we sent, and the response try: await my_msg.delete() await response.delete() except (discord.Forbidden, discord.NotFound): pass # For our case here, everything needs to be lowered and stripped, so just do this now return response.content async def get_info(self, song_url): try: # Just download the information info = await self.bot.downloader.extract_info(self.bot.loop, song_url, download=False) except Exception as e: # If we fail, it's possibly due to an incorrect detection as a URL instead of a search if "gaierror" in str(e) or "unknown url type" in str(e): # So just force a search song_url = "ytsearch:" + song_url info = await self.bot.downloader.extract_info(self.bot.loop, song_url, download=False) else: # Otherwise if we fail, we just want to return None return None # If we detected a search, get the first entry in the results if info.get('_type', None) == 'playlist': if info.get('extractor') == 'youtube:search': if len(info['entries']) == 0: return None else: info = info['entries'][0] song_url = info['webpage_url'] # If we are successful, create the entry we'll need to add to the playlist database, and return it if info: return { 'title': info.get('title', 'Untitled'), 'url': song_url } else: return None async def add_to_playlist(self, author, playlist, url): # Simply get the database entry for this user's playlist key = str(author.id) playlist = playlist.lower().strip() playlists = self.bot.db.load('user_playlists', key=key, pluck='playlists') or [] entry = await self.get_info(url) # Search through, find the name that matches the playlist if entry: for pl in playlists: if pl['name'] == playlist: # If we find it, add the song entry to the songs pl['songs'].append(entry) # Create the json needed to save to the database, and save update = { 'member_id': key, 'playlists': playlists } self.bot.db.save('user_playlists', update) await self.update_dj_for_member(author) return True async def rename_playlist(self, author, old_name, new_name): # Simply get the database entry for this user's playlist key = str(author.id) old_name = old_name.lower().strip() new_name = new_name.lower().strip() playlists = self.bot.db.load('user_playlists', key=key, pluck='playlists') or [] # Find the playlist that matches the old name for pl in playlists: if pl['name'] == old_name: # Once found, change the name, update the json, save pl['name'] = new_name update = { 'member_id': key, 'playlists': playlists } self.bot.db.save('user_playlists', update) await self.update_dj_for_member(author) return True async def remove_from_playlist(self, author, playlist, index): # Simply get the database entry for this user's playlist key = str(author.id) playlist = playlist.lower().strip() playlists = self.bot.db.load('user_playlists', key=key, pluck='playlists') or [] # Loop through till we find the playlist that matches for pl in playlists: if pl['name'] == playlist: song = pl['songs'][index] # Once found, remove the matching song, update json, save pl['songs'].remove(song) update = { 'member_id': key, 'playlists': playlists } self.bot.db.save('user_playlists', update) await self.update_dj_for_member(author) return song async def update_dj_for_member(self, member): music = self.bot.get_cog('Music') if music: for state in music.voice_states.values(): dj = state.get_dj(member) if dj: # We want to add a slight delay to this, because our database method launches a task to update # Before we update what is live, we need the information saved in (at least the cache) the database await asyncio.sleep(2) self.bot.loop.create_task(dj.resolve_playlist()) @commands.command() @utils.custom_perms(send_messages=True) @utils.check_restricted() async def playlists(self, ctx): """Displays the playlists you have EXAMPLE: !playlists RESULT: All your playlists""" # Get all the author's playlists playlists = self.bot.db.load('user_playlists', key=ctx.message.author.id, pluck='playlists') if playlists: # Create the entries for our paginator detailing the name of the playlist, and the number of songs in it entries = [ "{} ({} songs)".format(x['name'], len(x['songs'])) if not x.get('active') else "{} ({} songs) - Active playlist".format(x['name'], len(x['songs'])) for x in playlists ] try: # And paginate pages = utils.Pages(self.bot, message=ctx.message, entries=entries) await pages.paginate() except utils.CannotPaginate as e: await ctx.send(str(e)) else: await ctx.send("You do not have any playlists") @commands.group(invoke_without_command=True) @utils.custom_perms(send_messages=True) @utils.check_restricted() async def playlist(self, ctx, *, playlist_name): """Used to view your playlists EXAMPLE: !playlist Playlist 2 RESULT: Displays the songs in your playlist called "Playlist 2" """ playlist_name = playlist_name.lower().strip() playlists = self.bot.db.load('user_playlists', key=ctx.message.author.id, pluck='playlists') try: # Get the playlist if the name matches playlist = [x for x in playlists if playlist_name == x['name']][0] # Create the entries for our paginator just based on the title of the songs in the playlist entries = ["{}".format(x['title']) for x in playlist['songs']] # Paginate pages = utils.Pages(self.bot, message=ctx.message, entries=entries) await pages.paginate() except (IndexError, TypeError, KeyError): await ctx.send("You do not have a playlist named {}!".format(playlist_name)) except utils.CannotPaginate as e: await ctx.send(str(e)) @playlist.command(name='create') @utils.custom_perms(send_messages=True) @utils.check_restricted() async def _pl_create(self, ctx, *, name): """Used to create a new playlist EXAMPLE: !playlist create Playlist RESULT: A new playlist called Playlist""" key = str(ctx.message.author.id) playlists = self.bot.db.load('user_playlists', key=key, pluck='playlists') or [] # Create the new playlist entry entry = { 'name': name.lower().strip(), 'songs': [] } # Check to make sure that there isn't a playlist with the same name names = [x['name'] for x in playlists] if name in names: await ctx.send('You already have a playlist called {}'.format(name)) # Otherwise add this new playlist, and save else: # This is here to set the first playlist we create as the active one. # If someone has a playlist already, we don't want to change which is the active one # If they don't have any, then we want to set our first one as the active one entry['active'] = len(playlists) == 0 playlists.append(entry) update = { 'member_id': key, 'playlists': playlists } self.bot.db.save('user_playlists', update) await ctx.send("You have just created a new playlist called {}".format(name)) @playlist.command(name='edit') @utils.custom_perms(send_messages=True) @utils.check_restricted() async def _pl_edit(self, ctx): """A command used to edit a current playlist The available ways to edit a playlist are to rename, add a song, remove a song, or delete the playlist EXAMPLE: !playlist edit RESULT: A followalong asking for what you need""" # Load the playlists for the author author = ctx.message.author key = str(author.id) playlists = self.bot.db.load('user_playlists', key=key, pluck='playlists') or [] # Also create a list of the names for easy comparision names = [x['name'] for x in playlists] if not playlists: await ctx.send("You have no playlists to edit!") return # Show the playlists we have, and ask which to choose from await ctx.invoke(self.playlists) question = "Please provide what playlist you would like to edit, the playlists you have available are above." playlist = await self.get_response(ctx, question) if not playlist: return playlist = playlist.lower().strip() if playlist not in names: await ctx.send("You do not have a playlist named {}!".format(playlist)) return q1 = "How would you like to edit {}? Choices are `add`, `remove`, " \ "`rename`, `delete`, `list`, or `activate`.\n" \ "**add** - Adds a song to this playlist\n" \ "**remove** - Removes a song from this playlist\n" \ "**rename** - Changes the name of this playlist\n" \ "**list** - Lists the songs in this playlist\n" \ "**delete** - Deletes this playlist\n" \ "**activate** - Sets this as the active playlist\n\n" \ "Type **quit** to stop editing this playlist".format(playlist) # Lets create a list of the messages we'll delete after delete_msgs = [] # We want to loop this in order to continue editing, till the user is done while True: response = await self.get_response(ctx, q1) if not response: break response = response.lower().strip() if 'add' in response: # Ask the user what song to add, get the response, add it question = "What is the song you would like to add to {}?".format(playlist) response = await self.get_response(ctx, question) # If we didn't get a response, just continue with the loop, we have no need to say anything # The "error" message is sent with our `get_response` helper method if response: await ctx.message.channel.trigger_typing() if await self.add_to_playlist(author, playlist, response): delete_msgs.append(await ctx.send("Successfully added song {} to playlist {}".format(response, playlist))) else: delete_msgs.append(await ctx.send("Failed to lookup {}".format(response))) elif 'remove' in response: await ctx.invoke(self.playlist, playlist_name=playlist) question = "Please provide just the number of the song you want to delete" try: response = await self.get_response(ctx, question) if response: num = int(response.lower().strip()) - 1 song = await self.remove_from_playlist(ctx.author, playlist, num) await ctx.send("Successfully removed {} from {}".format(song['title'], playlist)) except (ValueError, IndexError): delete_msgs.append(await ctx.send("Please provide just the number of the song you want to delete " "next time!")) elif 'delete' in response: playlists = [x for x in playlists if x['name'] != playlist] entry = { 'member_id': str(key), 'playlists': playlists } self.bot.db.save('user_playlists', entry) delete_msgs.append(await ctx.send("Successfully deleted playlist {}".format(playlist))) await ctx.send("Finished editing {}".format(playlist)) break elif 'rename' in response: question = "What would you like to rename the playlist {} to?".format(playlist) new_name = await self.get_response(ctx, question) if new_name: await self.rename_playlist(ctx.message.author, playlist, new_name) new_name = new_name.lower().strip() playlist = new_name delete_msgs.append(await ctx.send("Successfully renamed {} to {}!".format(playlist, new_name))) elif 'list' in response: await ctx.invoke(self.playlist, playlist_name=playlist) elif 'activate' in response: for x in playlists: x['active'] = x['name'] == playlist entry = { 'member_id': str(key), 'playlists': playlists } self.bot.db.save('user_playlists', entry) # Now we have edited the user's actual playlist...but we need to delete_msgs.append(await ctx.send("{} is now your active playlist".format(playlist))) elif 'quit' in response: await ctx.send("Finished editing {}".format(playlist)) break else: delete_msgs.append(await ctx.send("That is not a valid option!")) if not isinstance(ctx.message.channel, discord.DMChannel): if len(delete_msgs) == 1: await delete_msgs[0].delete() elif len(delete_msgs) > 1: await ctx.message.channel.delete_messages(delete_msgs) def setup(bot): bot.add_cog(Playlist(bot))