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.
+
+
+
+❓ 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.
+