"""A Rewrite example of a cross guild music player with playlist support. 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 and the Rewrite Branch of Discord.py. For a more basic voice example please read: https://github.com/Rapptz/discord.py/blob/rewrite/examples/basic_voice.py """ import discord from discord.ext import commands import asyncio import async_timeout import youtube_dl opts = { '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 } ytdl = youtube_dl.YoutubeDL(opts) ffmpeg_options = { 'before_options': '-nostdin', 'options': '-vn' } class YTDLSource(discord.PCMVolumeTransformer): """A class which uses YTDL to retrieve a song and returns it as a source for Discord.""" def __init__(self, source, *, data, volume=.4): 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): loop = loop or asyncio.get_event_loop() data = await loop.run_in_executor(None, ytdl.extract_info, url) if 'entries' in data: data = data['entries'][0] filename = ytdl.prepare_filename(data) return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data) class MusicPlayer: """Music Player instance. Each guild using music will have a separate instance.""" def __init__(self, bot, ctx): self.bot = bot self.queue = asyncio.Queue() self.next = asyncio.Event() self.die = asyncio.Event() self.guild = ctx.guild self.default_chan = ctx.channel self.current = None self.volume = .4 self.now_playing = None self.player_task = self.bot.loop.create_task(self.player_loop()) self.inactive_task = self.bot.loop.create_task(self.inactive_check(ctx)) async def inactive_check(self, ctx): await self.die.wait() await ctx.invoke(self.bot.get_command('stop')) async def player_loop(self): await self.bot.wait_until_ready() while not self.bot.is_closed(): self.next.clear() try: with async_timeout.timeout(10): # Auto leave after 5 minutes of inactivity. entry = await self.queue.get() except asyncio.TimeoutError: await self.default_chan.send('I have been inactive for 5 minutes. Goodbye!') return self.die.set() try: info = await YTDLSource.from_url(entry.query, loop=self.bot.loop) info.volume = self.volume except Exception: await self.default_chan.send(f'There was an error with the request: `{entry.query}`') continue channel = entry.channel requester = entry.requester self.guild.voice_client.play(info, after=lambda s: self.bot.loop.call_soon_threadsafe(self.next.set())) self.now_playing = await channel.send(f'**Now Playing:** `{info.title}` requested by `{requester}`') await self.next.wait() # Wait until the players after function is called. try: await self.now_playing.delete() except (discord.Forbidden, discord.HTTPException): pass class MusicEntry: def __init__(self, ctx, query): self.requester = ctx.author self.channel = ctx.channel self.query = query class Music: """Music Cog containing various commands for playing music. This cog supports cross guild music playing and implements a queue for playlists.""" def __init__(self, bot): self.bot = bot self.players = {} async def __local_check(self, ctx): """A check which applies to all commands in Music.""" if not ctx.guild: await ctx.send('Music commands can not be used in DMs.') return False def get_player(self, ctx): try: player = self.players[ctx.guild.id] except KeyError: player = MusicPlayer(self.bot, ctx) self.players[ctx.guild.id] = player return player @commands.command(name='connect', aliases=['summon', 'join', 'move']) async def voice_connect(self, ctx, *, channel: discord.VoiceChannel=None): """Summon the bot to a voice channel. This command handles both summoning and moving the bot.""" channel = getattr(ctx.author.voice, 'channel', channel) vc = ctx.guild.voice_client if not channel: return await ctx.send('No channel to join. Please either specify a valid channel or join one.') if not vc: try: await channel.connect(timeout=10) except asyncio.TimeoutError: return await ctx.send('Unable to connect to the voice channel at this time. Please try again.') await ctx.send(f'Connected to: **{channel}**', delete_after=15) else: if channel == vc.channel: return try: await vc.move_to(channel) except Exception: return await ctx.send('Unable to move this channel. Perhaps missing permissions?') await ctx.send(f'Moved to: **{channel}**', delete_after=15) @commands.command(name='play') async def play_song(self, ctx, *, query: str): """Add a song to the queue. Uses YTDL to auto search for a song. A URL may also be provided.""" vc = ctx.guild.voice_client if vc is None: await ctx.invoke(self.voice_connect) if not ctx.guild.voice_client: return else: if ctx.author not in vc.channel.members: return await ctx.send(f'You must be in **{vc.channel}** to request songs.', delete_after=30) player = self.get_player(ctx) entry = MusicEntry(ctx, query) await player.queue.put(entry) await ctx.send(f'Added: `{query}` to the queue.', delete_after=30) @commands.command(name='stop') async def stop_player(self, ctx): """Stops the player and clears the queue.""" vc = ctx.guild.voice_client if vc is None: return player = self.get_player(ctx) inact = player.inactive_task vc.stop() try: player.player_task.cancel() del self.players[ctx.guild.id] except Exception as e: return print(e) await vc.disconnect() await ctx.send('Disconnected from voice and cleared your queue. Goodbye!', delete_after=15) try: inact.cancel() except Exception as e: print(e) @commands.command(name='pause') async def pause_song(self, ctx): """Pause the currently playing song.""" vc = ctx.guild.voice_client if vc is None or not vc.is_playing(): return await ctx.send('I am not currently playing anything.', delete_after=20) if vc.is_paused(): return await ctx.send('I am already paused.', delete_after=20) vc.pause() await ctx.send(f'{ctx.author.mention} has paused the song.') @commands.command(name='resume') async def resume_song(self, ctx): """Resume a song if it is currently paused.""" vc = ctx.guild.voice_client if vc is None or not vc.is_connected(): return await ctx.send('I am not currently playing anything.', delete_after=20) if vc.is_paused(): vc.resume() await ctx.send(f'{ctx.author.mention} has resumed the song.') @commands.command(name='skip') async def skip_song(self, ctx): """Skip the current song.""" vc = ctx.guild.voice_client if vc is None or not vc.is_connected(): return await ctx.send('I am not currently playing anything.', delete_after=20) vc.stop() await ctx.send(f'{ctx.author.mention} has skipped the song.') @commands.command(name='current', aliases=['currentsong', 'nowplaying', 'np']) async def current_song(self, ctx): """Return some information about the current song.""" vc = ctx.guild.voice_client if not vc.is_playing(): return await ctx.send('Not currently playing anything.') player = self.get_player(ctx) msg = player.now_playing.content try: await player.now_playing.delete() except (discord.Forbidden, discord.HTTPException): pass player.now_playing = await ctx.send(msg) @commands.command(name='volume', aliases=['vol']) async def adjust_volume(self, ctx, *, vol: int): """Adjust the player volume.""" if not 0 < vol < 101: return await ctx.send('Please enter a value between 1 and 100.') vc = ctx.guild.voice_client if vc is None: return await ctx.send('I am not currently connected to voice.') player = self.get_player(ctx) adj = float(vol) / 100 try: vc.source.volume = adj except AttributeError: pass finally: player.volume = adj await ctx.send(f'Changed player volume to: **{vol}%**') def setup(bot): bot.add_cog(Music(bot))