2016-12-22 12:06:32 +13:00
|
|
|
from .utils import *
|
|
|
|
from .voice_utilities import *
|
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
|
|
|
|
2016-08-20 16:00:23 +12:00
|
|
|
import math
|
2016-10-30 09:23:12 +13:00
|
|
|
import time
|
|
|
|
import asyncio
|
2016-10-30 09:30:30 +13:00
|
|
|
import re
|
2016-12-24 05:34:30 +13:00
|
|
|
import os
|
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:
|
2016-12-22 19:56:25 +13:00
|
|
|
def __init__(self, bot, download):
|
2016-07-08 01:05:42 +12:00
|
|
|
self.current = None
|
|
|
|
self.voice = None
|
|
|
|
self.bot = bot
|
|
|
|
self.play_next_song = asyncio.Event()
|
2016-08-17 03:22:32 +12:00
|
|
|
# This is the queue that holds all VoiceEntry's
|
2016-12-22 12:06:32 +13:00
|
|
|
self.songs = Playlist(bot)
|
2016-08-20 16:00:23 +12:00
|
|
|
self.required_skips = 0
|
2016-08-17 03:22:32 +12:00
|
|
|
# a set of user_ids that voted
|
|
|
|
self.skip_votes = set()
|
|
|
|
# Our actual task that handles the queue system
|
|
|
|
self.audio_player = self.bot.loop.create_task(self.audio_player_task())
|
2016-07-25 06:55:18 +12:00
|
|
|
self.opts = {
|
|
|
|
'default_search': 'auto',
|
2016-08-21 17:36:38 +12:00
|
|
|
'quiet': True
|
2016-07-25 06:55:18 +12:00
|
|
|
}
|
2016-09-01 04:58:59 +12:00
|
|
|
self.volume = 50
|
2016-12-22 19:56:25 +13:00
|
|
|
self.downloader = download
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
|
|
def is_playing(self):
|
2016-08-16 15:30:52 +12:00
|
|
|
# If our VoiceClient or current VoiceEntry do not exist, then we are not playing a song
|
2016-07-08 01:05:42 +12:00
|
|
|
if self.voice is None or self.current is None:
|
|
|
|
return False
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# If they do exist, check if the current player has finished
|
2016-07-08 01:05:42 +12:00
|
|
|
player = self.current.player
|
2016-08-21 19:18:35 +12:00
|
|
|
try:
|
|
|
|
return not player.is_done()
|
|
|
|
except AttributeError:
|
|
|
|
return False
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
|
|
@property
|
|
|
|
def player(self):
|
|
|
|
return self.current.player
|
|
|
|
|
|
|
|
def skip(self):
|
2016-08-16 15:30:52 +12:00
|
|
|
# Make sure we clear the votes, before stopping the player
|
|
|
|
# When the player is stopped, our toggle_next method is called, so the next song can be played
|
2016-07-08 01:05:42 +12:00
|
|
|
self.skip_votes.clear()
|
|
|
|
if self.is_playing():
|
|
|
|
self.player.stop()
|
|
|
|
|
|
|
|
def toggle_next(self):
|
2016-12-24 05:34:30 +13:00
|
|
|
# Delete the old file (screw caching, when on 5000+ Guilds this takes up too much space)
|
|
|
|
os.remove(self.current.filename)
|
2016-08-16 15:30:52 +12:00
|
|
|
# Set the Event so that the next song in the queue can be played
|
2016-07-08 01:05:42 +12:00
|
|
|
self.bot.loop.call_soon_threadsafe(self.play_next_song.set)
|
|
|
|
|
|
|
|
async def audio_player_task(self):
|
|
|
|
while True:
|
2016-08-16 15:30:52 +12:00
|
|
|
# At the start of our task, clear the Event, so we can wait till it is next set
|
2016-07-31 12:20:55 +12:00
|
|
|
self.play_next_song.clear()
|
2016-08-16 15:30:52 +12:00
|
|
|
# Clear the votes skip that were for the last song
|
2016-08-06 01:45:20 +12:00
|
|
|
self.skip_votes.clear()
|
2016-08-16 15:30:52 +12:00
|
|
|
# Now wait for the next song in the queue
|
2016-12-22 12:06:32 +13:00
|
|
|
self.current = await self.songs.get_next_entry()
|
|
|
|
|
|
|
|
# Make sure we find a song
|
2016-12-22 19:56:25 +13:00
|
|
|
while self.current is None:
|
2016-12-22 12:06:32 +13:00
|
|
|
await asyncio.sleep(1)
|
|
|
|
self.current = await self.songs.get_next_entry()
|
|
|
|
|
|
|
|
# At this point we're sure we have a song, however it needs to be downloaded
|
2016-12-22 19:56:25 +13:00
|
|
|
while not getattr(self.current, 'filename'):
|
2016-12-22 12:06:32 +13:00
|
|
|
print("Downloading...")
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
|
|
# Create the player object
|
|
|
|
self.current.player = self.voice.create_ffmpeg_player(
|
2016-12-22 19:56:25 +13:00
|
|
|
self.current.filename,
|
|
|
|
before_options="-nostdin",
|
|
|
|
options="-vn -b:a 128k",
|
|
|
|
after=self.toggle_next
|
|
|
|
)
|
2016-12-22 12:06:32 +13:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# Now we can start actually playing the song
|
2016-07-08 01:05:42 +12:00
|
|
|
self.current.player.start()
|
2016-09-01 04:58:59 +12:00
|
|
|
self.current.player.volume = self.volume / 100
|
2016-10-30 09:23:12 +13:00
|
|
|
|
|
|
|
# Save the variable for when our time for this song has started
|
|
|
|
self.current.start_time = time.time()
|
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# Wait till the Event has been set, before doing our task again
|
2016-07-08 01:05:42 +12:00
|
|
|
await self.play_next_song.wait()
|
|
|
|
|
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
|
|
|
|
|
|
|
def get_voice_state(self, server):
|
|
|
|
state = self.voice_states.get(server.id)
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# Internally handle creating a voice state if there isn't a current state
|
2016-08-17 03:22:32 +12:00
|
|
|
# This can be used for example, in case something is skipped when not being connected
|
|
|
|
# We create the voice state when checked
|
2016-08-16 15:30:52 +12:00
|
|
|
# This only creates the state, we are still not playing anything, which can then be handled separately
|
2016-07-08 01:05:42 +12:00
|
|
|
if state is None:
|
2016-12-22 12:06:32 +13:00
|
|
|
state = VoiceState(self.bot, self.downloader)
|
2016-07-08 01:05:42 +12:00
|
|
|
self.voice_states[server.id] = state
|
|
|
|
|
|
|
|
return state
|
|
|
|
|
|
|
|
async def create_voice_client(self, channel):
|
2016-12-22 19:56:25 +13:00
|
|
|
"""Creates a voice client and saves it"""
|
2016-08-16 15:30:52 +12:00
|
|
|
# First join the channel and get the VoiceClient that we'll use to save per server
|
2016-12-22 12:06:32 +13:00
|
|
|
server = channel.server
|
|
|
|
state = self.get_voice_state(server)
|
|
|
|
voice = self.bot.voice_client_in(server)
|
|
|
|
|
|
|
|
if voice is None:
|
|
|
|
state.voice = await self.bot.join_voice_channel(channel)
|
|
|
|
return True
|
|
|
|
elif voice.channel == channel:
|
|
|
|
state.voice = voice
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
await voice.disconnect()
|
|
|
|
state.voice = await self.bot.join_voice_channel(channel)
|
|
|
|
return True
|
|
|
|
|
2016-12-22 19:56:25 +13:00
|
|
|
async def remove_voice_client(self, server):
|
|
|
|
"""Removes any voice clients from a server
|
|
|
|
This is sometimes needed, due to the unreliability of Discord's voice connection
|
|
|
|
We do not want to end up with a voice client stuck somewhere, so this cancels any found for a server"""
|
|
|
|
state = self.get_voice_state(server)
|
|
|
|
voice = self.bot.voice_client_in(server)
|
|
|
|
|
|
|
|
if voice:
|
|
|
|
await voice.disconnect()
|
|
|
|
if state.voice:
|
|
|
|
await state.voice.disconnect()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
|
|
def __unload(self):
|
2016-08-16 15:30:52 +12:00
|
|
|
# If this is unloaded, cancel all players and disconnect from all channels
|
2016-07-08 01:05:42 +12:00
|
|
|
for state in self.voice_states.values():
|
|
|
|
try:
|
|
|
|
state.audio_player.cancel()
|
|
|
|
if state.voice:
|
|
|
|
self.bot.loop.create_task(state.voice.disconnect())
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2016-08-20 16:00:23 +12:00
|
|
|
async def on_voice_state_update(self, before, after):
|
|
|
|
state = self.get_voice_state(after.server)
|
|
|
|
if state.voice is None:
|
|
|
|
return
|
2016-08-20 16:25:17 +12:00
|
|
|
voice_channel = state.voice.channel
|
|
|
|
if voice_channel != before.voice.voice_channel and voice_channel != after.voice.voice_channel:
|
|
|
|
return
|
2016-08-21 17:36:38 +12:00
|
|
|
num_members = len(voice_channel.voice_members)
|
|
|
|
state.required_skips = math.ceil((num_members + 1) / 3)
|
|
|
|
|
2016-10-30 09:23:12 +13:00
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
|
|
@checks.custom_perms(send_messages=True)
|
|
|
|
async def progress(self, ctx):
|
|
|
|
"""Provides the progress of the current song"""
|
|
|
|
|
|
|
|
# Make sure we're playing first
|
|
|
|
state = self.get_voice_state(ctx.message.server)
|
|
|
|
if not state.is_playing():
|
|
|
|
await self.bot.say('Not playing anything.')
|
|
|
|
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:
|
|
|
|
await self.bot.say('Not playing anything.')
|
|
|
|
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)
|
2016-10-30 09:23:12 +13:00
|
|
|
await self.bot.say(fmt)
|
|
|
|
|
2016-12-23 05:01:02 +13:00
|
|
|
@commands.command(no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
2016-12-22 19:56:25 +13:00
|
|
|
async def join(self, *, channel: discord.Channel):
|
2016-07-08 01:05:42 +12:00
|
|
|
"""Joins a voice channel."""
|
|
|
|
try:
|
|
|
|
await self.create_voice_client(channel)
|
2016-08-16 15:30:52 +12:00
|
|
|
# Check if the channel given was an actual voice channel
|
2016-07-08 01:05:42 +12:00
|
|
|
except discord.InvalidArgument:
|
|
|
|
await self.bot.say('This is not a voice channel...')
|
2016-12-22 19:56:25 +13:00
|
|
|
except (asyncio.TimeoutError, discord.ConnectionClosed):
|
|
|
|
await self.bot.say("I failed to connect! This can sometimes be caused by your server region being far away."
|
|
|
|
" Otherwise this is an issue on Discord's end, causing the connect to timeout!")
|
|
|
|
await self.remove_voice_client(channel.server)
|
2016-07-08 01:05:42 +12:00
|
|
|
else:
|
|
|
|
await self.bot.say('Ready to play audio in ' + channel.name)
|
|
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
2016-07-08 01:05:42 +12:00
|
|
|
async def summon(self, ctx):
|
|
|
|
"""Summons the bot to join your voice channel."""
|
2016-08-28 10:18:37 +12:00
|
|
|
# This method will be invoked by other commands, so we should return True or False instead of just returning
|
2016-08-16 15:30:52 +12:00
|
|
|
# First check if the author is even in a voice_channel
|
2016-07-08 01:05:42 +12:00
|
|
|
summoned_channel = ctx.message.author.voice_channel
|
|
|
|
if summoned_channel is None:
|
|
|
|
await self.bot.say('You are not in a voice channel.')
|
|
|
|
return False
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-12-22 12:06:32 +13:00
|
|
|
# Then simply create a voice client
|
2016-12-22 19:56:25 +13:00
|
|
|
try:
|
|
|
|
success = await self.create_voice_client(summoned_channel)
|
|
|
|
except (asyncio.TimeoutError, discord.ConnectionClosed):
|
|
|
|
await self.bot.say("I failed to connect! This can sometimes be caused by your server region being far away."
|
|
|
|
" Otherwise this is an issue on Discord's end, causing the connect to timeout!")
|
|
|
|
await self.remove_voice_client(summoned_channel.server)
|
|
|
|
return False
|
2016-12-22 12:06:32 +13:00
|
|
|
|
|
|
|
if success:
|
|
|
|
try:
|
|
|
|
await self.bot.say('Ready to play audio in ' + summoned_channel.name)
|
|
|
|
except discord.Forbidden:
|
|
|
|
pass
|
|
|
|
return success
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
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
|
|
|
|
"""
|
2016-12-22 19:56:25 +13:00
|
|
|
# Fuck you soundcloud
|
|
|
|
if 'soundcloud.com' in song:
|
|
|
|
await self.bot.say("Soundcloud has changed the way that they allow downloads, and it now requires "
|
|
|
|
"authorization. Unfortunately, I currently have no way to download from soundcloud")
|
|
|
|
return
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
state = self.get_voice_state(ctx.message.server)
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# First check if we are connected to a voice channel at all, if not summon to the channel the author is in
|
|
|
|
# Since summon checks if the author is in a channel, we don't need to handle that here, just return if it failed
|
2016-07-08 01:05:42 +12:00
|
|
|
if state.voice is None:
|
|
|
|
success = await ctx.invoke(self.summon)
|
|
|
|
if not success:
|
|
|
|
return
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# If the queue is full, we ain't adding anything to it
|
2016-12-22 12:06:32 +13:00
|
|
|
if state.songs.full:
|
2016-07-29 04:59:12 +12:00
|
|
|
await self.bot.say("The queue is currently full! You'll need to wait to add a new song")
|
|
|
|
return
|
2016-08-10 14:13:53 +12:00
|
|
|
|
2016-08-07 00:35:59 +12:00
|
|
|
author_channel = ctx.message.author.voice.voice_channel
|
|
|
|
my_channel = ctx.message.server.me.voice.voice_channel
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# To try to avoid some abuse, ensure the requester is actually in our channel
|
2016-08-07 00:35:59 +12:00
|
|
|
if my_channel != author_channel:
|
|
|
|
await self.bot.say("You are not currently in the channel; please join before trying to request a song.")
|
|
|
|
return
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# Create the player, and check if this was successful
|
2016-08-21 17:36:38 +12:00
|
|
|
# Here all we want is to get the information of the player
|
2016-12-22 12:06:32 +13:00
|
|
|
song = re.sub('[<>\[\]]', '', song)
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-28 15:32:11 +12:00
|
|
|
try:
|
2016-12-23 09:57:32 +13:00
|
|
|
_entry, position = await state.songs.add_entry(song, ctx.message.author)
|
2016-12-22 12:06:32 +13:00
|
|
|
except WrongEntryTypeError:
|
|
|
|
# This means that a song was attempted to be searched, instead of a link provided
|
|
|
|
info = await self.downloader.extract_info(self.bot.loop, song, download=False, process=True)
|
2016-12-24 05:34:30 +13:00
|
|
|
try:
|
|
|
|
song = info.get('entries', [])[0]['webpage_url']
|
|
|
|
except IndexError:
|
|
|
|
await self.bot.send_message(ctx.message.channel, "No results found for {}!".format(song))
|
|
|
|
return
|
|
|
|
except ExtractionError as e:
|
|
|
|
# This gets the youtube_dl error, instead of our error raised
|
|
|
|
error = str(e).split("\n\n")[1]
|
|
|
|
# Youtube has a "fancy" colour error message it prints to the console
|
|
|
|
# Obviously this doesn't work in Discord, so just remove this
|
|
|
|
error = " ".join(error.split()[1:])
|
|
|
|
await self.bot.send_message(ctx.message.channel, error)
|
|
|
|
return
|
2016-12-28 14:55:50 +13:00
|
|
|
try:
|
|
|
|
_entry, position = await state.songs.add_entry(song, ctx.message.author)
|
|
|
|
except WrongEntryTypeError:
|
|
|
|
# This is either a playlist, or something not supported
|
|
|
|
fmt = "Sorry but I couldn't download that! Either you provided a playlist, a streamed link, or " \
|
|
|
|
"a page that is not supported to download."
|
|
|
|
await self.bot.send_message(ctx.message.channel, fmt)
|
2016-12-23 06:47:28 +13:00
|
|
|
except ExtractionError as e:
|
|
|
|
# This gets the youtube_dl error, instead of our error raised
|
|
|
|
error = str(e).split("\n\n")[1]
|
|
|
|
# Youtube has a "fancy" colour error message it prints to the console
|
|
|
|
# Obviously this doesn't work in Discord, so just remove this
|
|
|
|
error = " ".join(error.split()[1:])
|
2016-12-28 14:55:50 +13:00
|
|
|
# Make sure we are not over our 2000 message limit length (there are some youtube errors that are)
|
|
|
|
if len(error) >= 2000:
|
|
|
|
error = "{}...".format(error[:1996])
|
2016-12-23 06:47:28 +13:00
|
|
|
await self.bot.send_message(ctx.message.channel, error)
|
2016-12-23 09:57:32 +13:00
|
|
|
return
|
2016-12-22 12:06:32 +13:00
|
|
|
await self.bot.say('Enqueued ' + str(_entry))
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(kick_members=True)
|
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."""
|
|
|
|
|
|
|
|
state = self.get_voice_state(ctx.message.server)
|
2016-10-02 14:38:14 +13:00
|
|
|
if value is None:
|
|
|
|
volume = state.volume
|
|
|
|
await self.bot.say("Current volume is {}".format(volume))
|
|
|
|
return
|
2016-08-28 15:26:23 +12:00
|
|
|
if value > 200:
|
|
|
|
await self.bot.say("Sorry but the max volume is 200")
|
|
|
|
return
|
2016-09-01 04:58:59 +12:00
|
|
|
state.volume = value
|
2016-07-08 01:05:42 +12:00
|
|
|
if state.is_playing():
|
|
|
|
player = state.player
|
|
|
|
player.volume = value / 100
|
|
|
|
await self.bot.say('Set the volume to {:.0%}'.format(player.volume))
|
|
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(kick_members=True)
|
2016-07-08 01:05:42 +12:00
|
|
|
async def pause(self, ctx):
|
|
|
|
"""Pauses the currently played song."""
|
|
|
|
state = self.get_voice_state(ctx.message.server)
|
|
|
|
if state.is_playing():
|
2016-08-16 15:30:52 +12:00
|
|
|
state.player.pause()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(kick_members=True)
|
2016-07-08 01:05:42 +12:00
|
|
|
async def resume(self, ctx):
|
|
|
|
"""Resumes the currently played song."""
|
|
|
|
state = self.get_voice_state(ctx.message.server)
|
|
|
|
if state.is_playing():
|
2016-08-16 15:30:52 +12:00
|
|
|
state.player.resume()
|
2016-07-08 01:05:42 +12:00
|
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(kick_members=True)
|
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.
|
|
|
|
"""
|
|
|
|
server = ctx.message.server
|
|
|
|
state = self.get_voice_state(server)
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-09-06 14:52:14 +12:00
|
|
|
# Stop playing whatever song is playing.
|
2016-07-08 01:05:42 +12:00
|
|
|
if state.is_playing():
|
|
|
|
player = state.player
|
|
|
|
player.stop()
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# This will stop cancel the audio event we're using to loop through the queue
|
|
|
|
# Then erase the voice_state entirely, and disconnect from the channel
|
2016-07-08 01:05:42 +12:00
|
|
|
try:
|
|
|
|
state.audio_player.cancel()
|
|
|
|
del self.voice_states[server.id]
|
|
|
|
await state.voice.disconnect()
|
|
|
|
except:
|
|
|
|
pass
|
2016-08-10 14:13:53 +12:00
|
|
|
|
2016-08-07 01:09:24 +12:00
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
2016-08-07 01:09:24 +12:00
|
|
|
async def eta(self, ctx):
|
|
|
|
"""Provides an ETA on when your next song will play"""
|
2016-08-16 15:30:52 +12:00
|
|
|
# Note: There is no way to tell how long a song has been playing, or how long there is left on a song
|
|
|
|
# That is why this is called an "ETA"
|
2016-08-07 01:09:24 +12:00
|
|
|
state = self.get_voice_state(ctx.message.server)
|
|
|
|
author = ctx.message.author
|
2016-08-10 14:13:53 +12:00
|
|
|
|
2016-08-07 01:09:24 +12:00
|
|
|
if not state.is_playing():
|
|
|
|
await self.bot.say('Not playing any music right now...')
|
|
|
|
return
|
2016-12-22 12:06:32 +13:00
|
|
|
|
|
|
|
queue = state.songs.entries
|
2016-08-16 15:30:52 +12:00
|
|
|
if len(queue) == 0:
|
2016-08-07 01:09:24 +12:00
|
|
|
await self.bot.say("Nothing currently in the queue")
|
|
|
|
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
|
|
|
|
count = state.current.remaining
|
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
|
2016-12-22 12:06:32 +13:00
|
|
|
count += song.duration
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# This is checking if nothing from the queue has been added to the total
|
|
|
|
# If it has not, then we have not looped through the queue at all
|
|
|
|
# Since the queue was already checked to have more than one song in it, this means the author is next
|
2016-12-22 12:06:32 +13:00
|
|
|
if count == state.current.duration:
|
2016-08-07 01:09:24 +12:00
|
|
|
await self.bot.say("You are next in the queue!")
|
|
|
|
return
|
|
|
|
if not found:
|
|
|
|
await self.bot.say("You are not in the queue!")
|
|
|
|
return
|
2016-08-07 02:50:23 +12:00
|
|
|
await self.bot.say("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
|
|
|
|
2016-08-06 01:45:20 +12:00
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
2016-08-06 01:45:20 +12:00
|
|
|
async def queue(self, ctx):
|
|
|
|
"""Provides a printout of the songs that are in the queue"""
|
|
|
|
state = self.get_voice_state(ctx.message.server)
|
|
|
|
if not state.is_playing():
|
|
|
|
await self.bot.say('Not playing any music right now...')
|
|
|
|
return
|
2016-08-21 17:36:38 +12:00
|
|
|
|
2016-08-16 15:30:52 +12:00
|
|
|
# Asyncio provides no non-private way to access the queue, so we have to use _queue
|
2016-12-22 12:06:32 +13:00
|
|
|
queue = state.songs.entries
|
2016-08-16 15:30:52 +12:00
|
|
|
if len(queue) == 0:
|
2016-08-07 00:27:08 +12:00
|
|
|
fmt = "Nothing currently in the queue"
|
|
|
|
else:
|
2016-08-17 03:22:32 +12:00
|
|
|
fmt = "\n\n".join(str(x) for x in queue)
|
2016-08-06 01:45:20 +12:00
|
|
|
await self.bot.say("Current songs in the queue:```\n{}```".format(fmt))
|
2016-07-09 13:27:19 +12:00
|
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
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"""
|
2016-07-10 00:57:25 +12:00
|
|
|
await self.bot.say("There are a total of {} songs in the queue"
|
2016-12-22 12:06:32 +13:00
|
|
|
.format(len(self.get_voice_state(ctx.message.server).songs.entries)))
|
2016-07-31 12:20:55 +12:00
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
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
|
|
|
"""
|
|
|
|
|
|
|
|
state = self.get_voice_state(ctx.message.server)
|
|
|
|
if not state.is_playing():
|
|
|
|
await self.bot.say('Not playing any music right now...')
|
|
|
|
return
|
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
|
|
|
|
if voter == state.current.requester:
|
|
|
|
await self.bot.say('Requester requested skipping song...')
|
|
|
|
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:
|
2016-07-08 01:05:42 +12:00
|
|
|
await self.bot.say('Skip vote passed, skipping song...')
|
|
|
|
state.skip()
|
|
|
|
else:
|
2016-08-20 16:00:23 +12:00
|
|
|
await self.bot.say('Skip vote added, currently at [{}/{}]'.format(total_votes, state.required_skips))
|
2016-07-08 01:05:42 +12:00
|
|
|
else:
|
|
|
|
await self.bot.say('You have already voted to skip this song.')
|
2016-07-31 12:20:55 +12:00
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(kick_members=True)
|
2016-07-08 01:05:42 +12:00
|
|
|
async def modskip(self, ctx):
|
|
|
|
"""Forces a song skip, can only be used by a moderator"""
|
|
|
|
state = self.get_voice_state(ctx.message.server)
|
|
|
|
if not state.is_playing():
|
|
|
|
await self.bot.say('Not playing any music right now...')
|
|
|
|
return
|
2016-07-31 12:20:55 +12:00
|
|
|
|
2016-07-08 01:05:42 +12:00
|
|
|
state.skip()
|
|
|
|
await self.bot.say('Song has just been skipped.')
|
|
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
2016-08-15 14:10:12 +12:00
|
|
|
@checks.custom_perms(send_messages=True)
|
2016-07-08 01:05:42 +12:00
|
|
|
async def playing(self, ctx):
|
|
|
|
"""Shows info about the currently played song."""
|
|
|
|
|
|
|
|
state = self.get_voice_state(ctx.message.server)
|
2016-08-06 23:55:32 +12:00
|
|
|
if not state.is_playing():
|
2016-07-08 01:05:42 +12:00
|
|
|
await self.bot.say('Not playing anything.')
|
|
|
|
else:
|
|
|
|
skip_count = len(state.skip_votes)
|
2016-08-20 16:00:23 +12:00
|
|
|
await self.bot.say('Now playing {} [skips: {}/{}]'.format(state.current, skip_count, state.required_skips))
|
2016-07-09 13:27:19 +12:00
|
|
|
|
|
|
|
|
|
|
|
def setup(bot):
|
|
|
|
bot.add_cog(Music(bot))
|