2017-06-07 20:30:19 +12:00
|
|
|
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()
|
2017-06-28 12:12:49 +12:00
|
|
|
except (discord.Forbidden, discord.NotFound):
|
2017-06-07 20:30:19 +12:00
|
|
|
pass
|
|
|
|
|
|
|
|
# For our case here, everything needs to be lowered and stripped, so just do this now
|
2017-06-08 13:05:38 +12:00
|
|
|
return response.content
|
2017-06-07 20:30:19 +12:00
|
|
|
|
|
|
|
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)
|
2017-06-08 12:40:37 +12:00
|
|
|
await self.update_dj_for_member(author)
|
2017-06-07 20:30:19 +12:00
|
|
|
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)
|
2017-06-08 12:40:37 +12:00
|
|
|
await self.update_dj_for_member(author)
|
2017-06-07 20:30:19 +12:00
|
|
|
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)
|
2017-06-08 12:40:37 +12:00
|
|
|
await self.update_dj_for_member(author)
|
2017-06-07 20:30:19 +12:00
|
|
|
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)
|
2017-06-28 12:26:32 +12:00
|
|
|
@utils.check_restricted()
|
2017-06-07 20:30:19 +12:00
|
|
|
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
|
2017-06-28 12:12:49 +12:00
|
|
|
]
|
2017-06-07 20:30:19 +12:00
|
|
|
|
|
|
|
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)
|
2017-06-28 12:26:32 +12:00
|
|
|
@utils.check_restricted()
|
2017-06-07 20:30:19 +12:00
|
|
|
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)
|
2017-06-28 12:26:32 +12:00
|
|
|
@utils.check_restricted()
|
2017-06-07 20:30:19 +12:00
|
|
|
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)
|
2017-06-28 12:26:32 +12:00
|
|
|
@utils.check_restricted()
|
2017-06-07 20:30:19 +12:00
|
|
|
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
|
2017-06-08 13:05:38 +12:00
|
|
|
author = ctx.message.author
|
|
|
|
key = str(author.id)
|
2017-06-07 20:30:19 +12:00
|
|
|
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
|
2017-06-29 07:54:07 +12:00
|
|
|
playlist = playlist.lower().strip()
|
2017-06-07 20:30:19 +12:00
|
|
|
if playlist not in names:
|
|
|
|
await ctx.send("You do not have a playlist named {}!".format(playlist))
|
|
|
|
return
|
|
|
|
|
2017-06-28 12:12:49 +12:00
|
|
|
q1 = "How would you like to edit {}? Choices are `add`, `remove`, " \
|
|
|
|
"`rename`, `delete`, `list`, or `activate`.\n" \
|
2017-06-07 20:30:19 +12:00
|
|
|
"**add** - Adds a song to this playlist\n" \
|
|
|
|
"**remove** - Removes a song from this playlist\n" \
|
|
|
|
"**rename** - Changes the name of this playlist\n" \
|
2017-06-28 12:12:49 +12:00
|
|
|
"**list** - Lists the songs in this playlist\n" \
|
2017-06-07 20:30:19 +12:00
|
|
|
"**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)
|
|
|
|
|
2017-06-08 08:56:45 +12:00
|
|
|
if not response:
|
|
|
|
break
|
|
|
|
|
2017-06-18 11:05:40 +12:00
|
|
|
response = response.lower().strip()
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
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()
|
2017-06-08 13:05:38 +12:00
|
|
|
if await self.add_to_playlist(author, playlist, response):
|
2017-06-08 12:40:37 +12:00
|
|
|
delete_msgs.append(await ctx.send("Successfully added song {} to playlist {}".format(response,
|
2017-06-28 12:12:49 +12:00
|
|
|
playlist)))
|
2017-06-08 12:40:37 +12:00
|
|
|
else:
|
|
|
|
delete_msgs.append(await ctx.send("Failed to lookup {}".format(response)))
|
2017-06-07 20:30:19 +12:00
|
|
|
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:
|
2017-06-08 13:05:38 +12:00
|
|
|
num = int(response.lower().strip()) - 1
|
2017-06-07 20:30:19 +12:00
|
|
|
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)
|
2017-06-08 13:05:38 +12:00
|
|
|
new_name = new_name.lower().strip()
|
2017-06-07 20:30:19 +12:00
|
|
|
playlist = new_name
|
|
|
|
delete_msgs.append(await ctx.send("Successfully renamed {} to {}!".format(playlist, new_name)))
|
2017-06-28 12:12:49 +12:00
|
|
|
elif 'list' in response:
|
|
|
|
await ctx.invoke(self.playlist, playlist_name=playlist)
|
2017-06-07 20:30:19 +12:00
|
|
|
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!"))
|
|
|
|
|
2017-06-10 14:50:17 +12:00
|
|
|
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)
|
2017-06-07 20:30:19 +12:00
|
|
|
|
|
|
|
|
|
|
|
def setup(bot):
|
|
|
|
bot.add_cog(Playlist(bot))
|