diff --git a/.gitignore b/.gitignore index cbd2202..518dfa7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ *.pyc /logs/ + +jdk-13.0.2/ diff --git a/Lavalink.jar b/Lavalink.jar new file mode 100644 index 0000000..00e887b Binary files /dev/null and b/Lavalink.jar differ diff --git a/application.yml b/application.yml new file mode 100644 index 0000000..5bf19d6 --- /dev/null +++ b/application.yml @@ -0,0 +1,49 @@ +server: # REST and WS server + port: 2333 + address: localhost +lavalink: + server: + password: "youshallnotpass" + sources: + youtube: true + bandcamp: true + soundcloud: true + twitch: true + vimeo: true + http: true + local: false + bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses + frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered + youtubePlaylistLoadLimit: 6 # Number of pages at 100 each + playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds + youtubeSearchEnabled: true + soundcloudSearchEnabled: true + gc-warnings: true + #ratelimit: + #ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks + #excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink + #strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch + #searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing + #retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times + +metrics: + prometheus: + enabled: false + endpoint: /metrics + +sentry: + dsn: "" + environment: "" +# tags: +# some_key: some_value +# another_key: another_value + +logging: + file: + max-history: 30 + max-size: 1GB + path: ./logs/ + + level: + root: INFO + lavalink: INFO diff --git a/cogs/musicbot.py b/cogs/musicbot.py new file mode 100644 index 0000000..d61d70e --- /dev/null +++ b/cogs/musicbot.py @@ -0,0 +1,129 @@ +# Inspired by https://github.com/afazio1/robotic-nation-proj/blob/main/projects/discord-bot/new-music-bot/music-yt.py + +import os +import subprocess +import time + +import discord +import requests +import wavelink +from discord.ext import commands +from dotenv import load_dotenv + + +class MusicBot(commands.Cog): + voice_clients = {} + + def __init__(self, client): + self.client = client + + @commands.command() + async def join(self, ctx: commands.Context): + if not ctx.message.author.voice: + await ctx.send('You are not in a voice channel') + return + # Leave current channel + if ctx.voice_client is not None: + await ctx.voice_client.disconnect() + channel = ctx.message.author.voice.channel + self.voice_clients[ctx.guild.id] = await channel.connect(cls=wavelink.Player()) + await ctx.send(f'Joined {channel}') + + @commands.command() + async def leave(self, ctx: commands.Context): + if ctx.guild.id in self.voice_clients: + await self.voice_clients[ctx.guild.id].stop() + del self.voice_clients[ctx.guild.id] + await ctx.send('Left') + if ctx.voice_client is not None: + await ctx.voice_client.disconnect() + + @commands.command() + async def play(self, ctx: commands.Context, *, search: wavelink.YouTubeTrack): + if ctx.guild.id not in self.voice_clients: + # Join the user's voice channel + await self.join(ctx) + if ctx.guild.id not in self.voice_clients: + return + voice = self.voice_clients[ctx.guild.id] + await voice.play(search) + embed = discord.Embed( + title=voice.source.title, + url=voice.source.uri, + description=f"Playing {voice.source.title} in {voice.channel}" + ) + embed.set_image(url=voice.source.thumbnail) + if hasattr(ctx.author, 'avatar'): + embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar.url) + await ctx.send(embed=embed) + + @commands.command() + async def stop(self, ctx: commands.Context): + if ctx.guild.id in self.voice_clients: + await self.voice_clients[ctx.guild.id].stop() + await ctx.send('Stopped') + else: + await ctx.send('Not in a voice channel') + + @commands.command() + async def pause(self, ctx: commands.Context): + if ctx.guild.id in self.voice_clients: + await self.voice_clients[ctx.guild.id].pause() + await ctx.send('Paused') + else: + await ctx.send('Not in a voice channel') + + @commands.command() + async def resume(self, ctx: commands.Context): + if ctx.guild.id in self.voice_clients: + await self.voice_clients[ctx.guild.id].resume() + await ctx.send('Resumed') + else: + await ctx.send('Not in a voice channel') + + # Start the bot + @commands.Cog.listener() + async def on_ready(self): + print('Logged in as') + print(self.client.user.name) + print(self.client.user.id) + print('------') + # Try start Lavalink server + subprocess.Popen(["./jdk-13.0.2/bin/java", "-jar", "Lavalink.jar"]) + # wait for port to open + while True: + try: + r = requests.get('http://localhost:2333') + break + except requests.exceptions.ConnectionError: + print("Waiting for lavalink to go live...") + time.sleep(1) + continue + + async def connect_wavelink(): + await self.client.wait_until_ready() + await wavelink.NodePool.create_node( + bot=self.client, + host='localhost', + port=2333, + password='youshallnotpass' + ) + + self.client.loop.create_task(connect_wavelink()) + + @commands.Cog.listener() + async def on_wavelink_node_ready(self, node: wavelink.Node): + print(f'Connected to wavefront! ID: {node.identifier}') + + +async def setup(bot): + await bot.add_cog(MusicBot(bot)) + + +if __name__ == '__main__': + load_dotenv() + TOKEN = os.getenv('TOKEN') + + client = commands.Bot(command_prefix='!') + client.add_cog(MusicBot(client)) + client.run(TOKEN) diff --git a/w5/1.png b/w5/1.png new file mode 100644 index 0000000..6f0724b Binary files /dev/null and b/w5/1.png differ diff --git a/w5/README.md b/w5/README.md new file mode 100644 index 0000000..85676c7 --- /dev/null +++ b/w5/README.md @@ -0,0 +1,338 @@ +

[Participant's Workbook] Building a Music Bot

+ +> Related Pages: [DecodED 3](./README.md) + +--- + +

Table of Contents

+
+Table of Contents + +- [0. Cloning and setting up Lavalink](#0-Cloning-and-setting-up-Lavalink) +- [1. Installing Modules: `youtube_dl`](#1-create-an-application) +- [2. Setting up the join and leave commands](#2-installing-modules-discordpy-and-python-dotenv) +- [3. Streaming audio to discord](#3-creating-a-bot-and-adding-it-to-your-server) +- [4. Pausing and playing audio](#4-make-the-bot-say-hello-world) +- [5. Sending the Youtube Thumbnail](#5-adding-commands) + +
+ +--- + +## 0. Cloning and setting up Lavalink + +To get started with this lesson, you'll need the Hello foundations bot from the first workshop! +Clone it from here: https://github.com/HackMelbourne/Decoded-3 +You can also download the repo as a zip and extract it to a folder. +![image](1.png) + +
+❓ What is Wavelink and Lavalink? + +Wavelink is a library that allows you to play music with discord.py using +the [Lavalink](https://github.com/freyacodes/Lavalink) library. + +Lavalink is a server application which allows you to search and stream audio directly between a source and a destination +without too much overhead! + +
+ +### 0.1 Downloading Java 13 ☕ +To run Lavalink, you need to have Java 13. Nothing newer or older (although I've tested OpenJDK 11 and that worked too). +Download JDK 13 from this [link](https://jdk.java.net/archive/). + +Here are some links for your specific platform: + +* [Windows](https://download.java.net/java/GA/jdk13.0.2/d4173c853231432d94f001e99d882ca7/8/GPL/openjdk-13.0.2_windows-x64_bin.zip) +* [Mac](https://download.java.net/java/GA/jdk13.0.2/d4173c853231432d94f001e99d882ca7/8/GPL/openjdk-13.0.2_osx-x64_bin.tar.gz) +* [Linux](https://download.java.net/java/GA/jdk13.0.2/d4173c853231432d94f001e99d882ca7/8/GPL/openjdk-13.0.2_linux-x64_bin.tar.gz) + +Extract it into the same folder as the repo you just cloned. + +### 0.2 Downloading Lavalink ⏬ + +Then, download Lavalink from the following +URL: [Lavalink](https://github.com/freyacodes/Lavalink/releases/download/3.4/Lavalink.jar) +([Source](https://github.com/freyacodes/Lavalink/releases/tag/3.4)) + +You can also grab it from: [Here](https://github.com/HackMelbourne/Decoded-3/raw/music-bot/Lavalink.jar) + +### 0.3 Downloading application.yml ⚙ + +Finally, you'll need to grab an `application.yml` file, which will configure all the needed settings for Lavalink to +work. + +Grab a copy of ours from here: [Forms](https://github.com/HackMelbourne/Decoded-3/blob/music-bot/application.yml) +and place it in the same folder as your Lavalink jar file. + +### 0.4 Running Lavalink 🌋 + +Now, you can run Lavalink by running the following command in the same folder as your Lavalink jar file: + +```bash +./jdk-13.0.2/bin/java -jar Lavalink.jar +``` + +## 1. Installing Modules + +* Before we begin creating the bot, we have to install a few modules + +* Install wavelink 🌊 + * Just simply type this in your terminal + ``` + pip install wavelink + ``` + +We're hoping you already have a basic hello bot set up from the first workshop! + +* Now we need to start the bot by adding some import statement in our code. + +```python +import os +import subprocess +import time + +import discord +import requests +import wavelink +from discord.ext import commands +``` + +## 2. Setting up the join and leave commands + +* We first need to set up a COG to put all of our code in: + +```python +class MusicBot(commands.Cog): + voice_clients = {} + + def __init__(self, client): + self.client = client + +async def setup(bot): + await bot.add_cog(MusicBot(bot)) +``` + +Make a new file called `musicbot.py` (or whatever you want really) and put it into the `cogs/` folder with the above +code. + +You may notice an extra `voice_clients` dictionary. This is where we will store all the active song sessions we're +playing 🎵 + +* The first step for a music bot is being able to join the current voice channel. Let's make a command called `join` + that does just that: + +```python +@commands.command() +async def join(self, ctx: commands.Context): + # We need to: + # 1. Check if the user is in a voice channel + # 2. Leave the current channel + # 3. Join the channel and attach wavelink player + # 4. Say that we joined +``` + +Let's implement these steps one by one: + +1. We first check if the author of the message is in a voice channel. If they aren't, we send a reminder to join and + stop + there + ```python + if not ctx.message.author.voice: + await ctx.send('You are not in a voice channel') + return + ``` +2. Then we check if we're currently connected to a voice channel. If we are, we gotta leave + ```python + if ctx.voice_client is not None: + await ctx.voice_client.disconnect() + ``` +3. If all is good, we grab the channel they're in and join the party 🥳. It's important that we set wavelink as our audio + source here, as that's where our music is coming from. + ```python + channel = ctx.message.author.voice.channel + self.voice_clients[ctx.guild.id] = await channel.connect(cls=wavelink.Player()) + ``` + * Take note of how we store the audio client in the `voice_clients` dictionary. This will be important later. +4. Finally, we text the user that we joined. + ```python + await ctx.send(f'Joined {channel}') + ``` + +* We also need to make sure that users can ask our bot to leave. We can do this with the following: + +```python +@commands.command() +async def leave(self, ctx): + if ctx.guild.id in self.voice_clients: + await self.voice_clients[ctx.guild.id].stop() + del self.voice_clients[ctx.guild.id] + await ctx.send('Left') + if ctx.voice_client is not None: + await ctx.voice_client.disconnect() +``` + +Let's break this down: + +1. Let's check if we're in the voice channel to begin with in this guild. If we are, let's stop our player and delete + our voice client + * We also send a message to let the user know we left. +2. In case the bot was restarted, and the dictionary is empty, we do a simple disconnect as well. + +## 3. Connecting to Lavalink + +* There are quite a few steps to get our bot going. These include: + +1. A logline to confirm that we've started up +2. Start the Lavalink server +3. Wait for the Lavalink server to be ready +4. Connect to the server using wavelink + +Let's begin with the first step + +```python +# Start the bot +@commands.Cog.listener() +async def on_ready(self): + print('Logged in as') + print(self.client.user.name) + print(self.client.user.id) + print('------') +``` +For our bot to work, we need to start up the Lavalink server, so let's do that. +Notice how we also wait in a loop until the server is ready, as we can't do anything without it. +Make sure you use the correct Java command to start Lavalink (either using Java on your machine, or the one you downloaded). +```python + # Try start Lavafront server + subprocess.Popen(["java", "-jar", "Lavalink.jar"]) + # wait for port to open + while True: + try: + r = requests.get('http://localhost:2333') + break + except requests.exceptions.ConnectionError: + print("Waiting for lavalink to go live...") + time.sleep(1) + continue +``` +Once the server is live, we can make an attempt to connect to it +```python + async def connect_wavelink(): + await self.client.wait_until_ready() + await wavelink.NodePool.create_node( + bot=self.client, + host='localhost', + port=2333, + password='youshallnotpass' + ) + + self.client.loop.create_task(connect_wavelink()) +``` +A final touch is notifying the user once we've connected and all is ready! +```python +@commands.Cog.listener() +async def on_wavelink_node_ready(self, node: wavelink.Node): + print(f'Connected to wavelink! ID: {node.identifier}') + +``` + +## 4. Playing music from a search query + +Let's get to the fun part. Playing music from user input! + +```python +# To play the music +@commands.command() +async def play(self, ctx, *, search: wavelink.YouTubeTrack): + if ctx.guild.id not in self.voice_clients: + # Join the user's voice channel + await self.join(ctx) + if ctx.guild.id not in self.voice_clients: + return + voice = self.voice_clients[ctx.guild.id] + await voice.play(search) + embed = discord.Embed( + title=voice.source.title, + url=voice.source.uri, + description=f"Playing {voice.source.title} in {voice.channel}" + + ) + embed.set_image(url=voice.source.thumbnail) + if hasattr(ctx.author, 'avatar'): + embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar.url) + await ctx.send(embed=embed) +``` + +We do the following here: +1. If we're not in the same voice channel, we join the user's voice channel +2. If this fails, we return (relying on the join command to send the correct error message) +3. Otherwise, we grab the user's channel, and tell Wavelink to play the youtube track the user requested +4. We also prepare a nice embed to show the user what's playing. +5. We need to make sure the user has an avatar before setting the author (otherwise we get an error) + +## 5.1 Stopping the music +We can simply do in few steps. +```python +@commands.command() +async def stop(self, ctx): +# Check if someone is in the voice channel and if some one is than stop the music and print a 'Stopped' text. +# If no one is in the voice channel than print 'Not in a voice channel' +``` +Let's implement these steps one by one: + 1. We need to check if the author of the message is in the voice channel. And if they are in the voice channel than print 'Stopped'. + ```python + if ctx.guild.id in self.voice_clients: + await self.voice_clients[ctx.guild.id].stop() + await ctx.send('Stopped') + ``` + 2. If no one is in the voice channel than just simply print 'Not in a voice channel' + ```python + else: + await ctx.send('Not in a voice channel') + ``` + +## 5.2 Pausing the music +We can simply do in few steps. +```python +@commands.command() +async def pause(self, ctx): +# Check if someone is in the voice channel and if some one is than pause the music and print a 'Paused' text. +# If no one is in the voice channel than print 'Not in a voice channel' +``` +Let's implement these steps one by one: + 1. We need to check if the author of the message is in the voice channel. And if they are in the voice channel than print 'Paused'. + ```python + if ctx.guild.id in self.voice_clients: + await self.voice_clients[ctx.guild.id].pause() + await ctx.send('Paused') + ``` + 2. If no one is in the voice channel than just simply print 'Not in a voice channel' + ```python + else: + await ctx.send('Not in a voice channel') + ``` + +## 5.3 Resuming the music +We can simply do in few steps. +```python +@commands.command() +async def resume(self, ctx): +# Check if someone is in the voice channel and if some one is than resume the music and print a 'Resume' text. +# If no one is in the voice channel than print 'Not in a voice channel' +``` +Let's implement these steps one by one: + 1. We need to check if the author of the message is in the voice channel. And if they are in the voice channel than print 'Resumed'. + ```python + if ctx.guild.id in self.voice_clients: + await self.voice_clients[ctx.guild.id].resume() + await ctx.send('Resumed') + ``` + 2. If no one is in the voice channel than just simply print 'Not in a voice channel' + ```python + else: + await ctx.send('Not in a voice channel') + ``` + +Now when ever you will add '!paused' in discord it will pause the music and when you will add '!resumed' it will resume +the song again. +