Created
April 19, 2019 21:33
-
-
Save infideleraser/e4dfb9cbb6915450b0e6068e6ede869f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import discord | |
from discord.ext import commands | |
import youtube_dl | |
import asyncio | |
import itertools | |
import sys | |
import traceback | |
from async_timeout import timeout | |
from functools import partial | |
from youtube_dl import YoutubeDL | |
ytdlopts = { | |
'format': 'bestaudio/best', | |
'outtmpl': 'downloads/%(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': 'auto', | |
'source_address': '0.0.0.0' # ipv6 addresses cause issues sometimes | |
} | |
ffmpegopts = { | |
'before_options': '-nostdin', | |
'options': '-vn' | |
} | |
ytdl = YoutubeDL(ytdlopts) | |
class VoiceConnectionError(commands.CommandError): | |
"""Custom Exception class for connection errors.""" | |
class InvalidVoiceChannel(VoiceConnectionError): | |
"""Exception for cases of invalid Voice Channels.""" | |
class YTDLSource(discord.PCMVolumeTransformer): | |
def __init__(self, source, *, data, requester): | |
super().__init__(source) | |
self.requester = requester | |
self.title = data.get('title') | |
self.web_url = data.get('webpage_url') | |
# YTDL info dicts (data) have other useful information you might want | |
# https://github.com/rg3/youtube-dl/blob/master/README.md | |
def __getitem__(self, item: str): | |
"""Allows us to access attributes similar to a dict. | |
This is only useful when you are NOT downloading. | |
""" | |
return self.__getattribute__(item) | |
@classmethod | |
async def create_source(cls, ctx, search: str, *, loop, download=False): | |
loop = loop or asyncio.get_event_loop() | |
to_run = partial(ytdl.extract_info, url=search, download=download) | |
data = await loop.run_in_executor(None, to_run) | |
if 'entries' in data: | |
# take first item from a playlist | |
data = data['entries'][0] | |
await ctx.send(f'```ini\n[Added {data["title"]} to the Queue.]\n```', delete_after=15) | |
if download: | |
source = ytdl.prepare_filename(data) | |
else: | |
return {'webpage_url': data['webpage_url'], 'requester': ctx.author, 'title': data['title']} | |
return cls(discord.FFmpegPCMAudio(source), data=data, requester=ctx.author) | |
@classmethod | |
async def regather_stream(cls, data, *, loop): | |
"""Used for preparing a stream, instead of downloading. | |
Since Youtube Streaming links expire.""" | |
loop = loop or asyncio.get_event_loop() | |
requester = data['requester'] | |
to_run = partial(ytdl.extract_info, url=data['webpage_url'], download=False) | |
data = await loop.run_in_executor(None, to_run) | |
return cls(discord.FFmpegPCMAudio(data['url']), data=data, requester=requester) | |
class MusicPlayer(): | |
__slots__ = ('bot', '_guild', '_channel', '_cog', 'queue', 'next', 'current', 'np', 'volume') | |
def __init__(self, ctx): | |
self.bot = ctx.bot | |
self._guild = ctx.guild | |
self._channel = ctx.channel | |
self._cog = ctx.cog | |
self.queue = asyncio.Queue() | |
self.next = asyncio.Event() | |
self.np = None # Now playing message | |
self.volume = .5 | |
self.current = None | |
ctx.bot.loop.create_task(self.player_loop()) | |
async def player_loop(self): | |
"""Our main player loop.""" | |
await self.bot.wait_until_ready() | |
while not self.bot.is_closed(): | |
self.next.clear() | |
try: | |
# Wait for the next song. If we timeout cancel the player and disconnect... | |
async with timeout(300): # 5 minutes... | |
source = await self.queue.get() | |
except asyncio.TimeoutError: | |
return self.destroy(self._guild) | |
if not isinstance(source, YTDLSource): | |
# Source was probably a stream (not downloaded) | |
# So we should regather to prevent stream expiration | |
try: | |
source = await YTDLSource.regather_stream(source, loop=self.bot.loop) | |
except Exception as e: | |
await self._channel.send(f'There was an error processing your song.\n```css\n[{e}]\n```') | |
continue | |
source.volume = self.volume | |
self.current = source | |
self._guild.voice_client.play(source, after=lambda _: self.bot.loop.call_soon_threadsafe(self.next.set)) | |
self.np = await self._channel.send(f'**Now Playing:** `{source.title}` requested by `{source.requester}`') | |
await self.next.wait() | |
# Make sure the FFmpeg process is cleaned up. | |
source.cleanup() | |
self.current = None | |
try: | |
# We are no longer playing this song... | |
await self.np.delete() | |
except discord.HTTPException: | |
pass | |
def destroy(self, guild): | |
"""Disconnect and cleanup the player.""" | |
return self.bot.loop.create_task(self._cog.cleanup(guild)) | |
class Music(commands.Cog): | |
"""Music related commands.""" | |
__slots__ = ('bot', 'players') | |
def __init__(self, bot): | |
self.bot = bot | |
self.players = {} | |
async def cleanup(self, guild): | |
try: | |
await guild.voice_client.disconnect() | |
except AttributeError: | |
pass | |
try: | |
del self.players[guild.id] | |
except KeyError: | |
pass | |
async def __local_check(self, ctx): | |
"""A local check which applies to all commands in this cog.""" | |
if not ctx.guild: | |
raise commands.NoPrivateMessage | |
return True | |
async def __error(self, ctx, error): | |
"""A local error handler for all errors arising from commands in this cog.""" | |
if isinstance(error, commands.NoPrivateMessage): | |
try: | |
return await ctx.send('This command can not be used in Private Messages.') | |
except discord.HTTPException: | |
pass | |
elif isinstance(error, InvalidVoiceChannel): | |
await ctx.send('Error connecting to Voice Channel', delete_after =30) | |
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr) | |
traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr) | |
def get_player(self, ctx): | |
"""Retrieve the guild player, or generate one.""" | |
try: | |
player = self.players[ctx.guild.id] | |
except KeyError: | |
player = MusicPlayer(ctx) | |
self.players[ctx.guild.id] = player | |
return player | |
''' | |
@commands.command( aliases=['DJ'], brief = 'creates the DJ role, only has to be run once. Add users to this role if you want them to control the music') | |
async def createDJ(self, ctx): | |
role = discord.utils.get(ctx.guild.roles, name="Muted") | |
if role not in ctx.guild.roles: | |
await ctx.guild.create_role(name='DJ', reason='This Role is needed so the bot will work!') | |
else: | |
await ctx.send("This role already exists. use 'shrek promote @user DJ' to assign your DJ's") | |
''' | |
@commands.command(name='connect', aliases=['join'], brief = 'connects bot to your voice channel') | |
async def connect_(self, ctx, *, channel: discord.VoiceChannel=None): | |
"""Connect to voice channel.""" | |
if not channel: | |
try: | |
channel = ctx.author.voice.channel | |
except AttributeError: | |
raise InvalidVoiceChannel('No channel to join. Please either specify a valid channel or join one.') | |
vc = ctx.voice_client | |
if vc: | |
if vc.channel.id == channel.id: | |
return | |
try: | |
await vc.move_to(channel) | |
except asyncio.TimeoutError: | |
raise VoiceConnectionError(f'Moving to channel: <{channel}> timed out.') | |
else: | |
try: | |
await channel.connect() | |
except asyncio.TimeoutError: | |
raise VoiceConnectionError(f'Connecting to channel: <{channel}> timed out.') | |
await ctx.send(f'Connected to: **{channel}**', delete_after=20) | |
@commands.command(name='play', aliases=['sing'], brief = 'plays song from youtube or a link') | |
async def play_(self, ctx, *, search: str, channel: discord.VoiceChannel=None): | |
"""Request a song and add it to the queue""" | |
await ctx.trigger_typing() | |
if not channel: | |
try: | |
channel = ctx.author.voice.channel | |
except AttributeError: | |
raise InvalidVoiceChannel("You arent in a voice channel") | |
vc = ctx.voice_client | |
''' | |
if vc: | |
if vc.channel.id == channel.id: | |
return | |
try: | |
await vc.move_to(channel) | |
except asyncio.TimeoutError: | |
raise VoiceConnectionError(f'Moving to channel: <{channel}> timed out.') | |
else: | |
try: | |
await channel.connect() | |
except asyncio.TimeoutError: | |
raise VoiceConnectionError(f'Connecting to channel: <{channel}> timed out.') | |
''' | |
if not vc: | |
await ctx.invoke(self.connect_) | |
player = self.get_player(ctx) | |
source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop, download=False) | |
await player.queue.put(source) | |
await ctx.message.add_reaction('✅') | |
await asyncio.sleep(15) | |
await ctx.message.delete() | |
@commands.command(name='pause', brief = 'pauses current song') | |
async def pause_(self, ctx, channel: discord.VoiceChannel=None): | |
"""Pause the currently playing song.""" | |
await ctx.trigger_typing() | |
if not channel: | |
try: | |
channel = ctx.author.voice.channel | |
except AttributeError: | |
raise InvalidVoiceChannel("You arent in a voice channel") | |
vc = ctx.voice_client | |
if not vc or not vc.is_playing(): | |
return await ctx.send('I am not currently playing anything!', delete_after=20) | |
elif vc.is_paused(): | |
return | |
vc.pause() | |
await ctx.send(f'**`{ctx.author}`**: Paused the song!') | |
@commands.command(name='resume', brief = 'resumes paused song') | |
async def resume_(self, ctx, channel: discord.VoiceChannel=None): | |
"""Resume the currently paused song.""" | |
await ctx.trigger_typing() | |
if not channel: | |
try: | |
channel = ctx.author.voice.channel | |
except AttributeError: | |
raise InvalidVoiceChannel("You arent in a voice channel") | |
vc = ctx.voice_client | |
if not vc or not vc.is_connected(): | |
return await ctx.send('I am not currently playing anything!', delete_after=20) | |
elif not vc.is_paused(): | |
return | |
vc.resume() | |
await ctx.send(f'**`{ctx.author}`**: Resumed the song!') | |
await ctx.message.delete() | |
@commands.has_permissions(administrator = True) | |
@commands.command(name='skip', brief = 'skips current song') | |
async def skip_(self, ctx): | |
"""Skip the song.""" | |
vc = ctx.voice_client | |
if not vc or not vc.is_connected(): | |
return await ctx.send('I am not currently playing anything!', delete_after=20) | |
if vc.is_paused(): | |
pass | |
elif not vc.is_playing(): | |
return | |
vc.stop() | |
await ctx.send(f'**`{ctx.author}`**: Skipped the song!', delete_after = 20) | |
await asyncio.sleep(20) | |
await ctx.message.delete() | |
@commands.command(name='queue', aliases=['q', 'playlist'], brief = 'shows songs in queue') | |
async def queue_info(self, ctx): | |
"""Retrieve a basic queue of upcoming songs.""" | |
vc = ctx.voice_client | |
if not vc or not vc.is_connected(): | |
return await ctx.send('I am not currently connected to voice!', delete_after=20) | |
player = self.get_player(ctx) | |
if player.queue.empty(): | |
return await ctx.send('There are currently no more queued songs.') | |
# Grab up to 5 entries from the queue... | |
upcoming = list(itertools.islice(player.queue._queue, 0, 5)) | |
fmt = '\n'.join(f'**`{_["title"]}`**' for _ in upcoming) | |
embed = discord.Embed(title=f'Upcoming - Next {len(upcoming)}', description=fmt) | |
message = await ctx.send(embed=embed) | |
await message.add_reaction('❌') | |
def check(reaction, user): | |
return user == ctx.message.author and reaction.message.id == message.id | |
try: | |
reaction, user = await self.bot.wait_for('reaction_add', timeout=60.0, check=check) | |
except asyncio.TimeoutError: | |
pass | |
else: | |
await ctx.message.delete() | |
await message.delete() | |
@commands.command(name='nowplaying', aliases=['np', 'current', 'currentsong', 'playing', ], brief = 'shows current song') | |
async def now_playing_(self, ctx): | |
"""Display information about the currently playing song.""" | |
vc = ctx.voice_client | |
if not vc or not vc.is_connected(): | |
return await ctx.send('I am not currently connected to voice!', delete_after=20) | |
player = self.get_player(ctx) | |
if not player.current: | |
return await ctx.send('I am not currently playing anything!') | |
try: | |
# Remove our previous now_playing message. | |
await player.np.delete() | |
except discord.HTTPException: | |
pass | |
player.np = await ctx.send(f'**Now Playing:** `{vc.source.title}` requested by `{vc.source.requester}`') | |
@commands.has_permissions(administrator = True) | |
@commands.command(name='volume', aliases=['vol'], brief = 'i mean if you cant figure this one out') | |
async def change_volume(self, ctx, *, vol: float, channel: discord.VoiceChannel=None): | |
"""Change the player volume.""" | |
await ctx.trigger_typing() | |
if not channel: | |
try: | |
channel = ctx.author.voice.channel | |
except AttributeError: | |
raise InvalidVoiceChannel("You arent in a voice channel") | |
vc = ctx.voice_client | |
if not vc or not vc.is_connected(): | |
return await ctx.send('I am not currently connected to voice!', delete_after=20) | |
if not 0 < vol < 101: | |
return await ctx.send('Please enter a value between 1 and 100.') | |
player = self.get_player(ctx) | |
if vc.source: | |
vc.source.volume = vol / 100 | |
player.volume = vol / 100 | |
await ctx.send(f'**`{ctx.author}`**: Set the volume to **{vol}%**') | |
@commands.has_permissions(administrator = True) | |
@commands.command(name='stop', brief = 'disconnects from vc and stops playing') | |
async def stop_(self, ctx): | |
"""Stop the currently playing song and destroy the player. | |
!Warning! | |
This will destroy the player assigned to your guild, also deleting any queued songs and settings. | |
""" | |
vc = ctx.voice_client | |
if not vc or not vc.is_connected(): | |
return await ctx.send('I am not currently playing anything!', delete_after=20) | |
await self.cleanup(ctx.guild) | |
message = await ctx.send("Bot has stopped playing music") | |
await message.add_reaction('✅') | |
def check(reaction, user): | |
return user == ctx.message.author and reaction.message.id == message.id | |
try: | |
reaction, user = await self.bot.wait_for('reaction_add', timeout=120.0, check=check) | |
except asyncio.TimeoutError: | |
pass | |
else: | |
await ctx.message.delete() | |
await message.delete() | |
await asyncio.sleep(120) | |
await message.delete() | |
def setup(bot): | |
bot.add_cog(Music(bot)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment