Playlist additions and cleanup (#1579)

Add playlist append, create, remove, and upload.
This commit is contained in:
aikaterna 2018-05-03 20:43:00 -07:00 committed by Kowlin
parent d61827b92c
commit 79676c4f72

View File

@ -1,16 +1,18 @@
import aiohttp
import asyncio
import datetime
import discord
import heapq
import lavalink
import math
import re
import redbot.core
from discord.ext import commands
from redbot.core import Config, checks, bank
from .manager import shutdown_lavalink_server
__version__ = "0.0.5"
__version__ = "0.0.5a"
__author__ = ["aikaterna", "billy/bollo/ati"]
@ -46,6 +48,7 @@ class Audio:
self.config.register_guild(**default_guild)
self.config.register_global(**default_global)
self.skip_votes = {}
self.session = aiohttp.ClientSession()
async def init_config(self):
host = await self.config.host()
@ -98,7 +101,8 @@ class Audio:
await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
type=discord.ActivityType.listening))
if playing_servers > 1:
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers),
await self.bot.change_presence(
activity=discord.Activity(name='music in {} servers'.format(playing_servers),
type=discord.ActivityType.playing))
if event_type == lavalink.LavalinkEvents.QUEUE_END and notify:
@ -115,7 +119,8 @@ class Audio:
await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
type=discord.ActivityType.listening))
if playing_servers > 1:
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers),
await self.bot.change_presence(
activity=discord.Activity(name='music in {} servers'.format(playing_servers),
type=discord.ActivityType.playing))
if event_type == lavalink.LavalinkEvents.TRACK_EXCEPTION:
@ -143,7 +148,6 @@ class Audio:
dj_role_id = await self.config.guild(ctx.guild).dj_role()
if dj_role_id is None:
await self._embed_msg(ctx, 'Please set a role to use with DJ mode. Enter the role name now.')
def check(m):
return m.author == ctx.author
try:
@ -172,8 +176,6 @@ class Audio:
@checks.mod_or_permissions(administrator=True)
async def jukebox(self, ctx, price: int):
"""Set a price for queueing songs for non-mods. 0 to disable."""
jukebox = await self.config.guild(ctx.guild).jukebox()
jukebox_price = await self.config.guild(ctx.guild).jukebox_price()
if price < 0:
return await self._embed_msg(ctx, 'Can\'t be less than zero.')
if price == 0:
@ -363,6 +365,7 @@ class Audio:
def check(r, u):
return r.message.id == message.id and u == ctx.message.author
try:
(r, u) = await self.bot.wait_for('reaction_add', check=check, timeout=10.0)
except asyncio.TimeoutError:
@ -492,6 +495,8 @@ class Audio:
if not await self._currency_check(ctx, jukebox_price):
return
if not query:
return await self._embed_msg(ctx, 'No songs to play.')
query = query.strip('<>')
if not query.startswith('http'):
query = 'ytsearch:{}'.format(query)
@ -510,7 +515,8 @@ class Audio:
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Enqueued',
description='Added {} tracks to the queue.'.format(len(tracks)))
if not shuffle and queue_duration > 0:
embed.set_footer(text='{} until start of playlist playback: starts at #{} in queue'.format(queue_total_duration, before_queue_length))
embed.set_footer(text='{} until start of playlist playback: starts at #{} in queue'.format(
queue_total_duration, before_queue_length))
if not player.current:
await player.play()
else:
@ -519,7 +525,8 @@ class Audio:
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Track Enqueued',
description='**[{}]({})**'.format(single_track.title, single_track.uri))
if not shuffle and queue_duration > 0:
embed.set_footer(text='{} until track playback: #{} in queue'.format(queue_total_duration, before_queue_length))
embed.set_footer(text='{} until track playback: #{} in queue'.format(
queue_total_duration, before_queue_length))
if not player.current:
await player.play()
await ctx.send(embed=embed)
@ -531,17 +538,60 @@ class Audio:
if ctx.invoked_subcommand is None:
await ctx.send_help()
@playlist.command(name='append')
async def _playlist_append(self, ctx, playlist_name, *url):
"""Add a song URL, playlist link, or quick search to the end of a saved playlist."""
if not await self._playlist_check(ctx):
return
async with self.config.guild(ctx.guild).playlists() as playlists:
try:
if (playlists[playlist_name]['author'] != ctx.author.id and not
await self._can_instaskip(ctx, ctx.author)):
return await self._embed_msg(ctx, 'You are not the author of that playlist.')
player = lavalink.get_player(ctx.guild.id)
to_append = await self._playlist_tracks(ctx, player, url)
if not to_append:
return
track_list = playlists[playlist_name]['tracks']
if track_list:
playlists[playlist_name]['tracks'] = track_list + to_append
else:
playlists[playlist_name]['tracks'] = to_append
except KeyError:
return await self._embed_msg(ctx, 'No playlist with that name.')
if playlists[playlist_name]['playlist_url'] is not None:
playlists[playlist_name]['playlist_url'] = None
if len(to_append) == 1:
track_title = to_append[0]['info']['title']
return await self._embed_msg(ctx, '{} appended to {}.'.format(track_title, playlist_name))
await self._embed_msg(ctx, '{} tracks appended to {}.'.format(len(to_append), playlist_name))
@playlist.command(name='create')
async def _playlist_create(self, ctx, playlist_name):
"""Create an empty playlist."""
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
if dj_enabled:
if not await self._can_instaskip(ctx, ctx.author):
return await self._embed_msg(ctx, 'You need the DJ role to save playlists.')
async with self.config.guild(ctx.guild).playlists() as playlists:
if playlist_name in playlists:
return await self._embed_msg(ctx, 'Playlist name already exists, try again with a different name.')
playlist_list = self._to_json(ctx, None, None)
playlists[playlist_name] = playlist_list
await self._embed_msg(ctx, 'Empty playlist {} created.'.format(playlist_name))
@playlist.command(name='delete')
async def _playlist_delete(self, ctx, playlist_name):
"""Delete a saved playlist."""
async with self.config.guild(ctx.guild).playlists() as playlists:
try:
if playlists[playlist_name]['author'] != ctx.author.id and not await self._can_instaskip(ctx, ctx.author):
if (playlists[playlist_name]['author'] != ctx.author.id and not
await self._can_instaskip(ctx, ctx.author)):
return await self._embed_msg(ctx, 'You are not the author of that playlist.')
del playlists[playlist_name]
except KeyError:
return await self._embed_msg(ctx, 'No playlist with that name.')
await self._embed_msg(ctx, '{} playlist removed.'.format(playlist_name))
await self._embed_msg(ctx, '{} playlist deleted.'.format(playlist_name))
@playlist.command(name='info')
async def _playlist_info(self, ctx, playlist_name):
@ -556,18 +606,15 @@ class Audio:
try:
track_len = len(playlists[playlist_name]['tracks'])
except TypeError:
track_len = 1
track_len = 0
if playlist_url is None:
playlist_url = '**Not generated from a URL.**'
playlist_url = '**Custom playlist.**'
else:
playlist_url = 'URL: <{}>'.format(playlist_url)
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist info for {}:'.format(playlist_name),
description='Author: **{}**\n{}'.format(author_obj,
playlist_url))
if track_len > 1:
embed.set_footer(text='{} tracks'.format(track_len))
if track_len == 1:
embed.set_footer(text='{} track'.format(track_len))
embed.set_footer(text='{} track(s)'.format(track_len))
await ctx.send(embed=embed)
@playlist.command(name='list')
@ -597,11 +644,11 @@ class Audio:
return await self._embed_msg(ctx, 'Nothing playing.')
player = lavalink.get_player(ctx.guild.id)
tracklist = []
np_song = self._track_creator(ctx, player, 'np', None)
np_song = self._track_creator(player, 'np')
tracklist.append(np_song)
for track in player.queue:
queue_idx = player.queue.index(track)
track_obj = self._track_creator(ctx, player, queue_idx, None)
track_obj = self._track_creator(player, queue_idx)
tracklist.append(track_obj)
if not playlist_name:
await self._embed_msg(ctx, 'Please enter a name for this playlist.')
@ -616,11 +663,38 @@ class Audio:
return await self._embed_msg(ctx, 'Playlist name already exists, try again with a different name.')
except asyncio.TimeoutError:
return await self._embed_msg(ctx, 'No playlist name entered, try again later.')
playlist_list = self._to_json(ctx, None, tracklist, playlist_name)
playlist_list = self._to_json(ctx, None, tracklist)
async with self.config.guild(ctx.guild).playlists() as playlists:
playlists[playlist_name] = playlist_list
await self._embed_msg(ctx, 'Playlist {} saved from current queue: {} tracks added.'.format(playlist_name, len(tracklist)))
await self._embed_msg(ctx, 'Playlist {} saved from current queue: {} tracks added.'.format(
playlist_name, len(tracklist)))
@playlist.command(name='remove')
async def _playlist_remove(self, ctx, playlist_name, url):
"""Remove a song from a playlist by url."""
async with self.config.guild(ctx.guild).playlists() as playlists:
try:
if (playlists[playlist_name]['author'] != ctx.author.id and not
await self._can_instaskip(ctx, ctx.author)):
return await self._embed_msg(ctx, 'You are not the author of that playlist.')
except KeyError:
return await self._embed_msg(ctx, 'No playlist with that name.')
track_list = playlists[playlist_name]['tracks']
clean_list = [track for track in track_list if not url == track['info']['uri']]
if len(playlists[playlist_name]['tracks']) == len(clean_list):
return await self._embed_msg(ctx, 'URL not in playlist.')
del_count = len(playlists[playlist_name]['tracks']) - len(clean_list)
if not clean_list:
del playlists[playlist_name]
return await self._embed_msg(ctx, 'No songs left, removing playlist.')
playlists[playlist_name]['tracks'] = clean_list
if playlists[playlist_name]['playlist_url'] is not None:
playlists[playlist_name]['playlist_url'] = None
if del_count > 1:
await self._embed_msg(ctx, '{} entries have been removed from the {} playlist.'.format(
del_count, playlist_name))
else:
await self._embed_msg(ctx, 'The track has been removed from the {} playlist.'.format(playlist_name))
@playlist.command(name='save')
async def _playlist_save(self, ctx, playlist_name, playlist_url):
@ -628,18 +702,13 @@ class Audio:
if not await self._playlist_check(ctx):
return
player = lavalink.get_player(ctx.guild.id)
tracks = await player.get_tracks(playlist_url)
if not tracks:
return await self._embed_msg(ctx, 'Nothing found.')
tracklist = []
for track in tracks:
track_obj = self._track_creator(ctx, player, None, track)
tracklist.append(track_obj)
playlist_list = self._to_json(ctx, playlist_url, tracklist, playlist_name)
tracklist = await self._playlist_tracks(ctx, player, playlist_url)
playlist_list = self._to_json(ctx, playlist_url, tracklist)
if tracklist is not None:
async with self.config.guild(ctx.guild).playlists() as playlists:
playlists[playlist_name] = playlist_list
return await self._embed_msg(ctx, 'Playlist {} saved: {} tracks added.'.format(playlist_name, len(tracks)))
return await self._embed_msg(ctx, 'Playlist {} saved: {} tracks added.'.format(
playlist_name, len(tracklist)))
@playlist.command(name='start')
async def _playlist_start(self, ctx, playlist_name=None):
@ -647,14 +716,9 @@ class Audio:
if not await self._playlist_check(ctx):
return
playlists = await self.config.guild(ctx.guild).playlists.get_raw()
try:
author_id = playlists[playlist_name]["author"]
except KeyError:
return await self._embed_msg(ctx, 'That playlist doesn\'t exist.')
author_obj = self.bot.get_user(author_id)
author_obj = self.bot.get_user(ctx.author.id)
track_count = 0
try:
playlist_len = len(playlists[playlist_name]["tracks"])
player = lavalink.get_player(ctx.guild.id)
for track in playlists[playlist_name]["tracks"]:
player.add(author_obj, lavalink.rest_api.Track(data=track))
@ -666,6 +730,75 @@ class Audio:
await player.play()
except TypeError:
await ctx.invoke(self.play, query=playlists[playlist_name]["playlist_url"])
except KeyError:
await self._embed_msg(ctx, 'That playlist doesn\'t exist.')
@checks.is_owner()
@playlist.command(name='upload')
async def _playlist_upload(self, ctx):
"""Convert a Red v2 playlist file to a playlist."""
if not await self._playlist_check(ctx):
return
player = lavalink.get_player(ctx.guild.id)
await self._embed_msg(ctx, 'Please upload the playlist file. Any other message will cancel this operation.')
def check(m):
return m.author == ctx.author
try:
file_message = await ctx.bot.wait_for('message', timeout=30.0, check=check)
except asyncio.TimeoutError:
return await self._embed_msg(ctx, 'No file detected, try again later.')
try:
file_url = file_message.attachments[0].url
except IndexError:
return await self._embed_msg(ctx, 'Upload canceled.')
v2_playlist_name = (file_url.split('/')[6]).split('.')[0]
file_suffix = file_url.rsplit('.', 1)[1]
if file_suffix != "txt":
return await self._embed_msg(ctx, 'Only playlist files can be uploaded.')
async with self.session.request('GET', file_url) as r:
v2_playlist = await r.json(content_type='text/plain')
try:
v2_playlist_url = v2_playlist["link"]
except KeyError:
v2_playlist_url = None
if (not v2_playlist_url or not self._match_yt_playlist(v2_playlist_url) or not
await player.get_tracks(v2_playlist_url)):
track_list = []
track_count = 0
async with self.config.guild(ctx.guild).playlists() as v3_playlists:
try:
if v3_playlists[v2_playlist_name]:
return await self._embed_msg(ctx, 'A playlist already exists with this name.')
except KeyError:
pass
embed1 = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Please wait, adding tracks...')
playlist_msg = await ctx.send(embed=embed1)
for song_url in v2_playlist["playlist"]:
track = await player.get_tracks(song_url)
try:
track_obj = self._track_creator(player, other_track=track[0])
track_list.append(track_obj)
track_count = track_count + 1
except IndexError:
pass
if track_count % 5 == 0:
embed2 = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Loading track {}/{}...'.format(
track_count, len(v2_playlist["playlist"])))
await playlist_msg.edit(embed=embed2)
if not track_list:
return await self._embed_msg(ctx, 'No tracks found.')
playlist_list = self._to_json(ctx, v2_playlist_url, track_list)
v3_playlists[v2_playlist_name] = playlist_list
if len(v2_playlist["playlist"]) != track_count:
bad_tracks = len(v2_playlist["playlist"]) - track_count
msg = ('Added {} tracks from the {} playlist. {} track(s) could not '
'be loaded.'.format(track_count, v2_playlist_name, bad_tracks))
else:
msg = 'Added {} tracks from the {} playlist.'.format(track_count, v2_playlist_name)
embed3 = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Saved', description=msg)
await playlist_msg.edit(embed=embed3)
else:
await ctx.invoke(self._playlist_save, v2_playlist_name, v2_playlist_url)
async def _playlist_check(self, ctx):
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
@ -694,6 +827,27 @@ class Audio:
await self._data_check(ctx)
return True
async def _playlist_tracks(self, ctx, player, query):
search = False
if type(query) is tuple:
query = " ".join(query)
if not query.startswith('http'):
query = " ".join(query)
query = 'ytsearch:{}'.format(query)
search = True
tracks = await player.get_tracks(query)
if not tracks:
return await self._embed_msg(ctx, 'Nothing found.')
tracklist = []
if not search:
for track in tracks:
track_obj = self._track_creator(player, other_track=track)
tracklist.append(track_obj)
else:
track_obj = self._track_creator(player, other_track=tracks[0])
tracklist.append(track_obj)
return tracklist
@commands.command()
async def prev(self, ctx):
"""Skips to the start of the previously played track."""
@ -773,8 +927,8 @@ class Audio:
for i, track in enumerate(player.queue[start:end], start=start):
req_user = track.requester
next = i + 1
queue_list += '`{}.` **[{}]({})**, requested by **{}**\n'.format(next, track.title, track.uri, req_user)
_next = i + 1
queue_list += '`{}.` **[{}]({})**, requested by **{}**\n'.format(_next, track.title, track.uri, req_user)
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Queue for ' + ctx.guild.name,
description=queue_list)
@ -872,8 +1026,8 @@ class Audio:
end = start + items_per_page
search_list = ''
for i, track in enumerate(tracks[start:end], start=start):
next = i + 1
search_list += '`{0}.` [**{1}**]({2})\n'.format(next, track.title,
_next = i + 1
search_list += '`{0}.` [**{1}**]({2})\n'.format(_next, track.title,
track.uri)
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Tracks Found:', description=search_list)
@ -906,7 +1060,8 @@ class Audio:
queue_duration = await self._queue_duration(ctx)
queue_total_duration = lavalink.utils.format_time(queue_duration)
if not shuffle and queue_duration > 0:
songembed.set_footer(text='{} until start of search playback: starts at #{} in queue'.format(queue_total_duration, (len(player.queue) + 1)))
songembed.set_footer(text='{} until start of search playback: starts at #{} in queue'.format(
queue_total_duration, (len(player.queue) + 1)))
for track in tracks:
player.add(ctx.author, track)
if not player.current:
@ -926,7 +1081,8 @@ class Audio:
queue_duration = await self._queue_duration(ctx)
queue_total_duration = lavalink.utils.format_time(queue_duration)
if not shuffle and queue_duration > 0:
embed.set_footer(text='{} until track playback: #{} in queue'.format(queue_total_duration, (len(player.queue) + 1)))
embed.set_footer(text='{} until track playback: #{} in queue'.format(queue_total_duration, (
len(player.queue) + 1)))
player.add(ctx.author, search_choice)
if not player.current:
await player.play()
@ -1052,11 +1208,11 @@ class Audio:
nonbots = sum(not m.bot for m in ctx.guild.get_member(self.bot.user.id).voice.channel.members)
if nonbots == 1:
nonbots = 2
else:
if ctx.guild.get_member(member.id).voice.channel.members == 1:
elif ctx.guild.get_member(member.id).voice.channel.members == 1:
nonbots = 1
alone = nonbots <= 1
return alone
else:
nonbots = 0
return nonbots <= 1
async def _has_dj_role(self, ctx, member):
dj_role_id = await self.config.guild(ctx.guild).dj_role()
@ -1165,7 +1321,8 @@ class Audio:
await self.config.password.set('youshallnotpass')
await self.config.rest_port.set(2333)
await self.config.ws_port.set(2332)
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='External lavalink server: {}.'.format(not external))
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='External lavalink server: {}.'.format(
not external))
embed.set_footer(text='Defaults reset.')
return await ctx.send(embed=embed)
else:
@ -1187,7 +1344,8 @@ class Audio:
"""Set the lavalink server password."""
await self.config.password.set(str(password))
if await self._check_external():
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Server password set to {}.'.format(password))
embed = discord.Embed(colour=ctx.guild.me.top_role.colour,
title='Server password set to {}.'.format(password))
embed.set_footer(text='External lavalink server set to True.')
await ctx.send(embed=embed)
else:
@ -1209,7 +1367,8 @@ class Audio:
"""Set the lavalink websocket server port."""
await self.config.rest_port.set(ws_port)
if await self._check_external():
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Websocket port set to {}.'.format(ws_port))
embed = discord.Embed(colour=ctx.guild.me.top_role.colour,
title='Websocket port set to {}.'.format(ws_port))
embed.set_footer(text='External lavalink server set to True.')
await ctx.send(embed=embed)
else:
@ -1255,7 +1414,8 @@ class Audio:
if player.volume != volume:
await player.set_volume(volume)
async def _draw_time(self, ctx):
@staticmethod
async def _draw_time(ctx):
player = lavalink.get_player(ctx.guild.id)
paused = player.paused
pos = player.position
@ -1305,6 +1465,15 @@ class Audio:
else:
return 0
@staticmethod
def _match_yt_playlist(url):
yt_list_playlist = re.compile(
r'^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)'
r'(\/playlist\?).*(list=)(.*)(&|$)')
if yt_list_playlist.match(url):
return True
return False
@staticmethod
async def _queue_duration(ctx):
player = lavalink.get_player(ctx.guild.id)
@ -1333,14 +1502,16 @@ class Audio:
except KeyError:
return False
def _to_json(self, ctx, playlist_url, tracklist, playlist_name):
@staticmethod
def _to_json(ctx, playlist_url, tracklist):
playlist = {"author": ctx.author.id, "playlist_url": playlist_url, "tracks": tracklist}
return playlist
def _track_creator(self, ctx, player, position, other_track=None):
@staticmethod
def _track_creator(player, position=None, other_track=None):
if position == 'np':
queued_track = player.current
elif position == None:
elif position is None:
queued_track = other_track
else:
queued_track = player.queue[position]
@ -1365,6 +1536,7 @@ class Audio:
pass
def __unload(self):
self.session.close()
lavalink.unregister_event_listener(self.event_handler)
self.bot.loop.create_task(lavalink.close())
shutdown_lavalink_server()