diff options
author | AnInternetTroll <lucafulger@gmail.com> | 2020-08-04 12:23:32 +0000 |
---|---|---|
committer | AnInternetTroll <lucafulger@gmail.com> | 2020-08-04 12:23:32 +0000 |
commit | 7e3f3e006d3cf29001e3990375d028dc9c91b757 (patch) | |
tree | a3f3a306b3459401db453e1fb27a9f62121b5096 /cogs | |
parent | 389da56c03cccb204d2cd12df6030a9afe2f26a8 (diff) | |
download | steve-bot-7e3f3e006d3cf29001e3990375d028dc9c91b757.tar steve-bot-7e3f3e006d3cf29001e3990375d028dc9c91b757.tar.gz steve-bot-7e3f3e006d3cf29001e3990375d028dc9c91b757.tar.bz2 steve-bot-7e3f3e006d3cf29001e3990375d028dc9c91b757.tar.lz steve-bot-7e3f3e006d3cf29001e3990375d028dc9c91b757.tar.xz steve-bot-7e3f3e006d3cf29001e3990375d028dc9c91b757.tar.zst steve-bot-7e3f3e006d3cf29001e3990375d028dc9c91b757.zip |
Fixed stop command
Diffstat (limited to 'cogs')
-rw-r--r--[-rwxr-xr-x] | cogs/player.py | 887 |
1 files changed, 444 insertions, 443 deletions
diff --git a/cogs/player.py b/cogs/player.py index 7013e6d..8a0eb97 100755..100644 --- a/cogs/player.py +++ b/cogs/player.py @@ -1,443 +1,444 @@ -"""
-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 discord
-from discord.ext import commands
-
-import asyncio
-import itertools
-import sys
-import traceback
-from async_timeout import timeout
-from functools import partial
-from youtube_dl import YoutubeDL
-from pathlib import Path
-from discord.ext import tasks
-
-
-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 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5'
-}
-
-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=True):
- 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, **ffmpegopts), 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'], **ffmpegopts), 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:
- if self in self._cog.players.values():
- return self.destroy(self._guild)
- return
-
- 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 Player(commands.Cog):
- """Music related commands."""
-
- __slots__ = ('bot', 'players')
-
- def __init__(self, bot):
- self.bot = bot
- self.cleanup.start()
- self.players = {}
-
- async def cleanup(self, guild):
- try:
- await guild.voice_client.disconnect()
- except AttributeError:
- pass
-
- try:
- for entry in self.players[guild.id].queue._queue:
- if isinstance(entry, YTDLSource):
- entry.cleanup()
- self.players[guild.id].queue._queue.clear()
- except KeyError:
- 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. '
- 'Please make sure you are in a valid channel or provide me with one')
-
- 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(name='connectvc', 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.')
-
- 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'])
- async def play_(self, ctx, *, search: str):
- """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()
-
- vc = ctx.voice_client
-
- 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 player.queue.put(source)
-
- @commands.command(name='pause')
- async def pause_(self, ctx):
- """Pause the currently playing song."""
- 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')
- async def resume_(self, ctx):
- """Resume the currently paused 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)
- elif not vc.is_paused():
- return
-
- vc.resume()
- await ctx.send(f'**`{ctx.author}`**: Resumed the song!')
-
- @commands.command(name='skip')
- 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!')
-
- @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, *, vol: 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 < 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.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)
-
- @tasks.loop(hours=10.0)
- async def cleanup(self):
- for p in Path("./downloads/").glob("*"):
- p.unlink()
-
-
-def setup(bot):
- bot.add_cog(Player(bot))
+""" +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 discord +from discord.ext import commands + +import asyncio +import itertools +import sys +import traceback +from async_timeout import timeout +from functools import partial +from youtube_dl import YoutubeDL +from pathlib import Path +from discord.ext import tasks + + +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 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5' +} + +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=True): + 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, **ffmpegopts), 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']) + data = await loop.run_in_executor(None, to_run) + + return cls(discord.FFmpegPCMAudio(data['url'], **ffmpegopts), 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: + if self in self._cog.players.values(): + return self.destroy(self._guild) + return + + 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 Player(commands.Cog): + """Music related commands.""" + + __slots__ = ('bot', 'players') + + def __init__(self, bot): + self.bot = bot + #self.removedownloads.start() + self.players = {} + + async def cleanup(self, guild): + try: + await guild.voice_client.disconnect() + except AttributeError: + pass + + try: + for entry in self.players[guild.id].queue._queue: + if isinstance(entry, YTDLSource): + entry.cleanup() + self.players[guild.id].queue._queue.clear() + except KeyError: + 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. ' + 'Please make sure you are in a valid channel or provide me with one') + + 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(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.') + + 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']) + async def play_(self, ctx, *, search: str): + """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() + + vc = ctx.voice_client + + 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) + + await player.queue.put(source) + + @commands.command(name='pause') + async def pause_(self, ctx): + """Pause the currently playing song.""" + 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') + async def resume_(self, ctx): + """Resume the currently paused 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) + elif not vc.is_paused(): + return + + vc.resume() + await ctx.send(f'**`{ctx.author}`**: Resumed the song!') + + @commands.command(name='skip') + 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!') + + @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, *, vol: 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 < 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.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) + + @tasks.loop(hours=10.0) + async def removedownloads(self): + for p in Path("./downloads/").glob("*"): + p.unlink() + + +def setup(bot): + bot.add_cog(Player(bot)) + |