|
|
@ -1,15 +1,31 @@
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Please understand Music bots are complex, and that even this basic example can be daunting to a beginner.
|
|
|
|
|
|
|
|
For this reason it's highly advised you familiarize yourself with discord.py, python and asyncio, BEFORE
|
|
|
|
|
|
|
|
you attempt to write a music bot.
|
|
|
|
|
|
|
|
This example makes use of: Python 3.6
|
|
|
|
|
|
|
|
For a more basic voice example please read:
|
|
|
|
|
|
|
|
https://github.com/Rapptz/discord.py/blob/rewrite/examples/basic_voice.py
|
|
|
|
|
|
|
|
This is a very basic playlist example, which allows per guild playback of unique queues.
|
|
|
|
|
|
|
|
The commands implement very basic logic for basic usage. But allow for expansion. It would be advisable to implement
|
|
|
|
|
|
|
|
your own permissions and usage logic for commands.
|
|
|
|
|
|
|
|
e.g You might like to implement a vote before skipping the song or only allow admins to stop the player.
|
|
|
|
|
|
|
|
Music bots require lots of work, and tuning. Goodluck.
|
|
|
|
|
|
|
|
If you find any bugs feel free to ping me on discord. @Eviee#0666
|
|
|
|
|
|
|
|
"""
|
|
|
|
import asyncio
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
import itertools
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
from functools import partial
|
|
|
|
|
|
|
|
|
|
|
|
import discord
|
|
|
|
import discord
|
|
|
|
import youtube_dl
|
|
|
|
from async_timeout import timeout
|
|
|
|
from discord.ext import commands
|
|
|
|
from discord.ext import commands
|
|
|
|
|
|
|
|
from youtube_dl import YoutubeDL
|
|
|
|
|
|
|
|
|
|
|
|
# Suppress noise about console usage from errors
|
|
|
|
ytdlopts = {
|
|
|
|
youtube_dl.utils.bug_reports_message = lambda: ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ytdl_format_options = {
|
|
|
|
|
|
|
|
'format': 'bestaudio/best',
|
|
|
|
'format': 'bestaudio/best',
|
|
|
|
'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',
|
|
|
|
'outtmpl': 'downloads/%(extractor)s-%(id)s-%(title)s.%(ext)s',
|
|
|
|
'restrictfilenames': True,
|
|
|
|
'restrictfilenames': True,
|
|
|
|
'noplaylist': True,
|
|
|
|
'noplaylist': True,
|
|
|
|
'nocheckcertificate': True,
|
|
|
|
'nocheckcertificate': True,
|
|
|
@ -18,107 +34,375 @@ ytdl_format_options = {
|
|
|
|
'quiet': True,
|
|
|
|
'quiet': True,
|
|
|
|
'no_warnings': True,
|
|
|
|
'no_warnings': True,
|
|
|
|
'default_search': 'auto',
|
|
|
|
'default_search': 'auto',
|
|
|
|
'source_address': '0.0.0.0' # bind to ipv4 since ipv6 addresses cause issues sometimes
|
|
|
|
'source_address': '0.0.0.0' # ipv6 addresses cause issues sometimes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ffmpeg_options = {
|
|
|
|
ffmpegopts = {
|
|
|
|
|
|
|
|
'before_options': '-nostdin',
|
|
|
|
'options': '-vn'
|
|
|
|
'options': '-vn'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
|
|
|
|
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):
|
|
|
|
class YTDLSource(discord.PCMVolumeTransformer):
|
|
|
|
def __init__(self, source, *, data, volume=0.5):
|
|
|
|
|
|
|
|
super().__init__(source, volume)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.data = data
|
|
|
|
def __init__(self, source, *, data, requester):
|
|
|
|
|
|
|
|
super().__init__(source)
|
|
|
|
|
|
|
|
self.requester = requester
|
|
|
|
|
|
|
|
|
|
|
|
self.title = data.get('title')
|
|
|
|
self.title = data.get('title')
|
|
|
|
self.url = data.get('url')
|
|
|
|
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
|
|
|
|
@classmethod
|
|
|
|
async def from_url(cls, url, *, loop=None, stream=False):
|
|
|
|
async def create_source(cls, ctx, search: str, *, loop, download=False):
|
|
|
|
loop = loop or asyncio.get_event_loop()
|
|
|
|
loop = loop or asyncio.get_event_loop()
|
|
|
|
data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream))
|
|
|
|
|
|
|
|
|
|
|
|
to_run = partial(ytdl.extract_info, url=search, download=download)
|
|
|
|
|
|
|
|
data = await loop.run_in_executor(None, to_run)
|
|
|
|
|
|
|
|
|
|
|
|
if 'entries' in data:
|
|
|
|
if 'entries' in data:
|
|
|
|
# take first item from a playlist
|
|
|
|
# take first item from a playlist
|
|
|
|
data = data['entries'][0]
|
|
|
|
data = data['entries'][0]
|
|
|
|
|
|
|
|
|
|
|
|
filename = data['url'] if stream else ytdl.prepare_filename(data)
|
|
|
|
await ctx.send(f'```ini\n[Added {data["title"]} to the Queue.]\n```', delete_after=15)
|
|
|
|
return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
"""A class which is assigned to each guild using the bot for Music.
|
|
|
|
|
|
|
|
This class implements a queue and loop, which allows for different guilds to listen to different playlists
|
|
|
|
|
|
|
|
simultaneously.
|
|
|
|
|
|
|
|
When the bot disconnects from the Voice it's instance will be destroyed.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__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'
|
|
|
|
|
|
|
|
f'```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 '
|
|
|
|
|
|
|
|
f'`{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:
|
|
|
|
|
|
|
|
"""Music related commands."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__slots__ = ('bot', 'players')
|
|
|
|
|
|
|
|
|
|
|
|
class Music(commands.Cog):
|
|
|
|
|
|
|
|
def __init__(self, bot):
|
|
|
|
def __init__(self, bot):
|
|
|
|
self.bot = bot
|
|
|
|
self.bot = bot
|
|
|
|
|
|
|
|
self.players = {}
|
|
|
|
|
|
|
|
|
|
|
|
@commands.command()
|
|
|
|
async def cleanup(self, guild):
|
|
|
|
async def join(self, ctx, *, channel: discord.VoiceChannel):
|
|
|
|
try:
|
|
|
|
"""Joins a voice channel"""
|
|
|
|
await guild.voice_client.disconnect()
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if ctx.voice_client is not None:
|
|
|
|
try:
|
|
|
|
return await ctx.voice_client.move_to(channel)
|
|
|
|
del self.players[guild.id]
|
|
|
|
|
|
|
|
except KeyError:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
await channel.connect()
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
"""@commands.command()
|
|
|
|
async def __error(self, ctx, error):
|
|
|
|
async def play(self, ctx, *, query):
|
|
|
|
"""A local error handler for all errors arising from commands in this cog."""
|
|
|
|
""""""Plays a file from the local filesystem""""""
|
|
|
|
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. '
|
|
|
|
|
|
|
|
'Please make sure you are in a valid channel or provide me with one')
|
|
|
|
|
|
|
|
|
|
|
|
source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(query))
|
|
|
|
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
|
|
|
|
ctx.voice_client.play(source, after=lambda e: print('Player error: %s' % e) if e else None)
|
|
|
|
traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
await ctx.send('Now playing: {}'.format(query))"""
|
|
|
|
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(name='connect', aliases=['join'])
|
|
|
|
|
|
|
|
async def connect_(self, ctx, *, channel: discord.VoiceChannel = None):
|
|
|
|
|
|
|
|
"""Connect to voice.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
|
|
------------
|
|
|
|
|
|
|
|
channel: discord.VoiceChannel [Optional]
|
|
|
|
|
|
|
|
The channel to connect to. If a channel is not specified, an attempt to join the voice channel you are in
|
|
|
|
|
|
|
|
will be made.
|
|
|
|
|
|
|
|
This command also handles moving the bot to different channels.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
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.')
|
|
|
|
|
|
|
|
|
|
|
|
@commands.command(name="play", aliases=["p", "Play", "P"])
|
|
|
|
vc = ctx.voice_client
|
|
|
|
async def play(self, ctx, *, url):
|
|
|
|
|
|
|
|
"""Plays from a url (almost anything youtube_dl supports)"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async with ctx.typing():
|
|
|
|
if vc:
|
|
|
|
player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
|
|
|
|
if vc.channel.id == channel.id:
|
|
|
|
ctx.voice_client.play(player, after=lambda e: print('Player error: %s' % e) if e else None)
|
|
|
|
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('Now playing: **{}**'.format(player.title))
|
|
|
|
await ctx.send(f'Connected to: **{channel}**', delete_after=20)
|
|
|
|
|
|
|
|
|
|
|
|
@commands.command()
|
|
|
|
@commands.command(name='play', aliases=['sing'])
|
|
|
|
async def stream(self, ctx, *, url):
|
|
|
|
async def play_(self, ctx, *, search: str):
|
|
|
|
"""Streams from a url (same as yt, but doesn't predownload)"""
|
|
|
|
"""Request a song and add it to the queue.
|
|
|
|
|
|
|
|
This command attempts to join a valid voice channel if the bot is not already in one.
|
|
|
|
|
|
|
|
Uses YTDL to automatically search and retrieve a song.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
|
|
------------
|
|
|
|
|
|
|
|
search: str [Required]
|
|
|
|
|
|
|
|
The song to search and retrieve using YTDL. This could be a simple search, an ID or URL.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
await ctx.trigger_typing()
|
|
|
|
|
|
|
|
|
|
|
|
async with ctx.typing():
|
|
|
|
vc = ctx.voice_client
|
|
|
|
player = await YTDLSource.from_url(url, loop=self.bot.loop)
|
|
|
|
|
|
|
|
ctx.voice_client.play(player, after=lambda e: print('Player error: %s' % e) if e else None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not vc:
|
|
|
|
|
|
|
|
await ctx.invoke(self.connect_)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
player = self.get_player(ctx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# If download is False, source will be a dict which will be used later to regather the stream.
|
|
|
|
|
|
|
|
# If download is True, source will be a discord.FFmpegPCMAudio with a VolumeTransformer.
|
|
|
|
|
|
|
|
source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop, download=False)
|
|
|
|
await ctx.send('Now playing: {}'.format(player.title))
|
|
|
|
await ctx.send('Now playing: {}'.format(player.title))
|
|
|
|
|
|
|
|
|
|
|
|
@commands.command()
|
|
|
|
await player.queue.put(source)
|
|
|
|
async def volume(self, ctx, volume: int):
|
|
|
|
|
|
|
|
"""Changes the player's volume"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ctx.voice_client is None:
|
|
|
|
@commands.command(name='pause')
|
|
|
|
return await ctx.send("Not connected to a voice channel.")
|
|
|
|
async def pause_(self, ctx):
|
|
|
|
|
|
|
|
"""Pause the currently playing song."""
|
|
|
|
|
|
|
|
vc = ctx.voice_client
|
|
|
|
|
|
|
|
|
|
|
|
ctx.voice_client.source.volume = volume / 100
|
|
|
|
if not vc or not vc.is_playing():
|
|
|
|
await ctx.send("Changed volume to {}%".format(volume))
|
|
|
|
return await ctx.send('I am not currently playing anything!', delete_after=20)
|
|
|
|
|
|
|
|
elif vc.is_paused():
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
@commands.command()
|
|
|
|
vc.pause()
|
|
|
|
async def stop(self, ctx):
|
|
|
|
await ctx.send(f'**`{ctx.author}`**: Paused the song!')
|
|
|
|
"""Stops and disconnects the bot from voice"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await ctx.voice_client.disconnect()
|
|
|
|
@commands.command(name='resume')
|
|
|
|
|
|
|
|
async def resume_(self, ctx):
|
|
|
|
|
|
|
|
"""Resume the currently paused song."""
|
|
|
|
|
|
|
|
vc = ctx.voice_client
|
|
|
|
|
|
|
|
|
|
|
|
@play.before_invoke
|
|
|
|
if not vc or not vc.is_connected():
|
|
|
|
@stream.before_invoke
|
|
|
|
return await ctx.send('I am not currently playing anything!', delete_after=20)
|
|
|
|
async def ensure_voice(self, ctx):
|
|
|
|
elif not vc.is_paused():
|
|
|
|
if ctx.voice_client is None:
|
|
|
|
return
|
|
|
|
if ctx.author.voice:
|
|
|
|
|
|
|
|
await ctx.author.voice.channel.connect()
|
|
|
|
vc.resume()
|
|
|
|
else:
|
|
|
|
await ctx.send(f'**`{ctx.author}`**: Resumed the song!')
|
|
|
|
await ctx.send("You are not connected to a voice channel.")
|
|
|
|
|
|
|
|
raise commands.CommandError("Author not connected to a voice channel.")
|
|
|
|
@commands.command(name='skip')
|
|
|
|
elif ctx.voice_client.is_playing():
|
|
|
|
async def skip_(self, ctx):
|
|
|
|
ctx.voice_client.stop()
|
|
|
|
"""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!')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@commands.command(name='queue', aliases=['q', 'playlist'])
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await ctx.send(embed=embed)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@commands.command(name='now_playing', aliases=['np', 'current', 'currentsong', 'playing'])
|
|
|
|
|
|
|
|
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}` '
|
|
|
|
|
|
|
|
f'requested by `{vc.source.requester}`')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@commands.command(name='volume', aliases=['vol'])
|
|
|
|
|
|
|
|
async def change_volume(self, ctx, *, volume: float):
|
|
|
|
|
|
|
|
"""Change the player volume.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
|
|
------------
|
|
|
|
|
|
|
|
volume: float or int [Required]
|
|
|
|
|
|
|
|
The volume to set the player to in percentage. This must be between 1 and 100.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
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 < volume < 101:
|
|
|
|
|
|
|
|
return await ctx.send('Please enter a value between 1 and 100.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
player = self.get_player(ctx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if vc.source:
|
|
|
|
|
|
|
|
vc.source.volume = volume / 100
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
player.volume = volume / 100
|
|
|
|
|
|
|
|
await ctx.send(f'**`{ctx.author}`**: Set the volume to **{volume}%**')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@commands.command(name='stop')
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup(bot):
|
|
|
|
def setup(bot):
|
|
|
|