2017-02-13 15:25:56 +13:00
|
|
|
|
from .voice_utilities import *
|
2017-07-12 05:56:55 +12:00
|
|
|
|
from discord import PCMVolumeTransformer
|
2016-10-30 09:23:12 +13:00
|
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
|
import discord
|
|
|
|
|
from discord.ext import commands
|
2016-10-30 09:23:12 +13:00
|
|
|
|
|
2017-02-13 12:45:19 +13:00
|
|
|
|
from . import utils
|
|
|
|
|
|
2016-08-20 16:00:23 +12:00
|
|
|
|
import math
|
2016-10-30 09:23:12 +13:00
|
|
|
|
import asyncio
|
2017-04-20 15:18:05 +12:00
|
|
|
|
import time
|
2017-04-22 07:45:20 +12:00
|
|
|
|
import re
|
2017-05-09 09:54:43 +12:00
|
|
|
|
import logging
|
2017-06-28 12:12:01 +12:00
|
|
|
|
import random
|
2017-06-07 20:30:19 +12:00
|
|
|
|
from collections import deque
|
2017-05-09 09:54:43 +12:00
|
|
|
|
|
|
|
|
|
log = logging.getLogger()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
|
|
|
|
if not discord.opus.is_loaded():
|
|
|
|
|
discord.opus.load_opus('/usr/lib64/libopus.so.0')
|
2016-07-31 12:20:55 +12:00
|
|
|
|
|
2016-12-22 19:56:25 +13:00
|
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
|
class VoiceState:
|
2017-07-12 05:56:55 +12:00
|
|
|
|
def __init__(self, guild, bot, user_queue=False, volume=None):
|
2017-04-20 15:18:05 +12:00
|
|
|
|
self.guild = guild
|
2016-12-22 12:06:32 +13:00
|
|
|
|
self.songs = Playlist(bot)
|
2017-06-07 20:30:19 +12:00
|
|
|
|
self.djs = deque()
|
|
|
|
|
self.dj = None
|
2017-04-20 15:18:05 +12:00
|
|
|
|
self.current = None
|
2016-08-20 16:00:23 +12:00
|
|
|
|
self.required_skips = 0
|
2016-08-17 03:22:32 +12:00
|
|
|
|
self.skip_votes = set()
|
2017-06-07 20:30:19 +12:00
|
|
|
|
self.user_queue = user_queue
|
|
|
|
|
self.loop = bot.loop
|
2017-07-12 05:56:55 +12:00
|
|
|
|
self._volume = volume or .5
|
2017-07-31 14:48:39 +12:00
|
|
|
|
self.live = False
|
2017-05-05 10:33:26 +12:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def volume(self):
|
|
|
|
|
return self._volume
|
|
|
|
|
|
|
|
|
|
@volume.setter
|
|
|
|
|
def volume(self, v):
|
|
|
|
|
self._volume = v
|
|
|
|
|
if self.voice and self.voice.source:
|
|
|
|
|
self.voice.source.volume = v
|
2016-08-21 17:36:38 +12:00
|
|
|
|
|
2017-04-20 15:18:05 +12:00
|
|
|
|
@property
|
|
|
|
|
def voice(self):
|
|
|
|
|
return self.guild.voice_client
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
|
|
|
|
@property
|
2017-04-20 15:18:05 +12:00
|
|
|
|
def playing(self):
|
2017-04-24 10:28:13 +12:00
|
|
|
|
if self.voice is None:
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
return self.voice.is_playing() or self.voice.is_paused()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
def switch_queue_type(self):
|
|
|
|
|
self.songs.clear()
|
|
|
|
|
self.djs.clear()
|
|
|
|
|
self.dj = None
|
|
|
|
|
self.user_queue = not self.user_queue
|
|
|
|
|
self.skip()
|
|
|
|
|
|
|
|
|
|
def get_dj(self, member):
|
|
|
|
|
for x in self.djs:
|
|
|
|
|
if x.member.id == member.id:
|
|
|
|
|
return x
|
|
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
|
def skip(self):
|
|
|
|
|
self.skip_votes.clear()
|
2017-07-28 06:56:03 +12:00
|
|
|
|
if self.voice:
|
|
|
|
|
self.voice.stop()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
def after(self, _=None):
|
|
|
|
|
if self.user_queue:
|
|
|
|
|
self.djs.append(self.dj)
|
|
|
|
|
fut = asyncio.run_coroutine_threadsafe(self.play_next_song(), self.loop)
|
|
|
|
|
fut.result()
|
2017-05-09 09:54:43 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
async def play_next_song(self):
|
2017-06-08 08:56:51 +12:00
|
|
|
|
if self.playing or not self.voice:
|
2017-06-08 06:53:40 +12:00
|
|
|
|
return
|
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
self.skip_votes.clear()
|
|
|
|
|
try:
|
|
|
|
|
await self.next_song()
|
|
|
|
|
except ExtractionError:
|
|
|
|
|
# For now lets just silently continue in the queue
|
|
|
|
|
# Implementation to the music notifications channel will change what we do here
|
|
|
|
|
return await self.play_next_song()
|
2017-06-08 08:56:51 +12:00
|
|
|
|
|
|
|
|
|
if self.playing or not self.voice:
|
|
|
|
|
return
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if self.current:
|
2017-07-03 15:19:20 +12:00
|
|
|
|
# Transform our source into a volume source
|
|
|
|
|
source = PCMVolumeTransformer(self.current, volume=self.volume)
|
2017-04-20 15:18:05 +12:00
|
|
|
|
self.voice.play(source, after=self.after)
|
2016-10-30 09:23:12 +13:00
|
|
|
|
self.current.start_time = time.time()
|
2017-06-28 12:12:01 +12:00
|
|
|
|
# We handle users who join a user queue without songs (either at all, or ready) elsewhere
|
|
|
|
|
# So if self.current is None here, there are a few reasons:
|
|
|
|
|
# User queue, last song failed to download
|
|
|
|
|
# Either queue, all songs/dj's have gone been gone through
|
|
|
|
|
# The first one sucks, but there's not much we can do about it here, blame youtube
|
|
|
|
|
# Second one just means we're done and don't want to do anything
|
|
|
|
|
# So in either case....we simply do nothing here, and just the playing end
|
2017-06-07 20:30:19 +12:00
|
|
|
|
|
|
|
|
|
async def next_song(self):
|
|
|
|
|
if not self.user_queue:
|
2017-07-03 15:19:20 +12:00
|
|
|
|
self.current = await self.songs.next_entry()
|
2017-06-07 20:30:19 +12:00
|
|
|
|
else:
|
|
|
|
|
try:
|
2017-06-07 20:53:15 +12:00
|
|
|
|
dj = self.djs.popleft()
|
2017-06-07 20:30:19 +12:00
|
|
|
|
except IndexError:
|
2017-06-11 14:58:20 +12:00
|
|
|
|
self.dj = None
|
2017-06-12 07:30:09 +12:00
|
|
|
|
self.current = None
|
2017-06-07 20:30:19 +12:00
|
|
|
|
else:
|
2017-07-03 15:19:20 +12:00
|
|
|
|
song = await dj.next_entry()
|
2017-06-12 07:28:58 +12:00
|
|
|
|
# Add an extra check here in case in the very short period of time possible, someone has queued a
|
|
|
|
|
# song while we are downloading the next...which caused 2 play calls to be done
|
|
|
|
|
# The 2nd may be called while the first has already started playing...this check is for that 2nd one
|
|
|
|
|
# If this rare case does happen, we want to insert this dj back into the deque at the front
|
|
|
|
|
# Also rotate their songs back, since it shouldn't have been retrieved
|
|
|
|
|
if self.playing:
|
|
|
|
|
self.djs.insert(0, dj)
|
|
|
|
|
dj.entries.rotate()
|
|
|
|
|
return
|
|
|
|
|
|
2017-06-07 20:53:15 +12:00
|
|
|
|
if song is None:
|
2017-06-11 14:58:20 +12:00
|
|
|
|
return await self.next_song()
|
2017-06-07 20:30:19 +12:00
|
|
|
|
else:
|
2017-06-08 08:56:51 +12:00
|
|
|
|
song.requester = dj.member
|
2017-06-07 20:53:15 +12:00
|
|
|
|
self.dj = dj
|
2017-06-12 07:30:09 +12:00
|
|
|
|
self.current = song
|
2016-10-30 09:23:12 +13:00
|
|
|
|
|
2016-07-09 13:27:19 +12:00
|
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
|
class Music:
|
|
|
|
|
"""Voice related commands.
|
|
|
|
|
Works in multiple servers at once.
|
|
|
|
|
"""
|
2016-07-31 12:20:55 +12:00
|
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
|
def __init__(self, bot):
|
|
|
|
|
self.bot = bot
|
|
|
|
|
self.voice_states = {}
|
2016-12-22 12:06:32 +13:00
|
|
|
|
down = Downloader(download_folder='audio_tmp')
|
|
|
|
|
self.downloader = down
|
|
|
|
|
self.bot.downloader = down
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
2017-07-03 15:19:20 +12:00
|
|
|
|
def __unload(self):
|
|
|
|
|
for state in self.voice_states.values():
|
|
|
|
|
try:
|
|
|
|
|
if state.voice:
|
|
|
|
|
state.voice.stop()
|
|
|
|
|
self.bot.loop.create_task(state.voice.disconnect())
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
2017-02-07 15:56:04 +13:00
|
|
|
|
async def queue_embed_task(self, state, channel, author):
|
|
|
|
|
index = 0
|
|
|
|
|
message = None
|
|
|
|
|
fmt = None
|
2017-05-10 11:21:12 +12:00
|
|
|
|
possible_reactions = ['\u27A1', '\u2B05', '\u2b06', '\u2b07', '\u274c', '\u23ea', '\u23e9']
|
2017-04-20 15:18:05 +12:00
|
|
|
|
|
|
|
|
|
# Our check to ensure the only one who reacts is the bot
|
|
|
|
|
def check(react, u):
|
|
|
|
|
if message is None:
|
|
|
|
|
return False
|
|
|
|
|
elif react.message.id != message.id:
|
|
|
|
|
return False
|
|
|
|
|
elif react.emoji not in possible_reactions:
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
return u.id == author.id
|
|
|
|
|
|
2017-02-07 15:56:04 +13:00
|
|
|
|
while True:
|
|
|
|
|
# Get the current queue (It might change while we're doing this)
|
|
|
|
|
# So do this in the while loop
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if state.user_queue:
|
|
|
|
|
queue = state.djs
|
|
|
|
|
else:
|
|
|
|
|
queue = state.songs.entries
|
2017-02-07 15:56:04 +13:00
|
|
|
|
count = len(queue)
|
|
|
|
|
# This means the last song was removed
|
|
|
|
|
if count == 0:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
await channel.send("Nothing currently in the queue")
|
2017-02-07 15:56:04 +13:00
|
|
|
|
break
|
|
|
|
|
# Get the current entry
|
|
|
|
|
entry = queue[index]
|
2017-06-07 20:30:19 +12:00
|
|
|
|
dj = None
|
|
|
|
|
if state.user_queue:
|
|
|
|
|
dj = entry
|
|
|
|
|
entry = entry.peek()
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# Get the entry's embed
|
2017-07-03 15:19:20 +12:00
|
|
|
|
embed = entry.embed
|
2017-06-07 20:30:19 +12:00
|
|
|
|
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# Set the embed's title to indicate the amount of things in the queue
|
|
|
|
|
count = len(queue)
|
2017-04-20 15:18:05 +12:00
|
|
|
|
embed.title = "Current Queue [{}/{}]".format(index + 1, count)
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# Now we need to send the embed, so check if the message is already set
|
|
|
|
|
# If not, then we need to send a new one (i.e. this is the first time called)
|
|
|
|
|
if message:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
await message.edit(content=fmt, embed=embed)
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# There's only one reaction we want to make sure we remove in the circumstances
|
|
|
|
|
# If the member doesn't have kick_members permissions, and isn't the requester
|
|
|
|
|
# Then they can't remove the song, otherwise they can
|
2017-06-28 12:12:01 +12:00
|
|
|
|
if not author.guild_permissions.mute_members and author.id != entry.requester.id:
|
2017-02-07 15:56:04 +13:00
|
|
|
|
try:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
await message.remove_reaction('\u274c', channel.server.me)
|
2017-02-07 15:56:04 +13:00
|
|
|
|
except:
|
|
|
|
|
pass
|
2017-06-28 12:12:01 +12:00
|
|
|
|
elif not author.guild_permissions.mute_members and author.id == entry.requester.id:
|
2017-02-07 15:56:04 +13:00
|
|
|
|
try:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
await message.add_reaction('\u274c')
|
2017-02-07 15:56:04 +13:00
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
message = await channel.send(embed=embed)
|
2017-05-10 11:21:12 +12:00
|
|
|
|
await message.add_reaction('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}')
|
2017-04-20 15:18:05 +12:00
|
|
|
|
await message.add_reaction('\N{LEFTWARDS BLACK ARROW}')
|
|
|
|
|
await message.add_reaction('\N{BLACK RIGHTWARDS ARROW}')
|
2017-05-10 11:21:12 +12:00
|
|
|
|
await message.add_reaction('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}')
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# The moderation tools that can be used
|
2017-06-28 12:12:01 +12:00
|
|
|
|
if author.guild_permissions.mute_members:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
await message.add_reaction('\N{DOWNWARDS BLACK ARROW}')
|
|
|
|
|
await message.add_reaction('\N{UPWARDS BLACK ARROW}')
|
|
|
|
|
await message.add_reaction('\N{CROSS MARK}')
|
2017-02-07 15:56:04 +13:00
|
|
|
|
elif author == entry.requester:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
await message.add_reaction('\N{CROSS MARK}')
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# Reset the fmt message
|
2017-04-20 15:18:05 +12:00
|
|
|
|
fmt = "\u200B"
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# Now we wait for the next reaction
|
2017-04-20 15:18:05 +12:00
|
|
|
|
try:
|
|
|
|
|
reaction, user = await self.bot.wait_for('reaction_add', check=check, timeout=180)
|
|
|
|
|
except asyncio.TimeoutError:
|
2017-02-07 15:56:04 +13:00
|
|
|
|
break
|
|
|
|
|
# Now we can prepare for the next embed to be sent
|
|
|
|
|
# If right is clicked
|
|
|
|
|
if '\u27A1' in reaction.emoji:
|
|
|
|
|
index += 1
|
|
|
|
|
if index >= count:
|
|
|
|
|
index = 0
|
|
|
|
|
# If left is clicked
|
|
|
|
|
elif '\u2B05' in reaction.emoji:
|
|
|
|
|
index -= 1
|
|
|
|
|
if index < 0:
|
|
|
|
|
index = count - 1
|
|
|
|
|
# If up is clicked
|
|
|
|
|
elif '\u2b06' in reaction.emoji:
|
|
|
|
|
# A second check just to make sure, as well as ensuring index is higher than 0
|
2017-06-28 12:12:01 +12:00
|
|
|
|
if author.guild_permissions.mute_members and index > 0:
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if dj and dj != queue[index]:
|
|
|
|
|
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
|
|
|
|
|
elif not dj and entry != queue[index]:
|
2017-02-07 15:56:04 +13:00
|
|
|
|
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
|
|
|
|
|
else:
|
|
|
|
|
# Remove the current entry
|
|
|
|
|
del queue[index]
|
|
|
|
|
# Add it one position higher
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if state.user_queue:
|
|
|
|
|
queue.insert(index - 1, dj)
|
|
|
|
|
else:
|
|
|
|
|
queue.insert(index - 1, entry)
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# Lets move the index to look at the new place of the entry
|
|
|
|
|
index -= 1
|
|
|
|
|
# If down is clicked
|
|
|
|
|
elif '\u2b07' in reaction.emoji:
|
|
|
|
|
# A second check just to make sure, as well as ensuring index is lower than last
|
2017-06-28 12:12:01 +12:00
|
|
|
|
if author.guild_permissions.mute_members and index < (count - 1):
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if dj and dj != queue[index]:
|
|
|
|
|
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
|
|
|
|
|
elif not dj and entry != queue[index]:
|
2017-02-07 15:56:04 +13:00
|
|
|
|
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
|
|
|
|
|
else:
|
|
|
|
|
# Remove the current entry
|
|
|
|
|
del queue[index]
|
|
|
|
|
# Add it one position lower
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if state.user_queue:
|
|
|
|
|
queue.insert(index + 1, dj)
|
|
|
|
|
else:
|
|
|
|
|
queue.insert(index + 1, entry)
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# Lets move the index to look at the new place of the entry
|
|
|
|
|
index += 1
|
|
|
|
|
# If x is clicked
|
|
|
|
|
elif '\u274c' in reaction.emoji:
|
|
|
|
|
# A second check just to make sure
|
2017-06-28 12:12:01 +12:00
|
|
|
|
if author.guild_permissions.mute_members or author == entry.requester:
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if dj and dj != queue[index]:
|
|
|
|
|
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
|
|
|
|
|
elif not dj and entry != queue[index]:
|
2017-02-07 15:56:04 +13:00
|
|
|
|
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
|
|
|
|
|
else:
|
|
|
|
|
# Simply remove the entry in place
|
|
|
|
|
del queue[index]
|
|
|
|
|
# This is the only check we need to make, to ensure index is now not more than last
|
|
|
|
|
new_count = count - 1
|
|
|
|
|
if index >= new_count:
|
|
|
|
|
index = new_count - 1
|
2017-05-10 11:21:12 +12:00
|
|
|
|
# If first is clicked
|
|
|
|
|
elif '\u23ea':
|
|
|
|
|
index = 0
|
|
|
|
|
# If last is clicked
|
|
|
|
|
elif '\u23e9':
|
|
|
|
|
index = count - 1
|
2017-02-07 15:56:04 +13:00
|
|
|
|
try:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
await message.remove_reaction(reaction.emoji, user)
|
2017-02-07 15:56:04 +13:00
|
|
|
|
except discord.Forbidden:
|
|
|
|
|
pass
|
2017-04-20 15:18:05 +12:00
|
|
|
|
await message.delete()
|
|
|
|
|
|
2017-05-30 09:17:00 +12:00
|
|
|
|
# noinspection PyUnusedLocal
|
|
|
|
|
async def on_voice_state_update(self, _, __, after):
|
2017-04-20 15:18:05 +12:00
|
|
|
|
if after is None or after.channel is None:
|
|
|
|
|
return
|
|
|
|
|
state = self.voice_states.get(after.channel.guild.id)
|
2017-04-24 10:28:13 +12:00
|
|
|
|
if state is None or state.voice is None or state.voice.channel is None:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
return
|
|
|
|
|
voice_channel = state.voice.channel
|
|
|
|
|
num_members = len(voice_channel.members)
|
|
|
|
|
state.required_skips = math.ceil((num_members + 1) / 3)
|
|
|
|
|
|
|
|
|
|
async def add_entry(self, song, ctx):
|
|
|
|
|
state = self.voice_states[ctx.message.guild.id]
|
2017-07-03 15:19:20 +12:00
|
|
|
|
entry = await state.songs.add_entry(song)
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if not state.playing:
|
2017-07-03 15:19:20 +12:00
|
|
|
|
self.bot.loop.create_task(state.play_next_song())
|
2017-06-07 20:30:19 +12:00
|
|
|
|
entry.requester = ctx.message.author
|
2017-04-20 15:18:05 +12:00
|
|
|
|
return entry
|
2017-02-07 15:56:04 +13:00
|
|
|
|
|
2017-07-03 15:19:20 +12:00
|
|
|
|
async def import_playlist(self, url, ctx):
|
|
|
|
|
state = self.voice_states[ctx.message.guild.id]
|
|
|
|
|
try:
|
|
|
|
|
msg = await ctx.send("Looking up {}\nThis may take a while...".format(url))
|
|
|
|
|
except:
|
|
|
|
|
msg = None
|
|
|
|
|
|
|
|
|
|
num_songs = None
|
|
|
|
|
successful = 0
|
|
|
|
|
failed = 0
|
|
|
|
|
fmt = "Downloading {} songs\n{} successful\n{} failed"
|
|
|
|
|
if msg:
|
|
|
|
|
# Go through each song in the playlist
|
|
|
|
|
async for success in state.songs.import_from(url, ctx.message.author):
|
|
|
|
|
# If this hasn't been set yet, the first yield is the number of songs
|
|
|
|
|
# Otherwise just add one based on successful or not
|
|
|
|
|
if not num_songs:
|
|
|
|
|
num_songs = success
|
|
|
|
|
elif success:
|
|
|
|
|
# If we're not playing yet and this is the first successful one we found
|
|
|
|
|
if not state.playing and successful == 0:
|
|
|
|
|
await state.play_next_song()
|
|
|
|
|
successful += 1
|
|
|
|
|
else:
|
|
|
|
|
failed += 1
|
|
|
|
|
try:
|
|
|
|
|
await msg.edit(content=fmt.format(num_songs, successful, failed))
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
await state.songs.import_from(url)
|
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
async def join_channel(self, channel, text_channel):
|
2017-05-30 09:17:00 +12:00
|
|
|
|
state = self.voice_states.get(channel.guild.id)
|
|
|
|
|
log.info("Joining channel {} in guild {}".format(channel.id, channel.guild.id))
|
|
|
|
|
|
|
|
|
|
# Send a message letting the channel know we are attempting to join
|
|
|
|
|
try:
|
2017-06-07 20:30:19 +12:00
|
|
|
|
msg = await text_channel.send("Trying to join channel {}...".format(channel.name))
|
2017-05-30 09:17:00 +12:00
|
|
|
|
except discord.Forbidden:
|
|
|
|
|
msg = None
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# If we're already connected, try moving to the channel
|
|
|
|
|
if state and state.voice and state.voice.channel:
|
|
|
|
|
await state.voice.move_to(channel)
|
|
|
|
|
# Otherwise, try connecting
|
|
|
|
|
else:
|
|
|
|
|
await channel.connect()
|
|
|
|
|
|
|
|
|
|
# If we have connnected, create our voice state
|
2017-06-07 20:30:19 +12:00
|
|
|
|
queue_type = self.bot.db.load('server_settings', key=channel.guild.id, pluck='queue_type')
|
2017-07-12 05:56:55 +12:00
|
|
|
|
volume = self.bot.db.load('server_settings', key=channel.guild.id, pluck='volume')
|
2017-06-07 20:30:19 +12:00
|
|
|
|
user_queue = queue_type == "user"
|
2017-07-12 05:56:55 +12:00
|
|
|
|
self.voice_states[channel.guild.id] = VoiceState(channel.guild, self.bot, user_queue=user_queue,
|
|
|
|
|
volume=volume)
|
2017-05-30 09:17:00 +12:00
|
|
|
|
|
|
|
|
|
# If we can send messages, edit it to let the channel know we have succesfully joined
|
|
|
|
|
if msg:
|
2017-06-10 14:50:17 +12:00
|
|
|
|
try:
|
|
|
|
|
await msg.edit(content="Ready to play audio in channel {}".format(channel.name))
|
|
|
|
|
except discord.NotFound:
|
|
|
|
|
pass
|
2017-05-30 09:17:00 +12:00
|
|
|
|
return True
|
|
|
|
|
# If we time out trying to join, just let them know and return False
|
2017-06-12 06:03:34 +12:00
|
|
|
|
except (asyncio.TimeoutError, OSError):
|
2017-05-30 09:17:00 +12:00
|
|
|
|
if msg:
|
2017-06-10 14:50:17 +12:00
|
|
|
|
try:
|
|
|
|
|
await msg.edit(content="Sorry, but I couldn't connect right now! Please try again later")
|
|
|
|
|
except discord.NotFound:
|
|
|
|
|
pass
|
2017-05-30 09:17:00 +12:00
|
|
|
|
return False
|
|
|
|
|
# Theoretically this should never happen, however in rare cirumstances it does
|
|
|
|
|
# This error arises when we are already in a channel and don't use "move"
|
|
|
|
|
# We already checked if that existed above though, so this means the voice connection got stuck somewhere
|
|
|
|
|
except discord.ClientException:
|
|
|
|
|
if channel.guild.voice_client:
|
|
|
|
|
# Force a disconnection
|
|
|
|
|
await channel.guild.voice_client.disconnect(force=True)
|
|
|
|
|
# Log this so we can track it
|
|
|
|
|
log.warning(
|
|
|
|
|
"Force cleared voice connection on guild {} after being stuck "
|
|
|
|
|
"between connected/not connected".format(channel.guild.id))
|
|
|
|
|
# Let them know what happened
|
2017-06-08 06:53:40 +12:00
|
|
|
|
await text_channel.send("Sorry but I couldn't connect...try again?")
|
2017-05-30 09:17:00 +12:00
|
|
|
|
return False
|
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-02-13 12:45:19 +13:00
|
|
|
|
@utils.custom_perms(send_messages=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-10-30 09:23:12 +13:00
|
|
|
|
async def progress(self, ctx):
|
|
|
|
|
"""Provides the progress of the current song"""
|
|
|
|
|
|
|
|
|
|
# Make sure we're playing first
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-05-19 14:03:09 +12:00
|
|
|
|
if state is None or not state.playing or state.current is None:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Not playing anything.')
|
2016-10-30 09:23:12 +13:00
|
|
|
|
else:
|
|
|
|
|
progress = state.current.progress
|
|
|
|
|
length = state.current.length
|
|
|
|
|
# Another check, just to make sure; this may happen for a very brief amount of time
|
|
|
|
|
# Between when the song was requested, and still downloading to play
|
|
|
|
|
if not progress or not length:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Not playing anything.')
|
2016-10-30 09:23:12 +13:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Otherwise just format this nicely
|
|
|
|
|
progress = divmod(round(progress, 0), 60)
|
|
|
|
|
length = divmod(round(length, 0), 60)
|
2016-10-30 10:00:53 +13:00
|
|
|
|
fmt = "Current song progress: {0[0]}m {0[1]}s/{1[0]}m {1[1]}s".format(progress, length)
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send(fmt)
|
2016-10-30 09:23:12 +13:00
|
|
|
|
|
2017-04-20 15:18:05 +12:00
|
|
|
|
@commands.command(aliases=['summon'])
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-02-13 12:45:19 +13:00
|
|
|
|
@utils.custom_perms(send_messages=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2017-04-20 15:18:05 +12:00
|
|
|
|
async def join(self, ctx, *, channel: discord.VoiceChannel = None):
|
|
|
|
|
"""Joins a voice channel. Provide the name of a voice channel after the command, and
|
|
|
|
|
I will attempt to join this channel. Otherwise, I will join the channel you are in.
|
|
|
|
|
|
|
|
|
|
EXAMPLE: !join Music
|
|
|
|
|
RESULT: I have joined the music channel"""
|
|
|
|
|
if channel is None:
|
|
|
|
|
if ctx.message.author.voice is None or ctx.message.author.voice.channel is None:
|
|
|
|
|
await ctx.send("You need to either be in a voice channel, or provide the name of a voice channel!")
|
|
|
|
|
return False
|
|
|
|
|
channel = ctx.message.author.voice.channel
|
|
|
|
|
|
2017-04-20 15:32:18 +12:00
|
|
|
|
perms = channel.permissions_for(ctx.message.guild.me)
|
|
|
|
|
|
|
|
|
|
if not perms.connect or not perms.speak or not perms.use_voice_activation:
|
2017-04-20 15:43:24 +12:00
|
|
|
|
await ctx.send("I do not have correct permissions in {}! Please turn on `connect`, `speak`, and `use "
|
|
|
|
|
"voice activation`".format(channel.name))
|
2017-05-04 13:12:13 +12:00
|
|
|
|
return False
|
2017-04-20 15:32:18 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
return await self.join_channel(channel, ctx.channel)
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
2017-07-03 15:19:20 +12:00
|
|
|
|
@commands.command(name='import')
|
|
|
|
|
@commands.guild_only()
|
|
|
|
|
@utils.custom_perms(send_messages=True)
|
|
|
|
|
@utils.check_restricted()
|
|
|
|
|
async def _import(self, ctx, *, song: str):
|
|
|
|
|
"""Imports a song into the current voice queue"""
|
|
|
|
|
# If we don't have a voice state yet, create one
|
|
|
|
|
if not self.bot.db.load('server_settings', key=ctx.message.guild.id, pluck='playlists_allowed'):
|
2017-07-12 05:56:55 +12:00
|
|
|
|
await ctx.send("You cannot import playlists at this time; the {}allowplaylists command can be used to "
|
|
|
|
|
"change this setting".format(ctx.prefix))
|
2017-07-03 15:19:20 +12:00
|
|
|
|
return
|
|
|
|
|
if ctx.message.guild.id not in self.voice_states:
|
|
|
|
|
if not await ctx.invoke(self.join):
|
|
|
|
|
return
|
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
|
|
|
|
# If this is a user queue, this is the wrong command
|
|
|
|
|
if state.user_queue:
|
|
|
|
|
await ctx.send("The current queue type is the DJ queue. "
|
|
|
|
|
"Use the command {}dj to join this queue".format(ctx.prefix))
|
|
|
|
|
return
|
|
|
|
|
# Ensure the user is in the voice channel
|
|
|
|
|
try:
|
|
|
|
|
if ctx.message.author.voice.channel != ctx.message.guild.me.voice.channel:
|
|
|
|
|
await ctx.send("You need to be in the channel to use this command!")
|
|
|
|
|
return
|
|
|
|
|
except AttributeError:
|
|
|
|
|
await ctx.send("You need to be in the channel to use this command!")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
song = re.sub('[<>\[\]]', '', song)
|
2017-07-12 05:56:55 +12:00
|
|
|
|
# Check if we've got the list variable in the URL, if so lets just use this
|
|
|
|
|
playlist_id = re.search(r'list=(.+)', song)
|
|
|
|
|
if playlist_id:
|
|
|
|
|
song = playlist_id.group(1)
|
|
|
|
|
try:
|
|
|
|
|
await self.import_playlist(song, ctx)
|
|
|
|
|
except WrongEntryTypeError:
|
|
|
|
|
await ctx.send("This URL is not a playlist! If you want to play this song just use `play`")
|
|
|
|
|
except ExtractionError:
|
|
|
|
|
await ctx.send("Failed to download {}! If this is not a playlist, use the `play` command".format(song))
|
2017-07-03 15:19:20 +12:00
|
|
|
|
|
2017-04-20 15:18:05 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-02-13 12:45:19 +13:00
|
|
|
|
@utils.custom_perms(send_messages=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-07-09 13:27:19 +12:00
|
|
|
|
async def play(self, ctx, *, song: str):
|
2016-07-08 01:05:42 +12:00
|
|
|
|
"""Plays a song.
|
|
|
|
|
If there is a song currently in the queue, then it is
|
|
|
|
|
queued until the next song is done playing.
|
|
|
|
|
This command automatically searches as well from YouTube.
|
|
|
|
|
The list of supported sites can be found here:
|
|
|
|
|
https://rg3.github.io/youtube-dl/supportedsites.html
|
|
|
|
|
"""
|
2017-06-28 12:12:01 +12:00
|
|
|
|
# If we don't have a voice state yet, create one
|
2017-04-20 15:18:05 +12:00
|
|
|
|
if ctx.message.guild.id not in self.voice_states:
|
|
|
|
|
if not await ctx.invoke(self.join):
|
2016-07-08 01:05:42 +12:00
|
|
|
|
return
|
2017-06-28 12:12:01 +12:00
|
|
|
|
|
2017-07-17 12:46:06 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-06-28 12:12:01 +12:00
|
|
|
|
# If this is a user queue, this is the wrong command
|
2017-07-17 12:46:06 +12:00
|
|
|
|
if state and state.user_queue:
|
2017-06-07 20:30:19 +12:00
|
|
|
|
await ctx.send("The current queue type is the DJ queue. "
|
|
|
|
|
"Use the command {}dj to join this queue".format(ctx.prefix))
|
|
|
|
|
return
|
2017-07-31 14:48:39 +12:00
|
|
|
|
# If the state is live, songs can't play at the same time
|
|
|
|
|
if state and state.live:
|
|
|
|
|
await ctx.send("Currently playing a live stream! The stream needs to stop before a song can be played")
|
|
|
|
|
return
|
|
|
|
|
|
2017-06-28 12:12:01 +12:00
|
|
|
|
# Ensure the user is in the voice channel
|
|
|
|
|
try:
|
|
|
|
|
if ctx.message.author.voice.channel != ctx.message.guild.me.voice.channel:
|
|
|
|
|
await ctx.send("You need to be in the channel to use this command!")
|
2017-07-03 15:19:20 +12:00
|
|
|
|
return
|
2017-06-28 12:12:01 +12:00
|
|
|
|
except AttributeError:
|
|
|
|
|
await ctx.send("You need to be in the channel to use this command!")
|
2017-07-03 15:19:20 +12:00
|
|
|
|
return
|
2016-08-21 17:36:38 +12:00
|
|
|
|
|
2017-04-21 12:13:57 +12:00
|
|
|
|
song = re.sub('[<>\[\]]', '', song)
|
2017-05-30 09:17:00 +12:00
|
|
|
|
if len(song) == 11:
|
2017-06-29 07:53:48 +12:00
|
|
|
|
# Youtube-dl will attempt to use results with a length of 11 as a video ID
|
2017-05-14 07:23:18 +12:00
|
|
|
|
# If this is a search, this causes it to break
|
|
|
|
|
# Youtube will still succeed if this *is* an ID provided, if there's a . after
|
|
|
|
|
song += "."
|
2017-04-21 12:13:57 +12:00
|
|
|
|
|
2017-07-03 15:19:20 +12:00
|
|
|
|
try:
|
|
|
|
|
msg = await ctx.send("Looking up {}...".format(song))
|
|
|
|
|
except:
|
|
|
|
|
msg = None
|
|
|
|
|
|
2016-08-28 15:32:11 +12:00
|
|
|
|
try:
|
2017-04-20 15:18:05 +12:00
|
|
|
|
entry = await self.add_entry(song, ctx)
|
2017-07-12 05:56:55 +12:00
|
|
|
|
# This error only happens if Discord has derped, and the voice state didn't get created succesfully
|
|
|
|
|
except KeyError:
|
|
|
|
|
await ctx.send("Sorry, but I failed to connect! Please try again!")
|
2017-04-23 14:45:36 +12:00
|
|
|
|
except LiveStreamError as e:
|
2017-04-23 10:54:23 +12:00
|
|
|
|
await ctx.send(str(e))
|
2017-05-12 11:25:37 +12:00
|
|
|
|
except WrongEntryTypeError:
|
2017-07-03 15:19:20 +12:00
|
|
|
|
await ctx.send("Please use the {}import command to import a playlist.".format(ctx.prefix))
|
2017-05-14 07:23:18 +12:00
|
|
|
|
except ExtractionError as e:
|
2017-05-21 06:44:15 +12:00
|
|
|
|
error = e.message.split('\n')
|
|
|
|
|
if len(error) >= 3:
|
|
|
|
|
# The first entry is the "We couldn't download" printed by the exception
|
|
|
|
|
# The 2nd is the new line
|
|
|
|
|
# We want youtube_dl's error message, but just the first part, the actual "error"
|
|
|
|
|
error = error[2]
|
|
|
|
|
# This is colour formatting for the console...it's just going to show up as text on discord
|
2017-06-07 20:30:19 +12:00
|
|
|
|
error = error.replace("[0;31mERROR:[0m ", "")
|
2017-05-21 06:44:15 +12:00
|
|
|
|
else:
|
|
|
|
|
# This happens when the download just returns `None`
|
|
|
|
|
error = error[0]
|
2017-05-14 07:23:18 +12:00
|
|
|
|
await ctx.send(error)
|
2017-04-20 15:18:05 +12:00
|
|
|
|
else:
|
2017-05-12 11:25:37 +12:00
|
|
|
|
try:
|
|
|
|
|
if entry is None:
|
|
|
|
|
await ctx.send("Sorry but I couldn't download/find {}".format(song))
|
|
|
|
|
else:
|
2017-07-03 15:19:20 +12:00
|
|
|
|
embed = entry.embed
|
2017-05-12 11:25:37 +12:00
|
|
|
|
embed.title = "Enqueued song!"
|
2017-07-03 15:19:20 +12:00
|
|
|
|
try:
|
|
|
|
|
await msg.edit(content=None, embed=embed)
|
|
|
|
|
except:
|
|
|
|
|
await ctx.send(embed=embed)
|
2017-06-07 20:30:19 +12:00
|
|
|
|
except discord.Forbidden:
|
2017-05-12 11:25:37 +12:00
|
|
|
|
pass
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-06-28 12:12:01 +12:00
|
|
|
|
@utils.custom_perms(mute_members=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-10-03 07:08:20 +13:00
|
|
|
|
async def volume(self, ctx, value: int = None):
|
2016-07-08 01:05:42 +12:00
|
|
|
|
"""Sets the volume of the currently playing song."""
|
|
|
|
|
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-05-06 08:55:29 +12:00
|
|
|
|
if value:
|
2017-05-30 09:17:00 +12:00
|
|
|
|
value /= 100
|
2017-05-05 10:33:26 +12:00
|
|
|
|
if state is None or state.voice is None:
|
|
|
|
|
await ctx.send("I need to be in a channel before my volume can be set")
|
2017-04-24 12:09:49 +12:00
|
|
|
|
elif value is None:
|
2017-05-13 11:13:27 +12:00
|
|
|
|
await ctx.send('Current volume is {:.0%}'.format(state.volume))
|
2017-05-05 10:33:26 +12:00
|
|
|
|
elif value > 1.0:
|
|
|
|
|
await ctx.send("Sorry but the max volume is 100%")
|
|
|
|
|
else:
|
|
|
|
|
state.volume = value
|
2017-07-12 05:56:55 +12:00
|
|
|
|
entry = {'server_id': str(ctx.message.guild.id), 'volume': value}
|
|
|
|
|
self.bot.db.save('server_settings', entry)
|
2017-05-13 11:13:27 +12:00
|
|
|
|
await ctx.send('Set the volume to {:.0%}'.format(state.volume))
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-06-28 12:12:01 +12:00
|
|
|
|
@utils.custom_perms(mute_members=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
async def pause(self, ctx):
|
|
|
|
|
"""Pauses the currently played song."""
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-05-06 16:58:05 +12:00
|
|
|
|
if state and state.voice and state.voice.is_connected():
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state.voice.pause()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-06-28 12:12:01 +12:00
|
|
|
|
@utils.custom_perms(mute_members=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
async def resume(self, ctx):
|
|
|
|
|
"""Resumes the currently played song."""
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-05-06 16:58:05 +12:00
|
|
|
|
if state and state.voice and state.voice.is_connected():
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state.voice.resume()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-06-28 12:12:01 +12:00
|
|
|
|
@utils.custom_perms(mute_members=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
async def stop(self, ctx):
|
|
|
|
|
"""Stops playing audio and leaves the voice channel.
|
|
|
|
|
This also clears the queue.
|
|
|
|
|
"""
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-06-18 11:04:11 +12:00
|
|
|
|
voice = ctx.message.guild.voice_client
|
2016-08-21 17:36:38 +12:00
|
|
|
|
|
2017-06-18 11:04:11 +12:00
|
|
|
|
# If we have a state, clear the songs, dj's, then skip the current song
|
|
|
|
|
if state:
|
2017-04-20 15:35:06 +12:00
|
|
|
|
state.songs.clear()
|
2017-06-18 11:04:11 +12:00
|
|
|
|
state.djs.clear()
|
|
|
|
|
state.skip()
|
2017-05-19 14:03:09 +12:00
|
|
|
|
try:
|
|
|
|
|
del self.voice_states[ctx.message.guild.id]
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
2016-08-10 14:13:53 +12:00
|
|
|
|
|
2017-06-18 11:04:11 +12:00
|
|
|
|
# If we have a voice connection (separate from state...just in case....)
|
|
|
|
|
# Then stop playing, and disconnect
|
|
|
|
|
if voice:
|
|
|
|
|
voice.stop()
|
2017-07-10 08:04:48 +12:00
|
|
|
|
await voice.disconnect(force=True)
|
2017-08-07 11:10:59 +12:00
|
|
|
|
# So discord has a weird case where the connection can be interrupted, and an auto-reconnect is attempted
|
|
|
|
|
# Auto-reconnects aren't handled for bot accounts, and this causes the bot to appear to be in the channel while it's actually not
|
|
|
|
|
# Since this means there is no connection (checked by voice being None) there's nothing to disconnect from
|
|
|
|
|
# So our workaround here is try to connect (this may timeout, and force it to now be visually disconnected) then disconnect
|
|
|
|
|
else:
|
|
|
|
|
if ctx.message.guild.me.voice:
|
2017-08-07 11:15:10 +12:00
|
|
|
|
channel = ctx.message.guild.me.voice.channel
|
|
|
|
|
else:
|
|
|
|
|
# Get a list of all channels that we can connect to
|
|
|
|
|
channels = [c for c in ctx.message.guild.voice_channels if c.permissions_for(ctx.message.guild.me).connect]
|
|
|
|
|
if len(channels) > 0:
|
|
|
|
|
channel = channels[0]
|
|
|
|
|
# If we can't connect to any channels but we're stuck in a voice channel what is this server doing .-.
|
|
|
|
|
# Don't handle this, just return
|
2017-08-07 11:10:59 +12:00
|
|
|
|
else:
|
2017-08-07 11:15:10 +12:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Now simply connect then disconnect
|
|
|
|
|
try:
|
|
|
|
|
await channel.connect()
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
# Refresh the guild info, as want whatever the new VoiceClient is
|
|
|
|
|
guild = bot.get_guild(ctx.message.guild.id)
|
|
|
|
|
if guild.voice_client:
|
|
|
|
|
await guild.voice_client.disconnect(force=True)
|
2017-06-18 11:04:11 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-02-13 12:45:19 +13:00
|
|
|
|
@utils.custom_perms(send_messages=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-08-07 01:09:24 +12:00
|
|
|
|
async def eta(self, ctx):
|
|
|
|
|
"""Provides an ETA on when your next song will play"""
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2016-08-07 01:09:24 +12:00
|
|
|
|
author = ctx.message.author
|
2016-08-10 14:13:53 +12:00
|
|
|
|
|
2017-04-20 15:18:05 +12:00
|
|
|
|
if state is None or not state.playing:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Not playing any music right now...')
|
2016-08-07 01:09:24 +12:00
|
|
|
|
return
|
2016-12-22 12:06:32 +13:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if state.user_queue:
|
|
|
|
|
queue = [x.peek() for x in state.djs if x.peek()]
|
|
|
|
|
else:
|
|
|
|
|
queue = state.songs.entries
|
2016-08-16 15:30:52 +12:00
|
|
|
|
if len(queue) == 0:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send("Nothing currently in the queue")
|
2016-08-07 01:09:24 +12:00
|
|
|
|
return
|
2016-08-21 17:36:38 +12:00
|
|
|
|
|
2016-12-22 12:06:32 +13:00
|
|
|
|
# Start off by adding the remaining length of the current song
|
2017-05-14 07:23:18 +12:00
|
|
|
|
count = state.current.remaining or 0
|
2016-08-07 01:09:24 +12:00
|
|
|
|
found = False
|
2016-08-16 15:30:52 +12:00
|
|
|
|
# Loop through the songs in the queue, until the author is found as the requester
|
|
|
|
|
# The found bool is used to see if we actually found the author, or we just looped through the whole queue
|
|
|
|
|
for song in queue:
|
2016-08-07 01:09:24 +12:00
|
|
|
|
if song.requester == author:
|
|
|
|
|
found = True
|
|
|
|
|
break
|
2017-07-24 10:34:28 +12:00
|
|
|
|
count += song.length
|
2016-08-21 17:36:38 +12:00
|
|
|
|
|
2016-08-07 01:09:24 +12:00
|
|
|
|
if not found:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send("You are not in the queue!")
|
2016-08-07 01:09:24 +12:00
|
|
|
|
return
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send("ETA till your next play is: {0[0]}m {0[1]}s".format(divmod(round(count, 0), 60)))
|
2016-08-10 14:13:53 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-02-13 12:45:19 +13:00
|
|
|
|
@utils.custom_perms(send_messages=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-08-06 01:45:20 +12:00
|
|
|
|
async def queue(self, ctx):
|
|
|
|
|
"""Provides a printout of the songs that are in the queue"""
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
|
|
|
|
if state is None:
|
|
|
|
|
await ctx.send("Nothing currently in the queue")
|
2016-08-06 01:45:20 +12:00
|
|
|
|
return
|
2017-06-07 20:30:19 +12:00
|
|
|
|
|
|
|
|
|
if state.user_queue:
|
|
|
|
|
_queue = [x.peek() for x in state.djs if x.peek()]
|
|
|
|
|
else:
|
|
|
|
|
_queue = state.songs.entries
|
2017-04-20 15:18:05 +12:00
|
|
|
|
if len(_queue) == 0:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send("Nothing currently in the queue")
|
2016-08-07 00:27:08 +12:00
|
|
|
|
else:
|
2017-02-07 15:56:04 +13:00
|
|
|
|
self.bot.loop.create_task(self.queue_embed_task(state, ctx.message.channel, ctx.message.author))
|
2016-07-09 13:27:19 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-02-13 12:45:19 +13:00
|
|
|
|
@utils.custom_perms(send_messages=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-07-09 13:27:19 +12:00
|
|
|
|
async def queuelength(self, ctx):
|
2016-07-08 01:05:42 +12:00
|
|
|
|
"""Prints the length of the queue"""
|
2017-05-30 09:27:12 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-06-07 20:30:19 +12:00
|
|
|
|
if state is None:
|
|
|
|
|
await ctx.send("Nothing currently in the queue")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if state.user_queue:
|
|
|
|
|
_queue = [x.peek() for x in state.djs if x.peek()]
|
2017-05-30 09:27:12 +12:00
|
|
|
|
else:
|
2017-06-07 20:30:19 +12:00
|
|
|
|
_queue = state.songs.entries
|
|
|
|
|
if len(_queue) == 0:
|
|
|
|
|
await ctx.send("Nothing currently in the queue")
|
|
|
|
|
await ctx.send("There are a total of {} songs in the queue".format(len(_queue)))
|
2016-07-31 12:20:55 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-02-13 12:45:19 +13:00
|
|
|
|
@utils.custom_perms(send_messages=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
async def skip(self, ctx):
|
|
|
|
|
"""Vote to skip a song. The song requester can automatically skip.
|
2016-08-20 16:00:23 +12:00
|
|
|
|
approximately 1/3 of the members in the voice channel
|
|
|
|
|
are required to vote to skip for the song to be skipped.
|
2016-07-08 01:05:42 +12:00
|
|
|
|
"""
|
|
|
|
|
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-06-08 12:40:50 +12:00
|
|
|
|
if state is None or not state.playing:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Not playing any music right now...')
|
2016-07-08 01:05:42 +12:00
|
|
|
|
return
|
2017-06-28 12:12:01 +12:00
|
|
|
|
# Ensure the user is in our channel
|
|
|
|
|
try:
|
|
|
|
|
if ctx.message.author.voice.channel != ctx.message.guild.me.voice.channel:
|
|
|
|
|
await ctx.send("You need to be in the channel to use this command!")
|
|
|
|
|
except AttributeError:
|
|
|
|
|
await ctx.send("You need to be in the channel to use this command!")
|
2016-08-21 17:36:38 +12:00
|
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
|
# Check if the person requesting a skip is the requester of the song, if so automatically skip
|
2016-07-08 01:05:42 +12:00
|
|
|
|
voter = ctx.message.author
|
2017-06-08 06:53:40 +12:00
|
|
|
|
if hasattr(state.current, 'requester') and voter == state.current.requester:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Requester requested skipping song...')
|
2016-07-08 01:05:42 +12:00
|
|
|
|
state.skip()
|
2016-08-16 15:30:52 +12:00
|
|
|
|
# Otherwise check if the voter has already voted
|
2016-07-08 01:05:42 +12:00
|
|
|
|
elif voter.id not in state.skip_votes:
|
|
|
|
|
state.skip_votes.add(voter.id)
|
|
|
|
|
total_votes = len(state.skip_votes)
|
2016-08-21 17:36:38 +12:00
|
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
|
# Now check how many votes have been made, if 3 then go ahead and skip, otherwise add to the list of votes
|
2016-08-20 16:00:23 +12:00
|
|
|
|
if total_votes >= state.required_skips:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Skip vote passed, skipping song...')
|
2016-07-08 01:05:42 +12:00
|
|
|
|
state.skip()
|
|
|
|
|
else:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Skip vote added, currently at [{}/{}]'.format(total_votes, state.required_skips))
|
2016-07-08 01:05:42 +12:00
|
|
|
|
else:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('You have already voted to skip this song.')
|
2016-07-31 12:20:55 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-06-28 12:12:01 +12:00
|
|
|
|
@utils.custom_perms(mute_members=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
async def modskip(self, ctx):
|
|
|
|
|
"""Forces a song skip, can only be used by a moderator"""
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-07-24 10:34:28 +12:00
|
|
|
|
if state is None:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Not playing any music right now...')
|
2016-07-08 01:05:42 +12:00
|
|
|
|
return
|
2016-07-31 12:20:55 +12:00
|
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
|
state.skip()
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Song has just been skipped.')
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
2017-04-09 15:04:46 +12:00
|
|
|
|
@commands.guild_only()
|
2017-02-13 12:45:19 +13:00
|
|
|
|
@utils.custom_perms(send_messages=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
async def playing(self, ctx):
|
|
|
|
|
"""Shows info about the currently played song."""
|
|
|
|
|
|
2017-04-20 15:18:05 +12:00
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-05-20 06:55:45 +12:00
|
|
|
|
if state is None or not state.playing or not state.current:
|
2017-03-08 12:56:24 +13:00
|
|
|
|
await ctx.send('Not playing anything.')
|
2016-07-08 01:05:42 +12:00
|
|
|
|
else:
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# Create the embed object we'll use
|
|
|
|
|
embed = discord.Embed()
|
|
|
|
|
# Fill in the simple things
|
|
|
|
|
embed.add_field(name='Title', value=state.current.title, inline=False)
|
2017-07-31 14:48:39 +12:00
|
|
|
|
if state.current.requester:
|
|
|
|
|
embed.add_field(name='Requester', value=state.current.requester.display_name, inline=False)
|
2017-02-07 15:56:04 +13:00
|
|
|
|
# Get the amount of current skips, and display how many have been skipped/how many required
|
2016-07-08 01:05:42 +12:00
|
|
|
|
skip_count = len(state.skip_votes)
|
2017-02-07 15:56:04 +13:00
|
|
|
|
embed.add_field(name='Skip Count', value='{}/{}'.format(skip_count, state.required_skips), inline=False)
|
|
|
|
|
# Get the current progress and display this
|
|
|
|
|
length = state.current.length
|
2017-06-28 12:12:01 +12:00
|
|
|
|
progress = state.current.progress
|
|
|
|
|
if length and progress:
|
2017-05-14 07:23:18 +12:00
|
|
|
|
progress = divmod(round(progress, 0), 60)
|
|
|
|
|
length = divmod(round(length, 0), 60)
|
|
|
|
|
fmt = "{0[0]}m {0[1]}s/{1[0]}m {1[1]}s".format(progress, length)
|
|
|
|
|
embed.add_field(name='Progress', value=fmt, inline=False)
|
|
|
|
|
# And send the embed
|
2017-07-03 15:19:20 +12:00
|
|
|
|
await ctx.send(embed=embed)
|
2016-07-09 13:27:19 +12:00
|
|
|
|
|
2017-06-07 20:30:19 +12:00
|
|
|
|
@commands.command()
|
|
|
|
|
@commands.guild_only()
|
|
|
|
|
@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 dj(self, ctx):
|
|
|
|
|
"""Attempts to join the current DJ queue
|
|
|
|
|
|
|
|
|
|
EXAMPLE: !dj
|
|
|
|
|
RESULT: You are 7th on the waitlist for the queue"""
|
|
|
|
|
if ctx.message.guild.id not in self.voice_states:
|
|
|
|
|
if not await ctx.invoke(self.join):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
2017-07-31 14:48:39 +12:00
|
|
|
|
if state and not state.user_queue:
|
2017-06-07 20:30:19 +12:00
|
|
|
|
await ctx.send("The current queue type is the song queue. "
|
|
|
|
|
"Use the command {}play to add a song to the queue".format(ctx.prefix))
|
|
|
|
|
return
|
2017-07-31 14:48:39 +12:00
|
|
|
|
# If the state is live, songs can't play at the same time
|
|
|
|
|
if state and state.live:
|
|
|
|
|
await ctx.send("Currently playing a live stream! The stream needs to stop before a song can be played")
|
|
|
|
|
return
|
2017-06-07 20:30:19 +12:00
|
|
|
|
|
|
|
|
|
if state.get_dj(ctx.message.author):
|
|
|
|
|
await ctx.send("You are already in the DJ queue!")
|
|
|
|
|
else:
|
|
|
|
|
new_dj = self.bot.get_cog('DJEvents').djs[ctx.message.author.id]
|
2017-06-11 14:58:20 +12:00
|
|
|
|
if not new_dj.peek():
|
2017-06-28 12:12:01 +12:00
|
|
|
|
await ctx.send("You currently have nothing in your playlist! This can happen for two reasons:\n"
|
|
|
|
|
"1) You actually have nothing in your active playlist\n"
|
|
|
|
|
"2) You just joined the voice channel and your playlist is still being downloaded\n\n"
|
|
|
|
|
"If the first one is true, then you need to manage your playlist to have an active "
|
|
|
|
|
"playlist with songs in it. "
|
|
|
|
|
"Otherwise, you will need to wait while your songs are being downloaded before you can "
|
|
|
|
|
"join")
|
2017-06-11 14:58:20 +12:00
|
|
|
|
else:
|
|
|
|
|
state.djs.append(new_dj)
|
|
|
|
|
try:
|
|
|
|
|
await ctx.send("You have joined the DJ queue; there are currently {} people ahead of you".format(
|
|
|
|
|
state.djs.index(new_dj)))
|
|
|
|
|
except discord.Forbidden:
|
|
|
|
|
pass
|
2017-06-07 20:30:19 +12:00
|
|
|
|
|
2017-06-11 14:58:20 +12:00
|
|
|
|
if not state.playing:
|
|
|
|
|
await state.play_next_song()
|
2017-06-07 20:30:19 +12:00
|
|
|
|
|
2017-06-28 12:12:01 +12:00
|
|
|
|
@commands.command()
|
|
|
|
|
@commands.guild_only()
|
|
|
|
|
@utils.custom_perms(mute_members=True)
|
2017-06-28 12:26:32 +12:00
|
|
|
|
@utils.check_restricted()
|
2017-06-28 12:12:01 +12:00
|
|
|
|
async def shuffle(self, ctx):
|
|
|
|
|
"""Shuffles the current playlist, be it users or songs
|
|
|
|
|
|
|
|
|
|
EXAMPLE: !shuffle
|
|
|
|
|
RESULT: The queue is shuffled"""
|
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
|
|
|
|
if state:
|
|
|
|
|
if state.user_queue:
|
|
|
|
|
random.SystemRandom().shuffle(state.djs)
|
|
|
|
|
else:
|
|
|
|
|
state.songs.shuffle()
|
|
|
|
|
await ctx.send("The queue has been shuffled!")
|
|
|
|
|
else:
|
|
|
|
|
await ctx.send("There needs to be a queue before I can shuffle it!")
|
|
|
|
|
|
2017-07-31 14:48:39 +12:00
|
|
|
|
@commands.command()
|
|
|
|
|
@commands.guild_only()
|
|
|
|
|
@utils.custom_perms(mute_members=True)
|
|
|
|
|
@utils.check_restricted()
|
|
|
|
|
async def stream(self, ctx, *, url):
|
|
|
|
|
"""Plays a livestream
|
|
|
|
|
|
|
|
|
|
EXAMPLE: !stream live_stream_url
|
|
|
|
|
RESULT: A livestream starts playing"""
|
|
|
|
|
# If we don't have a voice state yet, create one
|
|
|
|
|
if ctx.message.guild.id not in self.voice_states:
|
|
|
|
|
if not await ctx.invoke(self.join):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
state = self.voice_states.get(ctx.message.guild.id)
|
|
|
|
|
|
|
|
|
|
# If we have a state, clear the songs, dj's, then skip the current song
|
|
|
|
|
if state and state.voice:
|
|
|
|
|
state.songs.clear()
|
|
|
|
|
state.djs.clear()
|
|
|
|
|
state.skip()
|
|
|
|
|
|
|
|
|
|
# Now we can start the livestream
|
|
|
|
|
# Create the source used
|
|
|
|
|
source = YoutubeDLLiveStreamSource(self.bot, url)
|
|
|
|
|
# Download the info
|
2017-08-05 06:59:27 +12:00
|
|
|
|
try:
|
|
|
|
|
await source.get_ready()
|
|
|
|
|
except ExtractionError as e:
|
|
|
|
|
error = e.message.split('\n')
|
|
|
|
|
if len(error) >= 3:
|
|
|
|
|
# The first entry is the "We couldn't download" printed by the exception
|
|
|
|
|
# The 2nd is the new line
|
|
|
|
|
# We want youtube_dl's error message, but just the first part, the actual "error"
|
|
|
|
|
error = error[2]
|
|
|
|
|
# This is colour formatting for the console...it's just going to show up as text on discord
|
|
|
|
|
error = error.replace("[0;31mERROR:[0m ", "")
|
|
|
|
|
else:
|
|
|
|
|
# This happens when the download just returns `None`
|
|
|
|
|
error = error[0]
|
|
|
|
|
await ctx.send(error)
|
|
|
|
|
return
|
2017-07-31 14:48:39 +12:00
|
|
|
|
# Set the current song as the livestream
|
|
|
|
|
state.current = source
|
|
|
|
|
# Use the volume transformer
|
|
|
|
|
source = PCMVolumeTransformer(source, volume=state.volume)
|
|
|
|
|
# Then play the livestream
|
|
|
|
|
state.voice.play(source)
|
|
|
|
|
state.live = True
|
|
|
|
|
else:
|
|
|
|
|
await ctx.send("Failed to join the channel...")
|
|
|
|
|
|
2016-07-09 13:27:19 +12:00
|
|
|
|
|
|
|
|
|
def setup(bot):
|
|
|
|
|
bot.add_cog(Music(bot))
|