Skip to content

Instantly share code, notes, and snippets.

@infideleraser
Created April 19, 2019 21:33
Show Gist options
  • Save infideleraser/e4dfb9cbb6915450b0e6068e6ede869f to your computer and use it in GitHub Desktop.
Save infideleraser/e4dfb9cbb6915450b0e6068e6ede869f to your computer and use it in GitHub Desktop.
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