diff --git a/cogs/music.py b/cogs/music.py deleted file mode 100644 index 99445bf2..00000000 --- a/cogs/music.py +++ /dev/null @@ -1,1002 +0,0 @@ -""" -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 itertools -import sys -import traceback -from functools import partial - -import discord -from async_timeout import timeout -from discord.ext import commands -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: - """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(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. ' - '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, download=False) - await ctx.send('Now playing: {}'.format(player.title)) - - 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, *, 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): - bot.add_cog(Music(bot)) - - -""" -import asyncio -import ffmpeg -import discord -import youtube_dl - -from discord.ext import commands - -# Suppress noise about console usage from errors -youtube_dl.utils.bug_reports_message = lambda: '' - -ytdl_format_options = { - 'format': 'bestaudio/best', - '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': 'auto', - 'source_address': '0.0.0.0' # bind to ipv4 since ipv6 addresses cause issues sometimes -} - -ffmpeg_options = { - 'options': '-vn' -} - -ytdl = youtube_dl.YoutubeDL(ytdl_format_options) - - -class YTDLSource(discord.PCMVolumeTransformer): - def __init__(self, source, *, data, volume=0.5): - super().__init__(source, volume) - - self.data = data - - self.title = data.get('title') - self.url = data.get('url') - - @classmethod - async def from_url(cls, url, *, loop=None, stream=False): - loop = loop or asyncio.get_event_loop() - data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream)) - - if 'entries' in data: - # take first item from a playlist - data = data['entries'][0] - - filename = data['url'] if stream else ytdl.prepare_filename(data) - return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data) - - -players = {} - - -class Music(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.command(aliases=['j', 'joi']) - async def join(self, ctx): - if ctx.message.author.voice: - channel = ctx.message.author.voice.channel - await channel.connect() - - @commands.command(aliases=['l', 'Leave']) - async def leave(self, ctx): - try: - server = ctx.voice_client - - await server.disconnect() - await ctx.send(f"Left the voice channel") - except Exception as e: - print(e) - - @commands.command() - async def yt(self, ctx, *, url): - Plays from a url (almost anything youtube_dl supports) - try: - async with ctx.typing(): - 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) - - await ctx.send('Now playing: {}'.format(player.title)) - except Exception as e: - print(e) - - async def play(self, url, ctx): - try: - guild = ctx.message.guild - voice_client = guild.voice_client - - player = await voice_client.create_ytdl_player(url) - players[guild.id] = player - player.start() - except Exception as e: - print(e) - - -import asyncio -import functools -import itertools -import math -import random - -import discord -import youtube_dl -from async_timeout import timeout -from discord.ext import commands - -# Silence useless bug reports messages -youtube_dl.utils.bug_reports_message = lambda: '' - - -class VoiceError(Exception): - pass - - -class YTDLError(Exception): - pass - - -class YTDLSource(discord.PCMVolumeTransformer): - YTDL_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': 'auto', - 'source_address': '0.0.0.0', - } - - FFMPEG_OPTIONS = { - 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', - 'options': '-vn', - } - - ytdl = youtube_dl.YoutubeDL(YTDL_OPTIONS) - - def __init__(self, ctx: commands.Context, source: discord.FFmpegPCMAudio, *, data: dict, volume: float = 0.5): - super().__init__(source, volume) - - self.requester = ctx.author - self.channel = ctx.channel - self.data = data - - self.uploader = data.get('uploader') - self.uploader_url = data.get('uploader_url') - date = data.get('upload_date') - self.upload_date = date[6:8] + '.' + date[4:6] + '.' + date[0:4] - self.title = data.get('title') - self.thumbnail = data.get('thumbnail') - self.description = data.get('description') - self.duration = self.parse_duration(int(data.get('duration'))) - self.tags = data.get('tags') - self.url = data.get('webpage_url') - self.views = data.get('view_count') - self.likes = data.get('like_count') - self.dislikes = data.get('dislike_count') - self.stream_url = data.get('url') - - def __str__(self): - return '**{0.title}** by **{0.uploader}**'.format(self) - - @classmethod - async def create_source(cls, ctx: commands.Context, search: str, *, loop: asyncio.BaseEventLoop = None): - loop = loop or asyncio.get_event_loop() - - partial = functools.partial(cls.ytdl.extract_info, search, download=False, process=False) - data = await loop.run_in_executor(None, partial) - - if data is None: - raise YTDLError('Couldn\'t find anything that matches `{}`'.format(search)) - - if 'entries' not in data: - process_info = data - else: - process_info = None - for entry in data['entries']: - if entry: - process_info = entry - break - - if process_info is None: - raise YTDLError('Couldn\'t find anything that matches `{}`'.format(search)) - - webpage_url = process_info['webpage_url'] - partial = functools.partial(cls.ytdl.extract_info, webpage_url, download=False) - processed_info = await loop.run_in_executor(None, partial) - - if processed_info is None: - raise YTDLError('Couldn\'t fetch `{}`'.format(webpage_url)) - - if 'entries' not in processed_info: - info = processed_info - else: - info = None - while info is None: - try: - info = processed_info['entries'].pop(0) - except IndexError: - raise YTDLError('Couldn\'t retrieve any matches for `{}`'.format(webpage_url)) - - return cls(ctx, discord.FFmpegPCMAudio(info['url'], **cls.FFMPEG_OPTIONS), data=info) - - @staticmethod - def parse_duration(duration: int): - minutes, seconds = divmod(duration, 60) - hours, minutes = divmod(minutes, 60) - days, hours = divmod(hours, 24) - - duration = [] - if days > 0: - duration.append('{} days'.format(days)) - if hours > 0: - duration.append('{} hours'.format(hours)) - if minutes > 0: - duration.append('{} minutes'.format(minutes)) - if seconds > 0: - duration.append('{} seconds'.format(seconds)) - - return ', '.join(duration) - - -class Song: - __slots__ = ('source', 'requester') - - def __init__(self, source: YTDLSource): - self.source = source - self.requester = source.requester - - def create_embed(self): - embed = (discord.Embed(title='Now playing', - description='```css\n{0.source.title}\n```'.format(self), - color=discord.Color.blurple()) - .add_field(name='Duration', value=self.source.duration) - .add_field(name='Requested by', value=self.requester.mention) - .add_field(name='Uploader', value='[{0.source.uploader}]({0.source.uploader_url})'.format(self)) - .add_field(name='URL', value='[Click]({0.source.url})'.format(self)) - .set_thumbnail(url=self.source.thumbnail)) - - return embed - - -class SongQueue(asyncio.Queue): - def __getitem__(self, item): - if isinstance(item, slice): - return list(itertools.islice(self._queue, item.start, item.stop, item.step)) - else: - return self._queue[item] - - def __iter__(self): - return self._queue.__iter__() - - def __len__(self): - return self.qsize() - - def clear(self): - self._queue.clear() - - def shuffle(self): - random.shuffle(self._queue) - - def remove(self, index: int): - del self._queue[index] - - -class VoiceState: - def __init__(self, bot: commands.Bot, ctx: commands.Context): - self.bot = bot - self._ctx = ctx - - self.current = None - self.voice = None - self.next = asyncio.Event() - self.songs = SongQueue() - - self._loop = False - self._volume = 0.5 - self.skip_votes = set() - - self.audio_player = bot.loop.create_task(self.audio_player_task()) - - def __del__(self): - self.audio_player.cancel() - - @property - def loop(self): - return self._loop - - @loop.setter - def loop(self, value: bool): - self._loop = value - - @property - def volume(self): - return self._volume - - @volume.setter - def volume(self, value: float): - self._volume = value - - @property - def is_playing(self): - return self.voice and self.current - - async def audio_player_task(self): - while True: - self.next.clear() - - if not self.loop: - # Try to get the next song within 3 minutes. - # If no song will be added to the queue in time, - # the player will disconnect due to performance - # reasons. - try: - async with timeout(180): # 3 minutes - self.current = await self.songs.get() - except asyncio.TimeoutError: - self.bot.loop.create_task(self.stop()) - return - - self.current.source.volume = self._volume - self.voice.play(self.current.source, after=self.play_next_song) - await self.current.source.channel.send(embed=self.current.create_embed()) - - await self.next.wait() - - def play_next_song(self, error=None): - if error: - raise VoiceError(str(error)) - - self.next.set() - - def skip(self): - self.skip_votes.clear() - - if self.is_playing: - self.voice.stop() - - async def stop(self): - self.songs.clear() - - if self.voice: - await self.voice.disconnect() - self.voice = None - - -class Music(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - self.voice_clients = {} - - def get_voice_client(self, ctx: commands.Context): - state = self.voice_clients.get(ctx.guild.id) - if not state: - state = VoiceState(self.bot, ctx) - self.voice_clients[ctx.guild.id] = state - - return state - - def cog_unload(self): - for state in self.voice_clients.values(): - self.bot.loop.create_task(state.stop()) - - def cog_check(self, ctx: commands.Context): - if not ctx.guild: - raise commands.NoPrivateMessage('This command can\'t be used in DM channels.') - - return True - - async def cog_before_invoke(self, ctx: commands.Context): - ctx.voice_state = self.get_voice_client(ctx) - - async def cog_command_error(self, ctx: commands.Context, error: commands.CommandError): - await ctx.send('An error occurred: {}'.format(str(error))) - - @commands.command(name='join', invoke_without_subcommand=True) - async def _join(self, ctx: commands.Context): - """ """Joins a voice channel."""""" - - destination = ctx.author.voice.channel - if ctx.voice_client.voice: - await ctx.voice_client.voice.move_to(destination) - return - - ctx.voice_client.voice = await destination.connect() - - @commands.command(name='summon') - @commands.has_permissions(manage_guild=True) - async def _summon(self, ctx: commands.Context, *, channel: discord.VoiceChannel = None): - """"""Summons the bot to a voice channel. - If no channel was specified, it joins your channel. - """""" - - if not channel and not ctx.author.voice: - raise VoiceError('You are neither connected to a voice channel nor specified a channel to join.') - - destination = channel or ctx.author.voice.channel - if ctx.voice_client.voice: - await ctx.voice_client.voice.move_to(destination) - return - - ctx.voice_client.voice = await destination.connect() - - @commands.command(name='leave', aliases=['disconnect']) - @commands.has_permissions(manage_guild=True) - async def _leave(self, ctx: commands.Context): - """"""Clears the queue and leaves the voice channel."""""" - - if not ctx.voice_client.voice: - return await ctx.send('Not connected to any voice channel.') - - await ctx.voice_client.stop() - del self.voice_clients[ctx.guild.id] - - @commands.command(name='volume') - async def _volume(self, ctx: commands.Context, *, volume: int): - """"""Sets the volume of the player."""""" - - if not ctx.voice_client.is_playing: - return await ctx.send('Nothing being played at the moment.') - - if 0 > volume > 100: - return await ctx.send('Volume must be between 0 and 100') - - ctx.voice_client.volume = volume / 100 - await ctx.send('Volume of the player set to {}%'.format(volume)) - - @commands.command(name='now', aliases=['current', 'playing']) - async def _now(self, ctx: commands.Context): - """"""Displays the currently playing song."""""" - - await ctx.send(embed=ctx.voice_client.current.create_embed()) - - @commands.command(name='pause') - @commands.has_permissions(manage_guild=True) - async def _pause(self, ctx: commands.Context): - """"""Pauses the currently playing song."""""" - - if not ctx.voice_client.is_playing and ctx.voice_client.voice.is_playing(): - ctx.voice_client.voice.pause() - await ctx.message.add_reaction('⏯') - - @commands.command(name='resume') - @commands.has_permissions(manage_guild=True) - async def _resume(self, ctx: commands.Context): - """"""Resumes a currently paused song."""""" - - if not ctx.voice_client.is_playing and ctx.voice_client.voice.is_paused(): - ctx.voice_client.voice.resume() - await ctx.message.add_reaction('⏯') - - @commands.command(name='stop') - @commands.has_permissions(manage_guild=True) - async def _stop(self, ctx: commands.Context): - """"""#Stops playing song and clears the queue."""""" - - ctx.voice_client.songs.clear() - - if not ctx.voice_client.is_playing: - ctx.voice_client.voice.stop() - await ctx.message.add_reaction('⏹') - - @commands.command(name='skip') - async def _skip(self, ctx: commands.Context): - """"""Vote to skip a song. The requester can automatically skip. - 3 skip votes are needed for the song to be skipped. - """""" - - if not ctx.voice_client.is_playing: - return await ctx.send('Not playing any music right now...') - - voter = ctx.message.author - if voter == ctx.voice_client.current.requester: - await ctx.message.add_reaction('⏭') - ctx.voice_client.skip() - - elif voter.id not in ctx.voice_client.skip_votes: - ctx.voice_client.skip_votes.add(voter.id) - total_votes = len(ctx.voice_client.skip_votes) - - if total_votes >= 3: - await ctx.message.add_reaction('⏭') - ctx.voice_client.skip() - else: - await ctx.send('Skip vote added, currently at **{}/3**'.format(total_votes)) - - else: - await ctx.send('You have already voted to skip this song.') - - @commands.command(name='queue') - async def _queue(self, ctx: commands.Context, *, page: int = 1): - """"""Shows the player's queue. - You can optionally specify the page to show. Each page contains 10 elements. - """""" - - if len(ctx.voice_client.songs) == 0: - return await ctx.send('Empty queue.') - - items_per_page = 10 - pages = math.ceil(len(ctx.voice_client.songs) / items_per_page) - - start = (page - 1) * items_per_page - end = start + items_per_page - - queue = '' - for i, song in enumerate(ctx.voice_client.songs[start:end], start=start): - queue += '`{0}.` [**{1.source.title}**]({1.source.url})\n'.format(i + 1, song) - - embed = (discord.Embed(description='**{} tracks:**\n\n{}'.format(len(ctx.voice_client.songs), queue)) - .set_footer(text='Viewing page {}/{}'.format(page, pages))) - await ctx.send(embed=embed) - - @commands.command(name='shuffle') - async def _shuffle(self, ctx: commands.Context): - """"""Shuffles the queue."""""" - - if len(ctx.voice_client.songs) == 0: - return await ctx.send('Empty queue.') - - ctx.voice_client.songs.shuffle() - await ctx.message.add_reaction('✅') - - @commands.command(name='remove') - async def _remove(self, ctx: commands.Context, index: int): - """"""Removes a song from the queue at a given index."""""" - - if len(ctx.voice_client.songs) == 0: - return await ctx.send('Empty queue.') - - ctx.voice_client.songs.remove(index - 1) - await ctx.message.add_reaction('✅') - - @commands.command(name='loop') - async def _loop(self, ctx: commands.Context): - """ """Loops the currently playing song. - Invoke this command again to unloop the song. - """""" - - if not ctx.voice_client.is_playing: - return await ctx.send('Nothing being played at the moment.') - - # Inverse boolean value to loop and unloop. - ctx.voice_client.loop = not ctx.voice_client.loop - await ctx.message.add_reaction('✅') - - @commands.command(name='play') - async def _play(self, ctx: commands.Context, *, search: str): - """"""Plays a song. - If there are songs in the queue, this will be queued until the - other songs finished playing. - This command automatically searches from various sites if no URL is provided. - A list of these sites can be found here: https://rg3.github.io/youtube-dl/supportedsites.html - """""" - - if not ctx.voice_client.voice: - await ctx.invoke(self._join) - - async with ctx.typing(): - try: - source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop) - except YTDLError as e: - await ctx.send('An error occurred while processing this request: {}'.format(str(e))) - else: - song = Song(source) - - await ctx.voice_client.songs.put(song) - await ctx.send('Enqueued {}'.format(str(source))) - - @_join.before_invoke - @_play.before_invoke - async def ensure_voice_client(self, ctx: commands.Context): - if not ctx.author.voice or not ctx.author.voice.channel: - raise commands.CommandError('You are not connected to any voice channel.') - - if ctx.voice_client: - if ctx.voice_client.channel != ctx.author.voice.channel: - raise commands.CommandError('Bot is already in a voice channel.') - - -"""