1
0
Fork 0
mirror of synced 2024-05-05 13:12:34 +12:00

remove old files/remove music

This commit is contained in:
phxntxm 2018-04-24 17:48:25 -05:00
parent 3f7eaaa248
commit 465c9fa316
10 changed files with 0 additions and 2209 deletions

View file

@ -1,979 +0,0 @@
from .voice_utilities import *
from discord import PCMVolumeTransformer
import discord
from discord.ext import commands
from . import utils
import math
import asyncio
import time
import re
import logging
import random
from collections import deque
log = logging.getLogger()
if not discord.opus.is_loaded():
discord.opus.load_opus('/usr/lib64/libopus.so.0')
class VoiceState:
def __init__(self, guild, bot, user_queue=False, volume=None):
self.guild = guild
self.songs = Playlist(bot)
self.djs = deque()
self.dj = None
self.current = None
self.required_skips = 0
self.skip_votes = set()
self.user_queue = user_queue
self.loop = bot.loop
self._volume = volume or .5
self.live = False
@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
@property
def voice(self):
return self.guild.voice_client
@property
def playing(self):
if self.voice is None:
return False
else:
return self.voice.is_playing() or self.voice.is_paused()
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
def skip(self):
self.skip_votes.clear()
if self.voice:
self.voice.stop()
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()
async def play_next_song(self):
if self.playing or not self.voice:
return
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()
if self.playing or not self.voice:
return
if self.current:
# Transform our source into a volume source
source = PCMVolumeTransformer(self.current, volume=self.volume)
self.voice.play(source, after=self.after)
self.current.start_time = time.time()
# 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
async def next_song(self):
if not self.user_queue:
self.current = await self.songs.next_entry()
else:
try:
dj = self.djs.popleft()
except IndexError:
self.dj = None
self.current = None
else:
song = await dj.next_entry()
# 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
if song is None:
return await self.next_song()
else:
song.requester = dj.member
self.dj = dj
self.current = song
class Music:
"""Voice related commands.
Works in multiple servers at once.
"""
def __init__(self, bot):
self.bot = bot
self.voice_states = {}
down = Downloader(download_folder='audio_tmp')
self.downloader = down
self.bot.downloader = down
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
async def queue_embed_task(self, state, channel, author):
index = 0
message = None
fmt = None
possible_reactions = ['\u27A1', '\u2B05', '\u2b06', '\u2b07', '\u274c', '\u23ea', '\u23e9']
# 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
while True:
# Get the current queue (It might change while we're doing this)
# So do this in the while loop
if state.user_queue:
queue = state.djs
else:
queue = state.songs.entries
count = len(queue)
# This means the last song was removed
if count == 0:
await channel.send("Nothing currently in the queue")
break
# Get the current entry
entry = queue[index]
dj = None
if state.user_queue:
dj = entry
entry = entry.peek()
# Get the entry's embed
embed = entry.embed
# Set the embed's title to indicate the amount of things in the queue
count = len(queue)
embed.title = "Current Queue [{}/{}]".format(index + 1, count)
# 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:
await message.edit(content=fmt, embed=embed)
# 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
if not author.guild_permissions.mute_members and author.id != entry.requester.id:
try:
await message.remove_reaction('\u274c', channel.server.me)
except:
pass
elif not author.guild_permissions.mute_members and author.id == entry.requester.id:
try:
await message.add_reaction('\u274c')
except:
pass
else:
message = await channel.send(embed=embed)
await message.add_reaction('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}')
await message.add_reaction('\N{LEFTWARDS BLACK ARROW}')
await message.add_reaction('\N{BLACK RIGHTWARDS ARROW}')
await message.add_reaction('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}')
# The moderation tools that can be used
if author.guild_permissions.mute_members:
await message.add_reaction('\N{DOWNWARDS BLACK ARROW}')
await message.add_reaction('\N{UPWARDS BLACK ARROW}')
await message.add_reaction('\N{CROSS MARK}')
elif author == entry.requester:
await message.add_reaction('\N{CROSS MARK}')
# Reset the fmt message
fmt = "\u200B"
# Now we wait for the next reaction
try:
reaction, user = await self.bot.wait_for('reaction_add', check=check, timeout=180)
except asyncio.TimeoutError:
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
if author.guild_permissions.mute_members and index > 0:
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]:
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
if state.user_queue:
queue.insert(index - 1, dj)
else:
queue.insert(index - 1, entry)
# 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
if author.guild_permissions.mute_members and index < (count - 1):
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]:
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
if state.user_queue:
queue.insert(index + 1, dj)
else:
queue.insert(index + 1, entry)
# 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
if author.guild_permissions.mute_members or author == entry.requester:
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]:
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
# If first is clicked
elif '\u23ea':
index = 0
# If last is clicked
elif '\u23e9':
index = count - 1
try:
await message.remove_reaction(reaction.emoji, user)
except discord.Forbidden:
pass
await message.delete()
# noinspection PyUnusedLocal
async def on_voice_state_update(self, _, __, after):
if after is None or after.channel is None:
return
state = self.voice_states.get(after.channel.guild.id)
if state is None or state.voice is None or state.voice.channel is None:
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]
entry = await state.songs.add_entry(song)
if not state.playing:
self.bot.loop.create_task(state.play_next_song())
entry.requester = ctx.message.author
return entry
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)
async def join_channel(self, channel, text_channel):
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:
msg = await text_channel.send("Trying to join channel {}...".format(channel.name))
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(reconnect=False)
# If we have connnected, create our voice state
queue_type = self.bot.db.load('server_settings', key=channel.guild.id, pluck='queue_type')
volume = self.bot.db.load('server_settings', key=channel.guild.id, pluck='volume')
user_queue = queue_type == "user"
self.voice_states[channel.guild.id] = VoiceState(channel.guild, self.bot, user_queue=user_queue,
volume=volume)
# If we can send messages, edit it to let the channel know we have succesfully joined
if msg:
try:
await msg.edit(content="Ready to play audio in channel {}".format(channel.name))
except discord.NotFound:
pass
return True
# If we time out trying to join, just let them know and return False
except (asyncio.TimeoutError, OSError):
if msg:
try:
await msg.edit(content="Sorry, but I couldn't connect right now! Please try again later")
except discord.NotFound:
pass
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
await text_channel.send("Sorry but I couldn't connect...try again?")
return False
@commands.command()
@commands.guild_only()
@utils.custom_perms(send_messages=True)
@utils.check_restricted()
async def progress(self, ctx):
"""Provides the progress of the current song"""
# Make sure we're playing first
state = self.voice_states.get(ctx.message.guild.id)
if state is None or not state.playing or state.current is None:
await ctx.send('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 ctx.send('Not playing anything.')
return
# Otherwise just format this nicely
progress = divmod(round(progress, 0), 60)
length = divmod(round(length, 0), 60)
fmt = "Current song progress: {0[0]}m {0[1]}s/{1[0]}m {1[1]}s".format(progress, length)
await ctx.send(fmt)
@commands.command(aliases=['summon'])
@commands.guild_only()
@utils.custom_perms(send_messages=True)
@utils.check_restricted()
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
perms = channel.permissions_for(ctx.message.guild.me)
if not perms.connect or not perms.speak or not perms.use_voice_activation:
await ctx.send("I do not have correct permissions in {}! Please turn on `connect`, `speak`, and `use "
"voice activation`".format(channel.name))
return False
return await self.join_channel(channel, ctx.channel)
@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'):
await ctx.send("You cannot import playlists at this time; the {}allowplaylists command can be used to "
"change this setting".format(ctx.prefix))
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)
# 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))
@commands.command()
@commands.guild_only()
@utils.custom_perms(send_messages=True)
@utils.check_restricted()
async def play(self, ctx, *, song: str):
"""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
"""
# 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 this is a user queue, this is the wrong command
if state and 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
# 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
# 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)
if len(song) == 11:
# Youtube-dl will attempt to use results with a length of 11 as a video ID
# 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 += "."
try:
msg = await ctx.send("Looking up {}...".format(song))
except:
msg = None
try:
entry = await self.add_entry(song, ctx)
# 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!")
except LiveStreamError as e:
await ctx.send(str(e))
except WrongEntryTypeError:
await ctx.send("Please use the {}import command to import a playlist.".format(ctx.prefix))
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("ERROR: ", "")
else:
# This happens when the download just returns `None`
error = error[0]
await ctx.send(error)
else:
try:
if entry is None:
await ctx.send("Sorry but I couldn't download/find {}".format(song))
else:
embed = entry.embed
embed.title = "Enqueued song!"
try:
await msg.edit(content=None, embed=embed)
except:
await ctx.send(embed=embed)
except discord.Forbidden:
pass
@commands.command()
@commands.guild_only()
@utils.custom_perms(mute_members=True)
@utils.check_restricted()
async def volume(self, ctx, value: int = None):
"""Sets the volume of the currently playing song."""
state = self.voice_states.get(ctx.message.guild.id)
if value:
value /= 100
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")
elif value is None:
await ctx.send('Current volume is {:.0%}'.format(state.volume))
elif value > 1.0:
await ctx.send("Sorry but the max volume is 100%")
else:
state.volume = value
entry = {'server_id': str(ctx.message.guild.id), 'volume': value}
self.bot.db.save('server_settings', entry)
await ctx.send('Set the volume to {:.0%}'.format(state.volume))
@commands.command()
@commands.guild_only()
@utils.custom_perms(mute_members=True)
@utils.check_restricted()
async def pause(self, ctx):
"""Pauses the currently played song."""
state = self.voice_states.get(ctx.message.guild.id)
if state and state.voice and state.voice.is_connected():
state.voice.pause()
@commands.command()
@commands.guild_only()
@utils.custom_perms(mute_members=True)
@utils.check_restricted()
async def resume(self, ctx):
"""Resumes the currently played song."""
state = self.voice_states.get(ctx.message.guild.id)
if state and state.voice and state.voice.is_connected():
state.voice.resume()
@commands.command()
@commands.guild_only()
@utils.custom_perms(mute_members=True)
@utils.check_restricted()
async def stop(self, ctx):
"""Stops playing audio and leaves the voice channel.
This also clears the queue.
"""
state = self.voice_states.get(ctx.message.guild.id)
voice = ctx.message.guild.voice_client
# If we have a state, clear the songs, dj's, then skip the current song
if state:
state.songs.clear()
state.djs.clear()
state.skip()
try:
del self.voice_states[ctx.message.guild.id]
except KeyError:
pass
# If we have a voice connection (separate from state...just in case....)
# Then stop playing, and disconnect
if voice:
voice.stop()
await voice.disconnect(force=True)
# Temporarily disabling what's after this
# 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:
# 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
# else:
# 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 = self.bot.get_guild(ctx.message.guild.id)
# if guild.voice_client:
# await guild.voice_client.disconnect(force=True)
@commands.command()
@commands.guild_only()
@utils.custom_perms(send_messages=True)
@utils.check_restricted()
async def eta(self, ctx):
"""Provides an ETA on when your next song will play"""
state = self.voice_states.get(ctx.message.guild.id)
author = ctx.message.author
if state is None or not state.playing:
await ctx.send('Not playing any music right now...')
return
if state.user_queue:
queue = [x.peek() for x in state.djs if x.peek()]
else:
queue = state.songs.entries
if len(queue) == 0:
await ctx.send("Nothing currently in the queue")
return
# Start off by adding the remaining length of the current song
count = state.current.remaining or 0
found = False
# 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:
if song.requester == author:
found = True
break
count += song.length
if not found:
await ctx.send("You are not in the queue!")
return
await ctx.send("ETA till your next play is: {0[0]}m {0[1]}s".format(divmod(round(count, 0), 60)))
@commands.command()
@commands.guild_only()
@utils.custom_perms(send_messages=True)
@utils.check_restricted()
async def queue(self, ctx):
"""Provides a printout of the songs that are in the queue"""
state = self.voice_states.get(ctx.message.guild.id)
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()]
else:
_queue = state.songs.entries
if len(_queue) == 0:
await ctx.send("Nothing currently in the queue")
else:
self.bot.loop.create_task(self.queue_embed_task(state, ctx.message.channel, ctx.message.author))
@commands.command()
@commands.guild_only()
@utils.custom_perms(send_messages=True)
@utils.check_restricted()
async def queuelength(self, ctx):
"""Prints the length of the queue"""
state = self.voice_states.get(ctx.message.guild.id)
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()]
else:
_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)))
@commands.command()
@commands.guild_only()
@utils.custom_perms(send_messages=True)
@utils.check_restricted()
async def skip(self, ctx):
"""Vote to skip a song. The song requester can automatically skip.
approximately 1/3 of the members in the voice channel
are required to vote to skip for the song to be skipped.
"""
state = self.voice_states.get(ctx.message.guild.id)
if state is None or not state.playing:
await ctx.send('Not playing any music right now...')
return
# 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!")
# Check if the person requesting a skip is the requester of the song, if so automatically skip
voter = ctx.message.author
if hasattr(state.current, 'requester') and voter == state.current.requester:
await ctx.send('Requester requested skipping song...')
state.skip()
# Otherwise check if the voter has already voted
elif voter.id not in state.skip_votes:
state.skip_votes.add(voter.id)
total_votes = len(state.skip_votes)
# Now check how many votes have been made, if 3 then go ahead and skip, otherwise add to the list of votes
if total_votes >= state.required_skips:
await ctx.send('Skip vote passed, skipping song...')
state.skip()
else:
await ctx.send('Skip vote added, currently at [{}/{}]'.format(total_votes, state.required_skips))
else:
await ctx.send('You have already voted to skip this song.')
@commands.command()
@commands.guild_only()
@utils.custom_perms(mute_members=True)
@utils.check_restricted()
async def modskip(self, ctx):
"""Forces a song skip, can only be used by a moderator"""
state = self.voice_states.get(ctx.message.guild.id)
if state is None:
await ctx.send('Not playing any music right now...')
return
state.skip()
await ctx.send('Song has just been skipped.')
@commands.command()
@commands.guild_only()
@utils.custom_perms(send_messages=True)
@utils.check_restricted()
async def playing(self, ctx):
"""Shows info about the currently played song."""
state = self.voice_states.get(ctx.message.guild.id)
if state is None or not state.playing or not state.current:
await ctx.send('Not playing anything.')
else:
# 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)
if state.current.requester:
embed.add_field(name='Requester', value=state.current.requester.display_name, inline=False)
# Get the amount of current skips, and display how many have been skipped/how many required
skip_count = len(state.skip_votes)
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
progress = state.current.progress
if length and progress:
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
await ctx.send(embed=embed)
@commands.command()
@commands.guild_only()
@utils.custom_perms(send_messages=True)
@utils.check_restricted()
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)
if state and not state.user_queue:
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
# 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
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]
if not new_dj.peek():
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")
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
if not state.playing:
await state.play_next_song()
@commands.command()
@commands.guild_only()
@utils.custom_perms(mute_members=True)
@utils.check_restricted()
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!")
@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
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("ERROR: ", "")
else:
# This happens when the download just returns `None`
error = error[0]
await ctx.send(error)
return
# They may have removed the bot from the channel during this time, so lets check again
if state.voice is None:
await ctx.send("Failed to join the channel...")
return
# 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...")
def setup(bot):
bot.add_cog(Music(bot))

View file

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

View file

@ -1,4 +0,0 @@
from .downloader import Downloader
from .exceptions import *
from .playlist import Playlist
from .source import *

View file

@ -1,92 +0,0 @@
import os
import asyncio
import functools
import youtube_dl
import discord
from .. import utils
from concurrent.futures import ThreadPoolExecutor
ytdl_format_options = {
'format': 'bestaudio/best',
'extractaudio': True,
'audioformat': 'mp3',
'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',
'restrictfilenames': True,
'noplaylist': True,
'nocheckcertificate': True,
'ignoreerrors': False,
'logtostderr': False,
'quiet': True,
'no_warnings': True,
'default_search': 'ytsearch',
'proxy': utils.ytdl_proxy,
'source_address': '0.0.0.0'
}
# Fuck your useless bugreports message that gets two link embeds and confuses users
youtube_dl.utils.bug_reports_message = lambda: ''
'''
Alright, here's the problem. To catch youtube-dl errors for their useful information, I have to
catch the exceptions with `ignoreerrors` off. To not break when ytdl hits a dumb video
(rental videos, etc), I have to have `ignoreerrors` on. I can change these whenever, but with async
that's bad. So I need multiple ytdl objects.
'''
class Downloader:
def __init__(self, download_folder=None):
self.thread_pool = ThreadPoolExecutor(max_workers=2)
self.unsafe_ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
self.safe_ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
self.safe_ytdl.params['ignoreerrors'] = True
self.download_folder = download_folder
if download_folder:
otmpl = self.unsafe_ytdl.params['outtmpl']
self.unsafe_ytdl.params['outtmpl'] = os.path.join(download_folder, otmpl)
# print("setting template to " + os.path.join(download_folder, otmpl))
otmpl = self.safe_ytdl.params['outtmpl']
self.safe_ytdl.params['outtmpl'] = os.path.join(download_folder, otmpl)
@property
def ytdl(self):
return self.safe_ytdl
async def extract_info(self, loop, *args, on_error=None, retry_on_error=False, **kwargs):
"""
Runs ytdl.extract_info within the threadpool. Returns a future that will fire when it's done.
If `on_error` is passed and an exception is raised, the exception will be caught and passed to
on_error as an argument.
"""
if callable(on_error):
try:
return await loop.run_in_executor(self.thread_pool,
functools.partial(self.unsafe_ytdl.extract_info, *args, **kwargs))
except Exception as e:
# (youtube_dl.utils.ExtractorError, youtube_dl.utils.DownloadError)
# I hope I don't have to deal with ContentTooShortError's
if asyncio.iscoroutinefunction(on_error):
asyncio.ensure_future(on_error(e), loop=loop)
elif asyncio.iscoroutine(on_error):
asyncio.ensure_future(on_error, loop=loop)
else:
loop.call_soon_threadsafe(on_error, e)
if retry_on_error:
return await self.safe_extract_info(loop, *args, **kwargs)
else:
return await loop.run_in_executor(self.thread_pool,
functools.partial(self.unsafe_ytdl.extract_info, *args, **kwargs))
async def safe_extract_info(self, loop, *args, **kwargs):
return await loop.run_in_executor(self.thread_pool,
functools.partial(self.safe_ytdl.extract_info, *args, **kwargs))

View file

@ -1,279 +0,0 @@
import asyncio
import json
import os
import traceback
import time
import aiohttp
from discord import Embed
from hashlib import md5
from .exceptions import ExtractionError
async def get_header(url, headerfield=None, *, timeout=5):
with aiohttp.Timeout(timeout):
async with aiohttp.ClientSession().head(url) as response:
if headerfield:
return response.headers.get(headerfield)
else:
return response.headers
def md5sum(filename, limit=0):
fhash = md5()
with open(filename, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
fhash.update(chunk)
return fhash.hexdigest()[-limit:]
class BasePlaylistEntry:
def __init__(self):
self.filename = None
self._is_downloading = False
self._waiting_futures = []
@property
def is_downloaded(self):
if self._is_downloading:
return False
return bool(self.filename)
@classmethod
def from_json(cls, playlist, jsonstring):
raise NotImplementedError
def to_json(self):
raise NotImplementedError
async def _download(self):
raise NotImplementedError
def get_ready_future(self):
"""
Returns a future that will fire when the song is ready to be played. The future will either fire with the
result (being the entry) or an exception as to why the song download failed.
"""
future = asyncio.Future()
if self.is_downloaded:
# In the event that we're downloaded, we're already ready for playback.
future.set_result(self)
else:
# If we request a ready future, let's ensure that it'll actually resolve at one point.
asyncio.ensure_future(self._download())
self._waiting_futures.append(future)
return future
def _for_each_future(self, cb):
"""
Calls `cb` for each future that is not cancelled. Absorbs and logs any errors that may have occurred.
"""
futures = self._waiting_futures
self._waiting_futures = []
for future in futures:
if future.cancelled():
continue
try:
cb(future)
except:
traceback.print_exc()
def __eq__(self, other):
return self is other
def __hash__(self):
return id(self)
class URLPlaylistEntry(BasePlaylistEntry):
def __init__(self, playlist, url, title, thumbnail, duration=0, expected_filename=None, **meta):
super().__init__()
self.playlist = playlist
self.url = url
self.title = title
self.duration = duration
self.thumbnail = thumbnail
self.expected_filename = expected_filename
self.meta = meta
self.download_folder = self.playlist.downloader.download_folder
def __str__(self):
fmt = '*{0}* requested by **{1.display_name}**'
if self.duration:
fmt += ' [length: {0[0]}m {0[1]}s]'.format(divmod(round(self.duration, 0), 60))
return fmt.format(self.title, self.requester)
@property
def length(self):
if self.duration:
return self.duration
@property
def progress(self):
if hasattr(self, 'start_time') and self.start_time:
return round(time.time() - self.start_time)
@property
def remaining(self):
length = self.length
progress = self.progress
if length and progress:
return length - progress
@classmethod
def from_json(cls, playlist, jsonstring):
data = json.loads(jsonstring)
# TODO: version check
url = data['url']
title = data['title']
duration = data['duration']
downloaded = data['downloaded']
filename = data['filename'] if downloaded else None
meta = {}
# TODO: Better [name] fallbacks
if 'channel' in data['meta']:
ch = playlist.bot.get_channel(data['meta']['channel']['id'])
meta['channel'] = ch or data['meta']['channel']['name']
if 'author' in data['meta']:
meta['author'] = meta['channel'].guild.get_member(data['meta']['author']['id'])
return cls(playlist, url, title, duration, filename, **meta)
def to_json(self):
data = {
'version': 1,
'type': self.__class__.__name__,
'url': self.url,
'title': self.title,
'duration': self.duration,
'downloaded': self.is_downloaded,
'filename': self.filename,
'meta': {
i: {
'type': self.meta[i].__class__.__name__,
'id': self.meta[i].id,
'name': self.meta[i].name
} for i in self.meta
}
# Actually I think I can just getattr instead, getattr(discord, type)
}
return json.dumps(data, indent=2)
# noinspection PyTypeChecker
async def _download(self):
if self._is_downloading:
return
self._is_downloading = True
try:
# Ensure the folder that we're going to move into exists.
if not os.path.exists(self.download_folder):
os.makedirs(self.download_folder)
# self.expected_filename: audio_cache\youtube-9R8aSKwTEMg-NOMA_-_Brain_Power.m4a
extractor = os.path.basename(self.expected_filename).split('-')[0]
# the generic extractor requires special handling
if extractor == 'generic':
flistdir = [f.rsplit('-', 1)[0] for f in os.listdir(self.download_folder)]
expected_fname_noex, fname_ex = os.path.basename(self.expected_filename).rsplit('.', 1)
if expected_fname_noex in flistdir:
try:
rsize = int(await get_header(self.playlist.bot.aiosession, self.url, 'CONTENT-LENGTH'))
except:
rsize = 0
lfile = os.path.join(
self.download_folder,
os.listdir(self.download_folder)[flistdir.index(expected_fname_noex)]
)
lsize = os.path.getsize(lfile)
if lsize != rsize:
await self._really_download(hash=True)
else:
self.filename = lfile
else:
await self._really_download(hash=True)
else:
ldir = os.listdir(self.download_folder)
flistdir = [f.rsplit('.', 1)[0] for f in ldir]
expected_fname_base = os.path.basename(self.expected_filename)
expected_fname_noex = expected_fname_base.rsplit('.', 1)[0]
# idk wtf this is but its probably legacy code
# or i have youtube to blame for changing shit again
if expected_fname_base in ldir:
self.filename = os.path.join(self.download_folder, expected_fname_base)
elif expected_fname_noex in flistdir:
self.filename = os.path.join(self.download_folder, ldir[flistdir.index(expected_fname_noex)])
else:
await self._really_download()
# Trigger ready callbacks.
self._for_each_future(lambda future: future.set_result(self))
except Exception as e:
traceback.print_exc()
self._for_each_future(lambda future: future.set_exception(e))
finally:
self._is_downloading = False
# noinspection PyShadowingBuiltins
async def _really_download(self, *, hash=False):
try:
result = await self.playlist.downloader.extract_info(self.playlist.loop, self.url, download=True)
except Exception as e:
raise ExtractionError(e)
if result is None:
raise ExtractionError("ytdl broke and hell if I know why")
# What the fuck do I do now?
self.filename = unhashed_fname = self.playlist.downloader.ytdl.prepare_filename(result)
if hash:
# insert the 8 last characters of the file hash to the file name to ensure uniqueness
self.filename = md5sum(unhashed_fname, 8).join('-.').join(unhashed_fname.rsplit('.', 1))
if os.path.isfile(self.filename):
# Oh bother it was actually there.
os.unlink(unhashed_fname)
else:
# Move the temporary file to it's final location.
os.rename(unhashed_fname, self.filename)
def to_embed(self):
"""Returns an embed that can be used to display information about this particular song"""
# Create the embed object we'll use
embed = Embed()
# Fill in the simple things
embed.add_field(name='Title', value=self.title, inline=False)
embed.add_field(name='Requester', value=self.requester.display_name, inline=False)
if self.thumbnail:
embed.set_thumbnail(url=self.thumbnail)
# Get the current length of the song and display this
if self.length:
length = divmod(round(self.length, 0), 60)
fmt = "{0[0]}m {0[1]}s".format(length)
embed.add_field(name='Duration', value=fmt, inline=False)
# And return the embed we created
return embed

View file

@ -1,38 +0,0 @@
import asyncio
import traceback
import collections
class EventEmitter:
def __init__(self):
self._events = collections.defaultdict(list)
self.loop = asyncio.get_event_loop()
def emit(self, event, *args, **kwargs):
if event not in self._events:
return
for cb in self._events[event]:
# noinspection PyBroadException
try:
if asyncio.iscoroutinefunction(cb):
asyncio.ensure_future(cb(*args, **kwargs), loop=self.loop)
else:
cb(*args, **kwargs)
except:
traceback.print_exc()
def on(self, event, cb):
self._events[event].append(cb)
return self
def off(self, event, cb):
self._events[event].remove(cb)
if not self._events[event]:
del self._events[event]
return self
# TODO: add .once

View file

@ -1,102 +0,0 @@
import shutil
import textwrap
# Base class for exceptions
class MusicbotException(Exception):
def __init__(self, message, *, expire_in=0):
self._message = message
self.expire_in = expire_in
@property
def message(self):
return self._message
@property
def message_no_format(self):
return self._message
# Something went wrong during the processing of a command
class CommandError(MusicbotException):
pass
# Something went wrong during the processing of a song/ytdl stuff
class ExtractionError(MusicbotException):
pass
# Live stream's cannot be downloaded
class LiveStreamError(MusicbotException):
pass
# The no processing entry type failed and an entry was a playlist/vice versa
class WrongEntryTypeError(ExtractionError):
def __init__(self, message, is_playlist, use_url):
super().__init__(message)
self.is_playlist = is_playlist
self.use_url = use_url
# The user doesn't have permission to use a command
class PermissionsError(CommandError):
@property
def message(self):
return "You don't have permission to use that command.\nReason: " + self._message
# Error with pretty formatting for hand-holding users through various errors
class HelpfulError(MusicbotException):
def __init__(self, issue, solution, *, preface="An error has occured:\n", expire_in=0):
self.issue = issue
self.solution = solution
self.preface = preface
self.expire_in = expire_in
@property
def message(self):
return ("\n{}\n{}\n{}\n").format(
self.preface,
self._pretty_wrap(self.issue, " Problem: "),
self._pretty_wrap(self.solution, " Solution: "))
@property
def message_no_format(self):
return "\n{}\n{}\n{}\n".format(
self.preface,
self._pretty_wrap(self.issue, " Problem: ", width=None),
self._pretty_wrap(self.solution, " Solution: ", width=None))
@staticmethod
def _pretty_wrap(text, pretext, *, width=-1):
if width is None:
return pretext + text
elif width == -1:
width = shutil.get_terminal_size().columns
l1, *lx = textwrap.wrap(text, width=width - 1 - len(pretext))
lx = [((' ' * len(pretext)) + l).rstrip().ljust(width) for l in lx]
l1 = (pretext + l1).ljust(width)
return ''.join([l1, *lx])
class HelpfulWarning(HelpfulError):
pass
# Base class for control signals
class Signal(Exception):
pass
# signal to restart the bot
class RestartSignal(Signal):
pass
# signal to end the bot "gracefully"
class TerminateSignal(Signal):
pass

View file

@ -1,187 +0,0 @@
import datetime
import traceback
import asyncio
from collections import deque
from itertools import islice
from random import shuffle
from .source import YoutubeDLSource
from .entry import URLPlaylistEntry, get_header
from .exceptions import ExtractionError, WrongEntryTypeError, LiveStreamError
from .event_emitter import EventEmitter
class Playlist(EventEmitter):
"""
A playlist is manages the list of songs that will be played.
"""
def __init__(self, bot):
super().__init__()
self.bot = bot
self.loop = bot.loop
self.downloader = bot.downloader
self.entries = deque()
def __iter__(self):
return iter(self.entries)
def shuffle(self):
shuffle(self.entries)
def clear(self):
self.entries.clear()
@property
def count(self):
if self.entries:
return len(self.entries)
else:
return 0
async def add_entry(self, song_url, **meta):
"""Adds a song to this playlist"""
entry = YoutubeDLSource(self, song_url)
await entry.prepare()
self.entries.append(entry)
return entry
async def import_from(self, playlist_url, requester):
"""
Imports the songs from `playlist_url` and queues them to be played.
Returns a list of `entries` that have been enqueued.
"""
try:
info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False)
except Exception as e:
raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))
if not info:
raise ExtractionError('Could not extract information from %s' % playlist_url)
if info.get('playlist') is None and 'playlist' not in info.get('extractor', ''):
raise WrongEntryTypeError('This is not a playlist!', False, playlist_url)
# Once again, the generic extractor fucks things up.
if info.get('extractor', None) == 'generic':
url_field = 'url'
else:
url_field = 'webpage_url'
yield len(info['entries'])
for items in info['entries']:
if items:
entry = YoutubeDLSource(self, items[url_field])
try:
await entry.prepare()
except:
yield False
else:
entry.requester = requester
self.entries.append(entry)
yield True
else:
yield False
async def async_process_youtube_playlist(self, playlist_url, **meta):
"""
Processes youtube playlists links from `playlist_url` in a questionable, async fashion.
:param playlist_url: The playlist url to be cut into individual urls and added to the playlist
:param meta: Any additional metadata to add to the playlist entry
"""
try:
info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False, process=False)
except Exception as e:
raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))
if not info:
raise ExtractionError('Could not extract information from %s' % playlist_url)
gooditems = []
baditems = 0
for entry_data in info['entries']:
if entry_data:
baseurl = info['webpage_url'].split('playlist?list=')[0]
song_url = baseurl + 'watch?v=%s' % entry_data['id']
try:
entry, elen = await self.add_entry(song_url, **meta)
gooditems.append(entry)
except ExtractionError:
baditems += 1
except Exception as e:
baditems += 1
else:
baditems += 1
return gooditems
async def async_process_sc_bc_playlist(self, playlist_url, **meta):
"""
Processes soundcloud set and bancdamp album links from `playlist_url` in a questionable, async fashion.
:param playlist_url: The playlist url to be cut into individual urls and added to the playlist
:param meta: Any additional metadata to add to the playlist entry
"""
try:
info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False, process=False)
except Exception as e:
raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))
if not info:
raise ExtractionError('Could not extract information from %s' % playlist_url)
gooditems = []
baditems = 0
for entry_data in info['entries']:
if entry_data:
song_url = entry_data['url']
try:
entry, elen = await self.add_entry(song_url, **meta)
gooditems.append(entry)
except ExtractionError:
baditems += 1
except Exception as e:
baditems += 1
else:
baditems += 1
return gooditems
def _add_entry(self, entry):
self.entries.append(entry)
async def next_entry(self):
"""Get the next song in the playlist; this class will wait until the next song is ready"""
entry = self.peek()
# While we have an entry available
while entry:
# Check if we are ready or if we've errored, either way we'll pop it from the deque
if entry.ready or entry.error:
return self.entries.popleft()
# Otherwise, wait a second and check again
else:
await asyncio.sleep(1)
# "Refresh" the next entry, in case someone cleared the next song in the queue
entry = self.peek()
# If we've reached here, we have no entries
return None
def peek(self):
"""
Returns the next entry that should be scheduled to be played.
"""
if self.entries:
return self.entries[0]
def count_for_user(self, user):
return sum(1 for e in self.entries if e.meta.get('author', None) == user)

View file

@ -1,171 +0,0 @@
import discord
import time
import asyncio
from .exceptions import ExtractionError, WrongEntryTypeError, LiveStreamError
from .entry import get_header
class YoutubeDLSource(discord.FFmpegPCMAudio):
def __init__(self, playlist, url):
self._process = None
self.playlist = playlist
self.loop = playlist.loop
self.downloader = playlist.downloader
self.url = url
self.info = None
self.ready = False
self.error = False
async def get_info(self):
try:
# First attempt to gather the information
info = await self.downloader.extract_info(self.loop, self.url, download=False)
except Exception as e:
raise ExtractionError('Could not extract information from {}\n\n{}'.format(self.url, e))
# Check if a playlist was provided
if info.get('_type', None) == 'playlist':
# It is possible that the 'playlist' is the search
if info.get('extractor') == 'youtube:search':
# If so, and we have no entries, then nothing with this search was found
if len(info['entries']) == 0:
raise ExtractionError('Could not extract information from %s' % self.url)
# Otherwise get the first result
else:
info = info['entries'][0]
# If this isn't a search, then it is a playlist, this can't be done
else:
raise WrongEntryTypeError("This is a playlist.", True,
info.get('webpage_url', None) or info.get('url', None))
if info['extractor'] in ['generic', 'Dropbox']:
try:
# unfortunately this is literally broken
# https://github.com/KeepSafe/aiohttp/issues/758
# https://github.com/KeepSafe/aiohttp/issues/852
headers = await get_header(info['url'])
content_type = headers.get('Content-Type')
except Exception as e:
content_type = None
if content_type:
if content_type.startswith(('application/', 'image/')):
if '/ogg' not in content_type: # How does a server say `application/ogg` what the actual fuck
raise ExtractionError("Invalid content type \"%s\" for url %s" % (content_type, self.url))
if headers.get('ice-audio-info'):
raise LiveStreamError("Cannot download from a livestream")
if info.get('is_live', False):
raise LiveStreamError("Cannot download from a livestream")
# Set our info
self.info = info
async def prepare(self):
await self.get_info()
asyncio.run_coroutine_threadsafe(self.download(), self.loop)
return self.info
async def download(self):
try:
result = await self.downloader.extract_info(self.loop, self.url, download=True)
except Exception as e:
self.error = True
raise ExtractionError(e)
if result:
self.ready = True
opts = {
'before_options': '-nostdin',
'options': '-vn -b:a 128k -v fatal'
}
super().__init__(self.downloader.ytdl.prepare_filename(self.info), **opts)
@property
def title(self):
return self.info.get('title', 'Untitled')
@property
def thumbnail(self):
return self.info.get('thumbnail', None)
@property
def length(self):
return self.info.get('duration') or 0
@property
def progress(self):
if hasattr(self, 'start_time') and self.start_time:
return round(time.time() - self.start_time)
@property
def remaining(self):
length = self.length
progress = self.progress
if length and progress:
return length - progress
@property
def embed(self):
"""A property that returns an embed that can be used to display information about this particular song"""
# Create the embed object we'll use
embed = discord.Embed()
# Fill in the simple things
embed.add_field(name='Title', value=self.title, inline=False)
embed.add_field(name='Requester', value=self.requester.display_name, inline=False)
embed.add_field(name='Place in Queue', value=str(self.playlist.count))
if self.thumbnail:
embed.set_thumbnail(url=self.thumbnail)
# Get the current length of the song and display this
if self.length:
length = divmod(round(self.length, 0), 60)
fmt = "{0[0]}m {0[1]}s".format(length)
embed.add_field(name='Duration', value=fmt, inline=False)
# And return the embed we created
return embed
class YoutubeDLLiveStreamSource(discord.FFmpegPCMAudio):
def __init__(self, bot, url):
self._process = None
self.downloader = bot.downloader
self.loop = bot.loop
self.url = url
@property
def title(self):
return self.info.get('title', 'Untitled')
@property
def thumbnail(self):
return self.info.get('thumbnail', None)
@property
def embed(self):
"""A property that returns an embed that can be used to display information about this particular song"""
# Create the embed object we'll use
embed = discord.Embed()
# Fill in the simple things
embed.add_field(name='Title', value=self.title, inline=False)
embed.add_field(name='Requester', value=self.requester.display_name, inline=False)
if self.thumbnail:
embed.set_thumbnail(url=self.thumbnail)
# And return the embed we created
return embed
async def get_ready(self):
try:
# First attempt to gather the information
info = await self.downloader.extract_info(self.loop, self.url, download=False)
except Exception as e:
raise ExtractionError('Could not extract information from {}\n\n{}'.format(self.url, e))
if not info.get('is_live', False):
raise WrongEntryTypeError("This is not a livestream!", True, info.get('webpage_url', None) or info.get('url', None))
# Set our info
self.info = info
opts = {
'before_options': '-nostdin',
'options': '-vn -b:a 128k -v fatal'
}
super().__init__(info.get('manifest_url'), **opts)

View file

@ -1 +0,0 @@
fe04e897-b34e-4bf5-b79e-ccd5664bbd54