From 209ffca7600e352a911fbbf9034aa0c08cb06d16 Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Sun, 23 Oct 2022 13:57:24 -0700 Subject: [PATCH 01/11] python streamer seems to be working --- .gitignore | 2 +- Dizplayer.py | 10 +- Dockerfile | 7 +- Music.py | 658 ++++++++++++++++++++++++++++++++------------- docker-compose.yml | 8 + requirements.txt | 6 +- 6 files changed, 486 insertions(+), 205 deletions(-) create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore index 5391d87..8034918 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ __pycache__/ *.py[cod] *$py.class - +.idea/ # C extensions *.so diff --git a/Dizplayer.py b/Dizplayer.py index dc6f08f..d64bedd 100644 --- a/Dizplayer.py +++ b/Dizplayer.py @@ -1,17 +1,14 @@ import os import discord -import nest_asyncio from discord.ext import commands from dotenv import load_dotenv -from Listener import ListenerCog -from Music import MusicPlayerCog +from Music import Music -nest_asyncio.apply() load_dotenv() # Get the API token from the .env file. -DISCORD_TOKEN = os.getenv("discord_token") +DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") print(DISCORD_TOKEN) initial_extensions = ['cogs.listener', @@ -21,6 +18,5 @@ bot = commands.Bot(command_prefix='!', intents=intents) if __name__ == "__main__": - bot.add_cog(MusicPlayerCog(bot)) - bot.add_cog(ListenerCog(bot)) + bot.add_cog(Music(bot)) bot.run(DISCORD_TOKEN) diff --git a/Dockerfile b/Dockerfile index d363c65..d9b2f8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,7 @@ WORKDIR /app COPY ./requirements.txt /app ENV PYTHONUNBUFFERED 1 - +RUN apt-get -y update +RUN apt-get install -y ffmpeg RUN pip3 install --no-cache-dir -r requirements.txt -COPY . . - -CMD ["python", "Dizplayer.py"] \ No newline at end of file +COPY . . \ No newline at end of file diff --git a/Music.py b/Music.py index ba1a39b..b5cf426 100644 --- a/Music.py +++ b/Music.py @@ -1,215 +1,493 @@ -import asyncio -import collections - import discord from discord.ext import commands +import random +import asyncio +import itertools +import sys +import traceback +from async_timeout import timeout +from functools import partial +import youtube_dl +from youtube_dl import YoutubeDL + +# Suppress noise about console usage from errors +youtube_dl.utils.bug_reports_message = lambda: '' + +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') + self.duration = data.get('duration') + + # 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] + + embed = discord.Embed(title="", + description=f"Queued [{data['title']}]({data['webpage_url']}) [{ctx.author.mention}]", + color=discord.Color.green()) + await ctx.send(embed=embed) + + 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'] -from Track import Track -from YTDL import YTDLSource + to_run = partial(ytdl.extract_info, url=data['webpage_url'], download=False) + data = await loop.run_in_executor(None, to_run) -song_queue = collections.deque() -history = collections.deque() -currentVoiceClient = None -currentVoiceChannel = None -currentSongData = None -guildTextChannel = None -currentTrack = None -is_playing = False -runningTask = None + return cls(discord.FFmpegPCMAudio(data['url']), data=data, requester=requester) -def get_or_create_event_loop(): - try: - loop = asyncio.get_event_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - return loop +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', 'ctx') -def add_songs_to_song_queue(data): - for song in data: - song_queue.appendleft(Track(song['artist'] + ' - ' + song['title'], song['webpage_url'])) + 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() -class MusicPlayerCog(commands.Cog): + self.np = None # Now playing message + self.volume = .5 + self.current = None + self.ctx = ctx + + 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.ctx.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)) + embed = discord.Embed(title="Now playing", + description=f"[{source.title}]({source.web_url}) [{source.requester.mention}]", + color=discord.Color.green()) + self.np = await self.ctx.send(embed=embed) + await self.next.wait() + + # Make sure the FFmpeg process is cleaned up. + source.cleanup() + self.current = None + + 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 - # region bot.commands - - @commands.command(name='play', help='To play song') - async def play(self, ctx, url=None): - global history - global currentVoiceClient - global currentVoiceChannel - global guildTextChannel - global currentTrack - global is_playing - - if currentTrack: - history.append(currentTrack) - currentTrack = None - if url is None: - if song_queue: - track = song_queue.pop() - currentTrack = track - url = track.url - else: - await ctx.send('Nothing to play') - return try: - await self.join(ctx) - if ctx is not None and ctx.message.guild.voice_client is not None and ctx.message.guild.voice_channels[ - 0] is not None: - currentVoiceClient = ctx.message.guild.voice_client - currentVoiceChannel = ctx.message.guild.voice_channels[0] - for text_channel in ctx.message.guild.text_channels: - if str(text_channel) == "bot-control": - guildTextChannel = text_channel - - await self.download_song_data(url) - - except RuntimeError as err: - print(f"Unexpected {err=}, {type(err)=}") - - @commands.command(name='add', help='Adds a track to the queue') - async def add_song(self, ctx, url): - global currentTrack + 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: - async with ctx.typing(): - file_data = await YTDLSource.from_url(url, loops=self.bot.loop) - # If there is a playlist: - if type(file_data) is list: - add_songs_to_song_queue(file_data) - # If single track: - else: - song_queue.appendleft( - Track(file_data['artist'] + ' - ' + file_data['title'], file_data['webpage_url'])) - await self.print_queue(ctx) - except: - await ctx.send("Some error occurred while accessing ytdl") - - @commands.command(name='queue', help='Prints queue and previous songs') - async def print_queue(self, ctx): - s = "" - s += "-------------Previous-------------\n" - if len(history) > 0: - for track in history: - s += track.filename + "\n" - if currentTrack: - s += "--------------Playing-------------\n" - s += "**" + currentTrack.filename + "** \n" - s += "-------------In Queue-------------\n" - # Reversed for songs to upper in order: next song on toppy - for track in reversed(song_queue): - s += track.filename + "\n" - - await ctx.send(s) - - @commands.command(name='next', help='') - async def next_song(self, ctx): - await self.play(ctx) - - @commands.command(name='prev', help='') - async def prev(self, ctx): - global song_queue - track = history.pop() - song_queue.append(currentTrack) - await self.download_song_data(track.url) - - @commands.command(name='join', help='Tells the bot to join the voice channel') - async def join(self, ctx): - if ctx is None: + player = self.players[ctx.guild.id] + except KeyError: + player = MusicPlayer(ctx) + self.players[ctx.guild.id] = player + + return player + + @commands.command(name='join', aliases=['connect', 'j'], description="connects to voice") + 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: + embed = discord.Embed(title="", + description="No channel to join. Please call `,join` from a voice channel.", + color=discord.Color.green()) + await ctx.send(embed=embed) + 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.') + if (random.randint(0, 1) == 0): + await ctx.message.add_reaction('πŸ‘') + await ctx.send(f'**Joined `{channel}`**') + + @commands.command(name='play', aliases=['sing', 'p'], description="streams music") + 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', description="pauses music") + async def pause_(self, ctx): + """Pause the currently playing song.""" + vc = ctx.voice_client + + if not vc or not vc.is_playing(): + embed = discord.Embed(title="", description="I am currently not playing anything", + color=discord.Color.green()) + return await ctx.send(embed=embed) + elif vc.is_paused(): return - if not ctx.message.author.voice: - await ctx.send("{} is not connected to a voice channel".format(ctx.message.author.name)) + + vc.pause() + await ctx.send("Paused ⏸️") + + @commands.command(name='resume', description="resumes music") + async def resume_(self, ctx): + """Resume the currently paused song.""" + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", + color=discord.Color.green()) + return await ctx.send(embed=embed) + elif not vc.is_paused(): return - if ctx.message.guild.voice_client: + + vc.resume() + await ctx.send("Resuming ⏯️") + + @commands.command(name='skip', description="skips to next song in queue") + async def skip_(self, ctx): + """Skip the song.""" + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + if vc.is_paused(): + pass + elif not vc.is_playing(): return - else: - channel = ctx.message.author.voice.channel - await channel.connect() - - @commands.command(name='pause', help='This command pauses the song') - async def pause(self, ctx): - voice_client = ctx.message.guild.voice_client - if voice_client.is_playing(): - voice_client.pause() - else: - await ctx.send("The bot is not playing anything at the moment.") - @commands.command(name='resume', help='Resumes the song') - async def resume(self, ctx): - voice_client = ctx.message.guild.voice_client - if voice_client.is_paused(): - voice_client.resume() - else: - await ctx.send("The bot was not playing anything before this. Use play_song command") - - @commands.command(name='leave', help='To make the bot leave the voice channel') - async def leave(self, ctx): - voice_client = ctx.message.guild.voice_client - if voice_client.is_connected(): - song_queue.clear() - history.clear() - await voice_client.disconnect() + vc.stop() + + @commands.command(name='remove', aliases=['rm', 'rem'], description="removes specified song from queue") + async def remove_(self, ctx, pos: int = None): + """Removes specified song from queue""" + + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + if pos == None: + player.queue._queue.pop() else: - await ctx.send("The bot is not connected to a voice channel.") - - @commands.command(name='stop', help='Stops the song') - async def stop(self, ctx): - voice_client = ctx.message.guild.voice_client - global is_playing - global currentTrack - if voice_client.is_playing(): - voice_client.stop() - is_playing = False - history.append(currentTrack) - currentTrack = None - runningTask.cancel() + try: + s = player.queue._queue[pos - 1] + del player.queue._queue[pos - 1] + embed = discord.Embed(title="", + description=f"Removed [{s['title']}]({s['webpage_url']}) [{s['requester'].mention}]", + color=discord.Color.green()) + await ctx.send(embed=embed) + except: + embed = discord.Embed(title="", description=f'Could not find a track for "{pos}"', + color=discord.Color.green()) + await ctx.send(embed=embed) + + @commands.command(name='clear', aliases=['clr', 'cl', 'cr'], description="clears entire queue") + async def clear_(self, ctx): + """Deletes entire queue of upcoming songs.""" + + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + player.queue._queue.clear() + await ctx.send('**Cleared**') + + @commands.command(name='queue', aliases=['q', 'playlist', 'que'], description="shows the 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(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + if player.queue.empty(): + embed = discord.Embed(title="", description="queue is empty", color=discord.Color.green()) + return await ctx.send(embed=embed) + + seconds = vc.source.duration % (24 * 3600) + hour = seconds // 3600 + seconds %= 3600 + minutes = seconds // 60 + seconds %= 60 + if hour > 0: + duration = "%dh %02dm %02ds" % (hour, minutes, seconds) else: - await ctx.send("The bot is not playing anything at the moment.") + duration = "%02dm %02ds" % (minutes, seconds) - # endregion + # Grabs the songs in the queue... + upcoming = list(itertools.islice(player.queue._queue, 0, int(len(player.queue._queue)))) + fmt = '\n'.join( + f"`{(upcoming.index(_)) + 1}.` [{_['title']}]({_['webpage_url']}) | ` {duration} Requested by: {_['requester']}`\n" + for _ in upcoming) + fmt = f"\n__Now Playing__:\n[{vc.source.title}]({vc.source.web_url}) | ` {duration} Requested by: {vc.source.requester}`\n\n__Up Next:__\n" + fmt + f"\n**{len(upcoming)} songs in queue**" + embed = discord.Embed(title=f'Queue for {ctx.guild.name}', description=fmt, color=discord.Color.green()) + embed.set_footer(text=f"{ctx.author.display_name}", icon_url=ctx.author.avatar_url) - async def download_song_data(self, url): - data = await YTDLSource.from_url(url, loops=self.bot.loop) - if type(data) is list: # If playlist - add_songs_to_song_queue(data[1:]) - await self.play_song(data[0]) + await ctx.send(embed=embed) + + @commands.command(name='np', aliases=['song', 'current', 'currentsong', 'playing'], + description="shows the current playing 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(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + if not player.current: + embed = discord.Embed(title="", description="I am currently not playing anything", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + seconds = vc.source.duration % (24 * 3600) + hour = seconds // 3600 + seconds %= 3600 + minutes = seconds // 60 + seconds %= 60 + if hour > 0: + duration = "%dh %02dm %02ds" % (hour, minutes, seconds) else: - await self.play_song(data) - return data - - async def play_song(self, song): - global currentTrack, is_playing, currentVoiceClient, runningTask - file_name = song['artist'] + ' - ' + song['title'] - duration = song['duration'] - currentTrack = Track(file_name, song['webpage_url']) - audio_stream = discord.FFmpegPCMAudio(executable="ffmpeg.exe", source=song['url'], options='-vn', - before_options="-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5") - print('Song Duration: ', duration) - is_playing = True - await guildTextChannel.send('Playing **' + file_name + '**') - currentVoiceClient.stop() - currentVoiceClient.play(audio_stream, after=lambda e: print('Player error: %s' % e) if e else None) - if runningTask: - runningTask.cancel() - runningTask = None - runningTask = asyncio.create_task(self.play_next_on_end(duration)) - await runningTask - - async def play_next_on_end(self, duration): - try: - await asyncio.sleep(duration) - except asyncio.CancelledError: - print('cancel sleep') - return - print('End sleep, playing next') - if song_queue: - history.append(currentTrack) - track = song_queue.pop() - loop = get_or_create_event_loop() - loop.create_task(self.download_song_data(track.url)) + duration = "%02dm %02ds" % (minutes, seconds) + + embed = discord.Embed(title="", + description=f"[{vc.source.title}]({vc.source.web_url}) [{vc.source.requester.mention}] | `{duration}`", + color=discord.Color.green()) + embed.set_author(icon_url=self.bot.user.avatar_url, name=f"Now Playing 🎢") + await ctx.send(embed=embed) + + @commands.command(name='volume', aliases=['vol', 'v'], description="changes Kermit's volume") + async def change_volume(self, ctx, *, vol: float = None): + """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(): + embed = discord.Embed(title="", description="I am not currently connected to voice", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + if not vol: + embed = discord.Embed(title="", description=f"πŸ”Š **{(vc.source.volume) * 100}%**", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + if not 0 < vol < 101: + embed = discord.Embed(title="", description="Please enter a value between 1 and 100", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + + if vc.source: + vc.source.volume = vol / 100 + + player.volume = vol / 100 + embed = discord.Embed(title="", description=f'**`{ctx.author}`** set the volume to **{vol}%**', + color=discord.Color.green()) + await ctx.send(embed=embed) + + @commands.command(name='leave', aliases=["stop", "dc", "disconnect", "bye"], + description="stops music and disconnects from voice") + async def leave_(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(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", + color=discord.Color.green()) + return await ctx.send(embed=embed) + + if (random.randint(0, 1) == 0): + await ctx.message.add_reaction('πŸ‘‹') + await ctx.send('**Successfully disconnected**') + + await self.cleanup(ctx.guild) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dd35025 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3.9" +services: + bot: + build: . + environment: + DISCORD_TOKEN: ${DISCORD_TOKEN} + PYTHONPATH: "." + command: python3 Dizplayer.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ad195a7..5f70e53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ discord==1.0.1 discord.py==1.6.0 python-dotenv==0.15.0 -youtube-dl==2021.2.10 -PyNaCl==1.4.0 -nest-asyncio==1.5.4 \ No newline at end of file +youtube-dl==2021.12.17 +PyNaCl==1.5.0 +nest-asyncio==1.5.6 \ No newline at end of file From 09d2fcdb9e261d527cbdff225b61f67cc4b754cb Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Sun, 23 Oct 2022 18:58:41 -0700 Subject: [PATCH 02/11] add readme and listner --- Dizplayer.py | 2 ++ README.md | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 README.md diff --git a/Dizplayer.py b/Dizplayer.py index d64bedd..7b2594a 100644 --- a/Dizplayer.py +++ b/Dizplayer.py @@ -5,6 +5,7 @@ from dotenv import load_dotenv from Music import Music +from Listener import ListenerCog load_dotenv() # Get the API token from the .env file. @@ -19,4 +20,5 @@ if __name__ == "__main__": bot.add_cog(Music(bot)) + bot.add_cog(ListenerCog(bot)) bot.run(DISCORD_TOKEN) diff --git a/README.md b/README.md new file mode 100644 index 0000000..a595d0b --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +#Run + +`DISCORD_TOKEN= docker-compose up` \ No newline at end of file From a59feb0ebbc50d0e8b71a57472ecdb7d3b30ed26 Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Fri, 11 Nov 2022 10:56:09 -0800 Subject: [PATCH 03/11] set default volume to 3% --- Music.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Music.py b/Music.py index b5cf426..3bf542b 100644 --- a/Music.py +++ b/Music.py @@ -206,14 +206,18 @@ async def __error(self, ctx, error): 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): + async 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 - + default_volume_percentage = 3 + player.volume = default_volume_percentage / 100 + embed = discord.Embed(title="", description=f'**Player Spun Up** setting the default volume to **{default_volume_percentage}%**', + color=discord.Color.green()) + await ctx.send(embed=embed) return player @commands.command(name='join', aliases=['connect', 'j'], description="connects to voice") @@ -271,7 +275,7 @@ async def play_(self, ctx, *, search: str): if not vc: await ctx.invoke(self.connect_) - player = self.get_player(ctx) + player = await 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. @@ -337,7 +341,7 @@ async def remove_(self, ctx, pos: int = None): color=discord.Color.green()) return await ctx.send(embed=embed) - player = self.get_player(ctx) + player = await self.get_player(ctx) if pos == None: player.queue._queue.pop() else: @@ -364,7 +368,7 @@ async def clear_(self, ctx): color=discord.Color.green()) return await ctx.send(embed=embed) - player = self.get_player(ctx) + player = await self.get_player(ctx) player.queue._queue.clear() await ctx.send('**Cleared**') @@ -378,7 +382,7 @@ async def queue_info(self, ctx): color=discord.Color.green()) return await ctx.send(embed=embed) - player = self.get_player(ctx) + player = await self.get_player(ctx) if player.queue.empty(): embed = discord.Embed(title="", description="queue is empty", color=discord.Color.green()) return await ctx.send(embed=embed) @@ -415,7 +419,7 @@ async def now_playing_(self, ctx): color=discord.Color.green()) return await ctx.send(embed=embed) - player = self.get_player(ctx) + player = await self.get_player(ctx) if not player.current: embed = discord.Embed(title="", description="I am currently not playing anything", color=discord.Color.green()) @@ -462,7 +466,7 @@ async def change_volume(self, ctx, *, vol: float = None): color=discord.Color.green()) return await ctx.send(embed=embed) - player = self.get_player(ctx) + player = await self.get_player(ctx) if vc.source: vc.source.volume = vol / 100 From 34f64b8fa51f1115d4596d88089d5045072b9d7f Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Fri, 11 Nov 2022 14:55:59 -0800 Subject: [PATCH 04/11] repeat functionality added --- Music.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/Music.py b/Music.py index 3bf542b..6fb4b47 100644 --- a/Music.py +++ b/Music.py @@ -105,7 +105,7 @@ class MusicPlayer: When the bot disconnects from the Voice it's instance will be destroyed. """ - __slots__ = ('bot', '_guild', '_channel', '_cog', 'queue', 'next', 'current', 'np', 'volume', 'ctx') + __slots__ = ('bot', '_guild', '_channel', '_cog', 'queue', 'next', 'current', 'np', 'volume', 'ctx', 'repeat', 'current_source') def __init__(self, ctx): self.bot = ctx.bot @@ -120,6 +120,8 @@ def __init__(self, ctx): self.volume = .5 self.current = None self.ctx = ctx + self.repeat = False + self.current_source = None ctx.bot.loop.create_task(self.player_loop()) @@ -136,29 +138,32 @@ async def player_loop(self): source = await self.queue.get() except asyncio.TimeoutError: return self.destroy(self._guild) - + self.current_source = source 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) + gathered_source = await YTDLSource.regather_stream(source, loop=self.bot.loop) except Exception as e: await self.ctx.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)) + gathered_source.volume = self.volume + self.current = gathered_source + if self.repeat: + # new_source = await YTDLSource.create_source(self.ctx, source['search'], loop=self.bot.loop, download=False) + # new_source.search = search + self.queue._queue.appendleft(source) + self._guild.voice_client.play(gathered_source, after=lambda _: self.bot.loop.call_soon_threadsafe(self.next.set)) embed = discord.Embed(title="Now playing", - description=f"[{source.title}]({source.web_url}) [{source.requester.mention}]", + description=f"[{gathered_source.title}]({gathered_source.web_url}) [{gathered_source.requester.mention}]", color=discord.Color.green()) self.np = await self.ctx.send(embed=embed) await self.next.wait() # Make sure the FFmpeg process is cleaned up. - source.cleanup() + gathered_source.cleanup() self.current = None def destroy(self, guild): @@ -283,6 +288,18 @@ async def play_(self, ctx, *, search: str): await player.queue.put(source) + @commands.command(name='repeat', description="repeats song until called again") + async def repeat_(self, ctx): + """Repeat the currently paused song.""" + player = await self.get_player(ctx) + player.repeat = not player.repeat + if not player.repeat: + player.queue._queue.popleft() + if player.repeat: + player.queue._queue.appendleft(player.current_source) + + await ctx.send(f"Repeat πŸ”οΈ toggled to {player.repeat}") + @commands.command(name='pause', description="pauses music") async def pause_(self, ctx): """Pause the currently playing song.""" From f5061325e8db59791637f10ae84cfcb00acc08e3 Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Wed, 16 Nov 2022 18:56:38 -0800 Subject: [PATCH 05/11] add a mongo db for permanently tracking server volume settings --- .env | 3 ++- Dizplayer.py | 5 +++++ Music.py | 19 ++++++++++++++++++- docker-compose.yml | 10 +++++++++- requirements.txt | 3 ++- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 32a0035..620980c 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -discord_token = "add_bot_token_here" \ No newline at end of file +DISCORD_TOKEN = "add_bot_token_here" +MONGO_URI = "mongodb://mongodb:27017" \ No newline at end of file diff --git a/Dizplayer.py b/Dizplayer.py index 7b2594a..6dd0899 100644 --- a/Dizplayer.py +++ b/Dizplayer.py @@ -6,10 +6,12 @@ from Music import Music from Listener import ListenerCog +import pymongo load_dotenv() # Get the API token from the .env file. DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") +MONGO_URI = os.getenv("MONGO_URI") print(DISCORD_TOKEN) initial_extensions = ['cogs.listener', @@ -18,6 +20,9 @@ intents = discord.Intents().all() bot = commands.Bot(command_prefix='!', intents=intents) +mongo_client = pymongo.MongoClient(MONGO_URI) +bot.mongo_db = mongo_client.youtube_streamer + if __name__ == "__main__": bot.add_cog(Music(bot)) bot.add_cog(ListenerCog(bot)) diff --git a/Music.py b/Music.py index 6fb4b47..7ccadb0 100644 --- a/Music.py +++ b/Music.py @@ -216,9 +216,19 @@ async def get_player(self, ctx): try: player = self.players[ctx.guild.id] except KeyError: + guilds = self.bot.mongo_db.guilds + this_guild = guilds.find_one({"guild_id": ctx.guild.id}) + + if not this_guild: + inserted = guilds.insert_one({ + "guild_id": ctx.guild.id, + "volume": 5 + }) + this_guild = guilds.find_one({"guild_id": ctx.guild.id}) + player = MusicPlayer(ctx) self.players[ctx.guild.id] = player - default_volume_percentage = 3 + default_volume_percentage = this_guild['volume'] player.volume = default_volume_percentage / 100 embed = discord.Embed(title="", description=f'**Player Spun Up** setting the default volume to **{default_volume_percentage}%**', color=discord.Color.green()) @@ -491,6 +501,13 @@ async def change_volume(self, ctx, *, vol: float = None): player.volume = vol / 100 embed = discord.Embed(title="", description=f'**`{ctx.author}`** set the volume to **{vol}%**', color=discord.Color.green()) + + #store new volume on guild in mongo + query = {"guild_id": ctx.guild.id} + new_value = {"$set": {"volume": vol}} + guilds = self.bot.mongo_db.guilds + guilds.update_one(query, new_value) + await ctx.send(embed=embed) @commands.command(name='leave', aliases=["stop", "dc", "disconnect", "bye"], diff --git a/docker-compose.yml b/docker-compose.yml index dd35025..0d6ecc8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,4 +5,12 @@ services: environment: DISCORD_TOKEN: ${DISCORD_TOKEN} PYTHONPATH: "." - command: python3 Dizplayer.py \ No newline at end of file + command: python3 Dizplayer.py + mongodb: + image: mongo:6.0.3 + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db +volumes: + mongo_data: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5f70e53..8e35640 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ discord.py==1.6.0 python-dotenv==0.15.0 youtube-dl==2021.12.17 PyNaCl==1.5.0 -nest-asyncio==1.5.6 \ No newline at end of file +nest-asyncio==1.5.6 +pymongo==4.3.2 \ No newline at end of file From 463e59a7977b8f15ecbd2c1d2cb08ec38ced77d6 Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Wed, 16 Nov 2022 19:19:06 -0800 Subject: [PATCH 06/11] add guild name to doc --- Music.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Music.py b/Music.py index 7ccadb0..ec54bf8 100644 --- a/Music.py +++ b/Music.py @@ -222,6 +222,7 @@ async def get_player(self, ctx): if not this_guild: inserted = guilds.insert_one({ "guild_id": ctx.guild.id, + "name": ctx.guild.name, "volume": 5 }) this_guild = guilds.find_one({"guild_id": ctx.guild.id}) From 8d4d2d79fc29d6b7aea47bce93e78cdfffa5ed5d Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Fri, 18 Nov 2022 18:36:32 -0800 Subject: [PATCH 07/11] added system to whitelist new servers --- .env | 3 ++- Dizplayer.py | 31 ++++++++++++++++++++++++++++++- Music.py | 17 ++++++++++------- README.md | 12 +++++++++++- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/.env b/.env index 620980c..c26d61e 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ DISCORD_TOKEN = "add_bot_token_here" -MONGO_URI = "mongodb://mongodb:27017" \ No newline at end of file +MONGO_URI = "mongodb://mongodb:27017" +BOT_OWNER_USER_NAME = "add bot owner user name here example: name#0001" \ No newline at end of file diff --git a/Dizplayer.py b/Dizplayer.py index 6dd0899..5d047a5 100644 --- a/Dizplayer.py +++ b/Dizplayer.py @@ -3,15 +3,16 @@ import discord from discord.ext import commands from dotenv import load_dotenv - from Music import Music from Listener import ListenerCog import pymongo +import datetime load_dotenv() # Get the API token from the .env file. DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") MONGO_URI = os.getenv("MONGO_URI") +BOT_OWNER_USER_NAME = os.getenv("BOT_OWNER_USER_NAME") print(DISCORD_TOKEN) initial_extensions = ['cogs.listener', @@ -23,6 +24,34 @@ mongo_client = pymongo.MongoClient(MONGO_URI) bot.mongo_db = mongo_client.youtube_streamer +@bot.event +async def on_guild_join(guild): + guilds = bot.mongo_db.guilds + this_guild = guilds.find_one({"guild_id": guild.id}) + if this_guild: + return + + inserted = guilds.insert_one({ + "guild_id": guild.id, + "name": guild.name, + "volume": 5, + "authorized": False, + "added_by": { + "name": str(guild.owner), + "id": guild.owner.id + }, + "created_at": datetime.datetime.utcnow() + }) + + guild_owner = await bot.fetch_user(guild.owner.id) + await guild_owner.send(f"You recently added me to {guild.name}. I cannot be used until authorized by whoever is running the bot. Please contact the bot admin to request authorization for your server.") + + owner_name = os.getenv("BOT_OWNER_USER_NAME").split('#')[0] + discriminator = os.getenv("BOT_OWNER_USER_NAME").split('#')[1] + bot_owner = discord.utils.get(bot.get_all_members(), name=owner_name, discriminator=discriminator) + await bot_owner.send(f"A new Guild is attempting to use the bot {guild.name} ({guild.id})\n" + f"Guild owner contact: {str(guild.owner)} ({guild.owner.id})") + if __name__ == "__main__": bot.add_cog(Music(bot)) bot.add_cog(ListenerCog(bot)) diff --git a/Music.py b/Music.py index ec54bf8..f18705c 100644 --- a/Music.py +++ b/Music.py @@ -43,6 +43,10 @@ class InvalidVoiceChannel(VoiceConnectionError): """Exception for cases of invalid Voice Channels.""" +class GuildNotAuthorized(VoiceConnectionError): + """This guild is not authorized to use the bot""" + + class YTDLSource(discord.PCMVolumeTransformer): def __init__(self, source, *, data, requester): @@ -219,13 +223,12 @@ async def get_player(self, ctx): guilds = self.bot.mongo_db.guilds this_guild = guilds.find_one({"guild_id": ctx.guild.id}) - if not this_guild: - inserted = guilds.insert_one({ - "guild_id": ctx.guild.id, - "name": ctx.guild.name, - "volume": 5 - }) - this_guild = guilds.find_one({"guild_id": ctx.guild.id}) + if not this_guild or not this_guild['authorized']: + embed = discord.Embed(title="Error", + description=f'{ctx.guild.name} ({ctx.guild.id}) has not been authorized to use the streamer. Please request authorization.', color=discord.Color.green()) + await ctx.send(embed=embed) + raise GuildNotAuthorized( + f'{ctx.guild.name} ({ctx.guild.id}) has not been authorized to use the streamer. Please request authorization.') player = MusicPlayer(ctx) self.players[ctx.guild.id] = player diff --git a/README.md b/README.md index a595d0b..1980c41 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ #Run -`DISCORD_TOKEN= docker-compose up` \ No newline at end of file +`DISCORD_TOKEN= BOT_OWNER_USER_NAME= docker-compose up` + +## All valid env vars +DISCORD_TOKEN +MONGO_URI +BOT_OWNER_USER_NAME + +#Authorize Guilds + +Whenever a new server tries to run the bot, it must be explicitly authorized. Connect to the mongo db instance using +mongo compass, retrieve the doc for the server in youtube_streamer.guilds, and set `authorized` to `True`. \ No newline at end of file From 3e98244c3457f36be28edca6424ec620cc9321db Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Sun, 20 Nov 2022 23:24:37 -0800 Subject: [PATCH 08/11] fix bot owner name var --- Dizplayer.py | 3 +-- docker-compose.yml | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dizplayer.py b/Dizplayer.py index 5d047a5..c427d8f 100644 --- a/Dizplayer.py +++ b/Dizplayer.py @@ -10,10 +10,9 @@ load_dotenv() # Get the API token from the .env file. +BOT_OWNER_USER_NAME = os.getenv("BOT_OWNER_USER_NAME") DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") MONGO_URI = os.getenv("MONGO_URI") -BOT_OWNER_USER_NAME = os.getenv("BOT_OWNER_USER_NAME") -print(DISCORD_TOKEN) initial_extensions = ['cogs.listener', 'cogs.music'] diff --git a/docker-compose.yml b/docker-compose.yml index 0d6ecc8..077daed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,7 @@ services: build: . environment: DISCORD_TOKEN: ${DISCORD_TOKEN} + BOT_OWNER_USER_NAME: ${BOT_OWNER_USER_NAME} PYTHONPATH: "." command: python3 Dizplayer.py mongodb: From ca72ba3d9f660f51a5fe597e3cd8f80b96863cc3 Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Mon, 5 Dec 2022 13:48:02 -0800 Subject: [PATCH 09/11] begin work on slash commands --- Dizplayer.py | 33 +++++++++++++++++++++- Music.py | 71 ++++++++++++++++++++++++++++++++++++++++++++---- decorators.py | 9 ++++++ requirements.txt | 3 +- 4 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 decorators.py diff --git a/Dizplayer.py b/Dizplayer.py index c427d8f..3c583cc 100644 --- a/Dizplayer.py +++ b/Dizplayer.py @@ -1,6 +1,7 @@ import os import discord +from discord import Option from discord.ext import commands from dotenv import load_dotenv from Music import Music @@ -8,6 +9,8 @@ import pymongo import datetime +from YTDL import YTDLSource + load_dotenv() # Get the API token from the .env file. BOT_OWNER_USER_NAME = os.getenv("BOT_OWNER_USER_NAME") @@ -23,6 +26,35 @@ mongo_client = pymongo.MongoClient(MONGO_URI) bot.mongo_db = mongo_client.youtube_streamer + +@bot.slash_command(name='play', description="streams music") +async def play_(ctx: discord.ApplicationContext, + *, + search: Option(str, + description="The song to search and retrieve using YTDL. This could be a simple search, an ID or URL.", + required=True)): + """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. + + Args: + search (str): 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 = await 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) + @bot.event async def on_guild_join(guild): guilds = bot.mongo_db.guilds @@ -52,6 +84,5 @@ async def on_guild_join(guild): f"Guild owner contact: {str(guild.owner)} ({guild.owner.id})") if __name__ == "__main__": - bot.add_cog(Music(bot)) bot.add_cog(ListenerCog(bot)) bot.run(DISCORD_TOKEN) diff --git a/Music.py b/Music.py index f18705c..7bfc974 100644 --- a/Music.py +++ b/Music.py @@ -1,4 +1,5 @@ import discord +from discord import Option from discord.ext import commands import random import asyncio @@ -9,6 +10,8 @@ from functools import partial import youtube_dl from youtube_dl import YoutubeDL +from decorators import copy_doc + # Suppress noise about console usage from errors youtube_dl.utils.bug_reports_message = lambda: '' @@ -183,6 +186,20 @@ class Music(commands.Cog): def __init__(self, bot): self.bot = bot self.players = {} + self.bot.application_command(name="join", cls=discord.SlashCommand)(self.join_slash) + self.bot.application_command(name="play", cls=discord.SlashCommand)(self.play_slash) + self.bot.application_command(name="pause", cls=discord.SlashCommand)(self.pause_slash) + self.bot.application_command(name="resume", cls=discord.SlashCommand)(self.resume_slash) + self.bot.application_command(name="skip", cls=discord.SlashCommand)(self.skip_slash) + self.bot.application_command(name="volume", cls=discord.SlashCommand)(self.volume_slash) + self.bot.application_command(name="queue", cls=discord.SlashCommand)(self.queue_slash) + self.bot.application_command(name="now_playing", cls=discord.SlashCommand)(self.now_playing_slash) + self.bot.application_command(name="repeat", cls=discord.SlashCommand)(self.repeat_slash) + self.bot.application_command(name="remove", cls=discord.SlashCommand)(self.remove_slash) + self.bot.application_command(name="clear", cls=discord.SlashCommand)(self.clear_slash) + self.bot.application_command(name="leave", cls=discord.SlashCommand)(self.leave_slash) + + async def cleanup(self, guild): try: @@ -273,19 +290,22 @@ async def connect_(self, ctx, *, channel: discord.VoiceChannel = None): await channel.connect() except asyncio.TimeoutError: raise VoiceConnectionError(f'Connecting to channel: <{channel}> timed out.') - if (random.randint(0, 1) == 0): - await ctx.message.add_reaction('πŸ‘') + await ctx.send(f'**Joined `{channel}`**') + async def join_slash(self, + ctx: commands.Context, + channel: discord.VoiceChannel = None): + await self.connect_(ctx, channel=channel) + @commands.command(name='play', aliases=['sing', 'p'], description="streams music") 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. + + Args: + search (str): The song to search and retrieve using YTDL. This could be a simple search, an ID or URL. """ await ctx.trigger_typing() @@ -302,6 +322,15 @@ async def play_(self, ctx, *, search: str): await player.queue.put(source) + async def play_slash(self, + ctx, + *, + search=Option(str, + description="The song to search and retrieve using YTDL. This could be a simple search, an ID or URL.", + required=True)): + """Request a song and add it to the queue.""" + await self.play_(ctx, search=search) + @commands.command(name='repeat', description="repeats song until called again") async def repeat_(self, ctx): """Repeat the currently paused song.""" @@ -314,6 +343,9 @@ async def repeat_(self, ctx): await ctx.send(f"Repeat πŸ”οΈ toggled to {player.repeat}") + async def repeat_slash(self, ctx): + await self.repeat_(ctx) + @commands.command(name='pause', description="pauses music") async def pause_(self, ctx): """Pause the currently playing song.""" @@ -329,6 +361,9 @@ async def pause_(self, ctx): vc.pause() await ctx.send("Paused ⏸️") + async def pause_slash(self, ctx): + await self.pause_(ctx) + @commands.command(name='resume', description="resumes music") async def resume_(self, ctx): """Resume the currently paused song.""" @@ -344,6 +379,9 @@ async def resume_(self, ctx): vc.resume() await ctx.send("Resuming ⏯️") + async def resume_slash(self, ctx): + await self.resume_(ctx) + @commands.command(name='skip', description="skips to next song in queue") async def skip_(self, ctx): """Skip the song.""" @@ -361,6 +399,9 @@ async def skip_(self, ctx): vc.stop() + async def skip_slash(self, ctx): + await self.skip_(ctx) + @commands.command(name='remove', aliases=['rm', 'rem'], description="removes specified song from queue") async def remove_(self, ctx, pos: int = None): """Removes specified song from queue""" @@ -388,6 +429,9 @@ async def remove_(self, ctx, pos: int = None): color=discord.Color.green()) await ctx.send(embed=embed) + async def remove_slash(self, ctx, pos: int = None): + await self.remove_(ctx, pos=pos) + @commands.command(name='clear', aliases=['clr', 'cl', 'cr'], description="clears entire queue") async def clear_(self, ctx): """Deletes entire queue of upcoming songs.""" @@ -403,6 +447,9 @@ async def clear_(self, ctx): player.queue._queue.clear() await ctx.send('**Cleared**') + async def clear_slash(self, ctx): + await self.clear_(ctx) + @commands.command(name='queue', aliases=['q', 'playlist', 'que'], description="shows the queue") async def queue_info(self, ctx): """Retrieve a basic queue of upcoming songs.""" @@ -439,6 +486,9 @@ async def queue_info(self, ctx): await ctx.send(embed=embed) + async def queue_slash(self, ctx): + await self.queue_info(ctx) + @commands.command(name='np', aliases=['song', 'current', 'currentsong', 'playing'], description="shows the current playing song") async def now_playing_(self, ctx): @@ -472,6 +522,9 @@ async def now_playing_(self, ctx): embed.set_author(icon_url=self.bot.user.avatar_url, name=f"Now Playing 🎢") await ctx.send(embed=embed) + async def now_playing_slash(self, ctx): + await self.now_playing_(ctx) + @commands.command(name='volume', aliases=['vol', 'v'], description="changes Kermit's volume") async def change_volume(self, ctx, *, vol: float = None): """Change the player volume. @@ -514,6 +567,9 @@ async def change_volume(self, ctx, *, vol: float = None): await ctx.send(embed=embed) + async def volume_slash(self, ctx, vol: float = None): + await self.change_volume(ctx, vol) + @commands.command(name='leave', aliases=["stop", "dc", "disconnect", "bye"], description="stops music and disconnects from voice") async def leave_(self, ctx): @@ -533,3 +589,6 @@ async def leave_(self, ctx): await ctx.send('**Successfully disconnected**') await self.cleanup(ctx.guild) + + async def leave_slash(self, ctx): + await self.leave_(ctx) diff --git a/decorators.py b/decorators.py new file mode 100644 index 0000000..829ce6f --- /dev/null +++ b/decorators.py @@ -0,0 +1,9 @@ +from typing import Callable + + +def copy_doc(copy_func: Callable) -> Callable: + """Use Example: copy_doc(self.copy_func)(self.func) or used as deco""" + def wrapper(func: Callable) -> Callable: + func.__doc__ = copy_func.__doc__ + return func + return wrapper \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8e35640..ac7e8f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ -discord==1.0.1 -discord.py==1.6.0 +py-cord==2.3.2 python-dotenv==0.15.0 youtube-dl==2021.12.17 PyNaCl==1.5.0 From 8df3f119a38ce60c8f1d26bea585adb615e49e5f Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Mon, 5 Dec 2022 14:35:09 -0800 Subject: [PATCH 10/11] slash commands implemented --- Dizplayer.py | 33 ++------------------------------- Music.py | 48 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/Dizplayer.py b/Dizplayer.py index 3c583cc..a27bb3e 100644 --- a/Dizplayer.py +++ b/Dizplayer.py @@ -1,15 +1,13 @@ import os import discord -from discord import Option from discord.ext import commands from dotenv import load_dotenv -from Music import Music from Listener import ListenerCog import pymongo import datetime -from YTDL import YTDLSource +from Music import Music load_dotenv() # Get the API token from the .env file. @@ -27,34 +25,6 @@ bot.mongo_db = mongo_client.youtube_streamer -@bot.slash_command(name='play', description="streams music") -async def play_(ctx: discord.ApplicationContext, - *, - search: Option(str, - description="The song to search and retrieve using YTDL. This could be a simple search, an ID or URL.", - required=True)): - """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. - - Args: - search (str): 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 = await 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) - @bot.event async def on_guild_join(guild): guilds = bot.mongo_db.guilds @@ -84,5 +54,6 @@ async def on_guild_join(guild): f"Guild owner contact: {str(guild.owner)} ({guild.owner.id})") if __name__ == "__main__": + bot.add_cog(Music(bot)) bot.add_cog(ListenerCog(bot)) bot.run(DISCORD_TOKEN) diff --git a/Music.py b/Music.py index 7bfc974..6eebde8 100644 --- a/Music.py +++ b/Music.py @@ -10,6 +10,7 @@ from functools import partial import youtube_dl from youtube_dl import YoutubeDL + from decorators import copy_doc @@ -295,7 +296,12 @@ async def connect_(self, ctx, *, channel: discord.VoiceChannel = None): async def join_slash(self, ctx: commands.Context, - channel: discord.VoiceChannel = None): + channel: Option(discord.VoiceChannel, + description="The channel to connect to. By default, the bot will attempt to join your current voice channel.", + required=False, + default=None)): + """Connect the bot to a voice channel""" + await ctx.respond(f'Request to join {channel} received. Processing...', ephemeral=True) await self.connect_(ctx, channel=channel) @commands.command(name='play', aliases=['sing', 'p'], description="streams music") @@ -323,12 +329,13 @@ async def play_(self, ctx, *, search: str): await player.queue.put(source) async def play_slash(self, - ctx, + ctx: commands.Context, *, - search=Option(str, - description="The song to search and retrieve using YTDL. This could be a simple search, an ID or URL.", + search: Option(str, + description="The song to search and retrieve. This could be a simple search, an ID or URL.", required=True)): """Request a song and add it to the queue.""" + await ctx.respond(f'Request to play {search} received. Processing...', ephemeral=True) await self.play_(ctx, search=search) @commands.command(name='repeat', description="repeats song until called again") @@ -344,6 +351,8 @@ async def repeat_(self, ctx): await ctx.send(f"Repeat πŸ”οΈ toggled to {player.repeat}") async def repeat_slash(self, ctx): + """Repeat the current song until 'repeat' called again.""" + await ctx.respond(f'Toggling repeat...', ephemeral=True) await self.repeat_(ctx) @commands.command(name='pause', description="pauses music") @@ -362,6 +371,8 @@ async def pause_(self, ctx): await ctx.send("Paused ⏸️") async def pause_slash(self, ctx): + """Pause the currently playing song.""" + await ctx.respond(f'Pausing...', ephemeral=True) await self.pause_(ctx) @commands.command(name='resume', description="resumes music") @@ -380,6 +391,8 @@ async def resume_(self, ctx): await ctx.send("Resuming ⏯️") async def resume_slash(self, ctx): + """Resume the currently paused song.""" + await ctx.respond(f'Attempting to resume...', ephemeral=True) await self.resume_(ctx) @commands.command(name='skip', description="skips to next song in queue") @@ -400,6 +413,8 @@ async def skip_(self, ctx): vc.stop() async def skip_slash(self, ctx): + """Skip the song.""" + await ctx.respond(f'Attempting to skip...', ephemeral=True) await self.skip_(ctx) @commands.command(name='remove', aliases=['rm', 'rem'], description="removes specified song from queue") @@ -429,7 +444,11 @@ async def remove_(self, ctx, pos: int = None): color=discord.Color.green()) await ctx.send(embed=embed) - async def remove_slash(self, ctx, pos: int = None): + async def remove_slash(self, ctx, pos: Option(int, + description="The position of the song in the queue to remove.", + required=True)): + """Removes specified song from queue""" + await ctx.respond(f'Attempting to remove song at position {pos}...', ephemeral=True) await self.remove_(ctx, pos=pos) @commands.command(name='clear', aliases=['clr', 'cl', 'cr'], description="clears entire queue") @@ -448,6 +467,8 @@ async def clear_(self, ctx): await ctx.send('**Cleared**') async def clear_slash(self, ctx): + """Empties the queue.""" + await ctx.respond(f'Attempting to clear the queue...', ephemeral=True) await self.clear_(ctx) @commands.command(name='queue', aliases=['q', 'playlist', 'que'], description="shows the queue") @@ -487,6 +508,8 @@ async def queue_info(self, ctx): await ctx.send(embed=embed) async def queue_slash(self, ctx): + """Describes the queue of upcoming songs.""" + await ctx.respond(f'Attempting to describe the queue...', ephemeral=True) await self.queue_info(ctx) @commands.command(name='np', aliases=['song', 'current', 'currentsong', 'playing'], @@ -523,6 +546,8 @@ async def now_playing_(self, ctx): await ctx.send(embed=embed) async def now_playing_slash(self, ctx): + """Display information about the currently playing song.""" + await ctx.respond(f'Attempting to report now playing...', ephemeral=True) await self.now_playing_(ctx) @commands.command(name='volume', aliases=['vol', 'v'], description="changes Kermit's volume") @@ -567,7 +592,13 @@ async def change_volume(self, ctx, *, vol: float = None): await ctx.send(embed=embed) - async def volume_slash(self, ctx, vol: float = None): + async def volume_slash(self, ctx, *, vol: Option(float, + description="Volume level. Default is 5.", + required=True, + min_value=0, + max_value=100)): + """Change the player volume. A little goes a long way.""" + await ctx.respond(f'Attempting to change volume to {vol}...', ephemeral=True) await self.change_volume(ctx, vol) @commands.command(name='leave', aliases=["stop", "dc", "disconnect", "bye"], @@ -584,11 +615,12 @@ async def leave_(self, ctx): color=discord.Color.green()) return await ctx.send(embed=embed) - if (random.randint(0, 1) == 0): - await ctx.message.add_reaction('πŸ‘‹') await ctx.send('**Successfully disconnected**') await self.cleanup(ctx.guild) async def leave_slash(self, ctx): + """Stop the currently playing song and destroy the player.""" + await ctx.respond(f'Attempting to destroy the player...', ephemeral=True) await self.leave_(ctx) + From b563c4fd5e84240dd28e60ebe6bba458cd9bc753 Mon Sep 17 00:00:00 2001 From: Isaac Martin Date: Mon, 5 Dec 2022 23:17:17 -0800 Subject: [PATCH 11/11] slash commands fix introduced bugs --- Music.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Music.py b/Music.py index 6eebde8..c6689c5 100644 --- a/Music.py +++ b/Music.py @@ -503,7 +503,7 @@ async def queue_info(self, ctx): for _ in upcoming) fmt = f"\n__Now Playing__:\n[{vc.source.title}]({vc.source.web_url}) | ` {duration} Requested by: {vc.source.requester}`\n\n__Up Next:__\n" + fmt + f"\n**{len(upcoming)} songs in queue**" embed = discord.Embed(title=f'Queue for {ctx.guild.name}', description=fmt, color=discord.Color.green()) - embed.set_footer(text=f"{ctx.author.display_name}", icon_url=ctx.author.avatar_url) + embed.set_footer(text=f"{ctx.author.display_name}", icon_url=ctx.author.avatar.url) await ctx.send(embed=embed) @@ -542,7 +542,7 @@ async def now_playing_(self, ctx): embed = discord.Embed(title="", description=f"[{vc.source.title}]({vc.source.web_url}) [{vc.source.requester.mention}] | `{duration}`", color=discord.Color.green()) - embed.set_author(icon_url=self.bot.user.avatar_url, name=f"Now Playing 🎢") + embed.set_author(icon_url=self.bot.user.display_avatar.url, name=f"Now Playing 🎢") await ctx.send(embed=embed) async def now_playing_slash(self, ctx):