├── .env ├── kill.sh ├── autorestart.sh ├── Playlist └── placeholder.txt ├── SongLog └── placeholder.txt ├── startup.sh ├── Docker ├── musicbotstart.sh └── Dockerfile ├── Resources └── config.json ├── Old_Unused_Cogs ├── welcome.py ├── README.md └── Admin.py ├── LICENSE.md ├── application.yml ├── bot.py ├── Cogs ├── cpu.py └── music.py ├── fileProcessing.py ├── playlist.py └── README.md /.env: -------------------------------------------------------------------------------- 1 | # .env 2 | DISCORD_TOKEN = INSERT TOKEN HERE WITHOUT QUOTES -------------------------------------------------------------------------------- /kill.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | tmux kill-session -t Lavalink 3 | tmux kill-session -t Bot -------------------------------------------------------------------------------- /autorestart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | tmux new-session -d 'sh kill.sh && sleep 5 && sh startup.sh' -------------------------------------------------------------------------------- /Playlist/placeholder.txt: -------------------------------------------------------------------------------- 1 | This is only here as a place holder. This folder is manadatory. This text file can be deleted. -------------------------------------------------------------------------------- /SongLog/placeholder.txt: -------------------------------------------------------------------------------- 1 | This is only here as a place holder. This folder is manadatory. This text file can be deleted. -------------------------------------------------------------------------------- /startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | tmux new-session -s Lavalink -d 'java -jar Lavalink.jar' 3 | sleep 20 4 | tmux new-session -s Bot -d 'python3 ./bot.py' 5 | -------------------------------------------------------------------------------- /Docker/musicbotstart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sleep 30 3 | docker container start musicbot 4 | sleep 5 5 | docker exec -d --user=root musicbot tmux new-session -d 'cd /MusicBot/ && sh startup.sh' 6 | -------------------------------------------------------------------------------- /Resources/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "roles": [ 3 | "Dj", 4 | "Administrator", 5 | "DJ" 6 | ], 7 | "voice_permission_check_list": [ 8 | "play", 9 | "playfromlist", 10 | "skip", 11 | "pause", 12 | "unpause", 13 | "clear", 14 | "shuffle" 15 | ] 16 | } -------------------------------------------------------------------------------- /Docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | RUN apt-get update && \ 3 | apt-get full-upgrade -y && \ 4 | apt-get install python3 -y && \ 5 | apt-get install python3-pip -y && \ 6 | apt-get install tmux -y && \ 7 | apt-get install openjdk-13-jre-headless -y && \ 8 | apt-get install zip -y 9 | 10 | RUN pip3 install --upgrade pip && \ 11 | pip3 install discord.py lavalink python-dotenv psutil && \ 12 | apt-get remove python3-pip -y 13 | 14 | COPY ./Bot /MusicBot 15 | 16 | RUN groupadd -g 1000 basicuser && useradd -r -u 1000 -g basicuser basicuser 17 | 18 | USER basicuser -------------------------------------------------------------------------------- /Old_Unused_Cogs/welcome.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord.utils import get 4 | 5 | class welcome(commands.Cog): 6 | def __init__(self,bot): 7 | self.bot=bot 8 | 9 | @commands.Cog.listener() 10 | async def on_member_join(self, member): 11 | serverrolename = 'Example' #Name of the role you have made for the bot to give 12 | servername = str(member.guild) # Grabs the server name 13 | role = get(member.guild.roles, name = serverrolename) #Remove this line and below to not add a role to a new user 14 | await member.add_roles(role) #Remove me if you remove the line above. 15 | channel = get(member.guild.text_channels, name = 'general') # Change 'general' to the exact channel you want the message to be sent to. 16 | await channel.send("Hello! " + str(member.display_name) + ", welcome to " + servername + "!") #You can customize this message. 17 | 18 | 19 | async def setup(bot): 20 | await bot.add_cog(welcome(bot)) 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Robert Andion 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the 4 | Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 5 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 10 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 11 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 12 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | -------------------------------------------------------------------------------- /application.yml: -------------------------------------------------------------------------------- 1 | server: # REST and WS server 2 | port: 2333 3 | address: 0.0.0.0 4 | lavalink: 5 | server: 6 | password: "changeme123" 7 | sources: 8 | youtube: true 9 | bandcamp: true 10 | soundcloud: true 11 | twitch: true 12 | vimeo: true 13 | mixer: true 14 | http: true 15 | local: false 16 | bufferDurationMs: 400 17 | youtubePlaylistLoadLimit: 6 # Number of pages at 100 each 18 | youtubeSearchEnabled: true 19 | soundcloudSearchEnabled: true 20 | gc-warnings: true 21 | #ratelimit: 22 | #ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks 23 | #excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink 24 | #strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch 25 | #searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing 26 | #retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times 27 | 28 | metrics: 29 | prometheus: 30 | enabled: false 31 | endpoint: /metrics 32 | 33 | sentry: 34 | dsn: "" 35 | # tags: 36 | # some_key: some_value 37 | # another_key: another_value 38 | 39 | logging: 40 | file: 41 | max-history: 30 42 | max-size: 1GB 43 | path: ./logs/ 44 | 45 | level: 46 | root: INFO 47 | lavalink: INFO -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | import os 4 | import asyncio 5 | import subprocess 6 | import shlex 7 | from dotenv import load_dotenv 8 | load_dotenv() 9 | TOKEN = os.getenv('DISCORD_TOKEN') 10 | intents = discord.Intents.default() 11 | intents.message_content = True 12 | intents.members = True 13 | 14 | client = commands.Bot(command_prefix='.', intents=intents) 15 | 16 | 17 | @client.event 18 | async def on_ready(): 19 | print("Bot is live") 20 | await client.load_extension('playlist') 21 | for file in os.listdir("./Cogs"): 22 | if file.endswith(".py"): 23 | await client.load_extension(f'Cogs.{file[:-3]}') 24 | 25 | # TODO: Refactor so that shell files can go into a folder 26 | @client.command(name="reboot") 27 | @commands.is_owner() 28 | async def reboot(ctx): 29 | await ctx.send("Rebooting") 30 | subprocess.call(["sh", "./autorestart.sh"]) 31 | 32 | 33 | @client.command(name="backupPlaylists") 34 | @commands.is_owner() 35 | async def backup_playlists(ctx): 36 | await ctx.send("Backing up playlists and will send as a personal message.") 37 | if os.path.isfile('./backup.zip'): 38 | os.remove('./backup.zip') 39 | 40 | zipCommand = shlex.split("zip -r backup.zip ./Playlist") 41 | outcome = subprocess.Popen(zipCommand) 42 | waitCounter = 10 43 | while outcome.poll() is None and waitCounter > 0: 44 | await asyncio.sleep(1) 45 | waitCounter = waitCounter - 1 46 | 47 | if os.path.isfile('./backup.zip'): 48 | await ctx.author.send(file=discord.File(r'./backup.zip')) 49 | os.remove('./backup.zip') 50 | 51 | client.run(TOKEN) 52 | -------------------------------------------------------------------------------- /Cogs/cpu.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | from discord.utils import get 3 | import discord 4 | import psutil 5 | 6 | 7 | class cpu(commands.Cog): 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | @commands.command(name='serverinfo', description="Permanant server hardware information") 12 | async def server_info(self, ctx): 13 | embed = discord.Embed(color=discord.Color.blurple()) 14 | embed.title = 'Server Information' 15 | data = "" 16 | data += str(psutil.cpu_count()) + " total threads \n" 17 | data += f"{psutil.virtual_memory().total / 2**30:.2f}" + \ 18 | " GB Total Memory \n" 19 | data += f"{psutil.virtual_memory().available / 2**30:.2f}" + \ 20 | " GB Available Currently \n" 21 | embed.description = data 22 | await ctx.channel.send(embed=embed) 23 | 24 | @commands.command(name='cpu', description="Cpu Information") 25 | async def cpu_info(self, ctx): 26 | embed = discord.Embed(color=discord.Color.blurple()) 27 | embed.title = 'CPU Information' 28 | embed.description = str(psutil.cpu_percent( 29 | interval=1)) + "% CPU Usage \n" 30 | # embed.description += str(psutil.sensors_temperatures(fahrenheit=False)["k10temp"][0][1]) + " C \n" Manjaro version 31 | # This works on Ubuntu. You will have to change this for your hardware, remove ["coretemp"][0][1] to print the full package and then pick. 32 | embed.description += str(psutil.sensors_temperatures( 33 | fahrenheit=False)["coretemp"][0][1]) + " C \n" 34 | embed.description += str(psutil.getloadavg() 35 | [1]) + " average load over the last 5 minutes" 36 | await ctx.channel.send(embed=embed) 37 | 38 | 39 | async def setup(bot): 40 | await bot.add_cog(cpu(bot)) 41 | -------------------------------------------------------------------------------- /Old_Unused_Cogs/README.md: -------------------------------------------------------------------------------- 1 | ## ADMIN COMMANDS: 2 | ### NOTE: 3 | These require either "Administrator" or "Admin" commands for more advanced commands while intuitive 4 | ones like ban and kick only require those permissions. 5 | 6 | ``` 7 | .kick <@membername> (boot) 8 | ``` 9 | This will kick the named user out of the server. 10 | It requires the ban and kick permissions. 11 | 12 | ``` 13 | .ban <@membername> (block) 14 | ``` 15 | This will ban the named user from the server. 16 | It requires the ban and kick permissions. 17 | 18 | ``` 19 | .assign <@membername, Role> 20 | ``` 21 | Used to assign a role to a user on text command. Once this is done that role will be locked to bot control. 22 | The first argument is the @user and he second is the name of the roll. Requires "Admin" or "Administrator" role. 23 | 24 | ``` 25 | .log <@membername, amount> 26 | ``` 27 | Will list the actions performed by the person for x amount of entries. ex: .log @Rob 12 28 | will show the last 12 actions (If there are that many) for rob. Requires "Administrator" or "Admin" role. 29 | 30 | ``` 31 | .move <@member, channel name> 32 | ``` 33 | This will move the given user into the channel name listed. Requires the "Administrator" or "Admin" role. 34 | 35 | ``` 36 | .disconnectuser <@membername> (dcuser) 37 | ``` 38 | This will disconnect the named user from voice channels. Requires the "Administrator" or "Admin" role. 39 | 40 | ##### 'client.load_extension('Admin')' This is the command that loads in the Cog. 41 | 42 | ## WELCOME FUNCTIONS: 43 | 44 | In order to use the welcome functions you must enable "Privileged Gateway Intents" on the discord developer page under the Bot section. Enable the 45 | "SERVER MEMBERS INTENT" this will allow the function to welcome new members. 46 | 47 | These are automated functions that will activate on a new member joining. They will be greeted in your "general" chat 48 | and given the role "Example" automatically. These should be changed to your unique needs, and the role should be created and customized in your server first. 49 | If you do not want automatic roles the two lines to remove are marked in the welcome.py file. There are also comments there that 50 | direct you how to changed the channel the announcement will be placed in. There is also the option to change the printed message. 51 | 52 | NOTE: The role will follow any server you add it to and fail. If you plan to have the bot in more than one server add the following instead 53 | of the current two lines for the role. 54 | ``` 55 | if servername == "YourServerHere": 56 | role = get(member.guild.roles, name = serverrolename) #Remove this line and below to not add a role to a new user 57 | await member.add_roles(role) #Remove me if you remove the line above. 58 | ``` 59 | 60 | This will make it so the role function only applies to your server. You can put the greeting under this protection as well, 61 | however most servers have a general chat. The name of the user and of the server are dynamicaly coded. 62 | 63 | (**FUTURE:** In the future the if statement above will not be needed. Server specific files will be created and managed through the bot to control this.) 64 | 65 | The welcome functions can be removed by deleting line 30 in bot.py and deleting welcome.py. 66 | 'client.load_extension('welcome')' This is the command that loads in the Cog. 67 | You must also delete line 8 and 9 from bot.py to remove discord intents if you do not want to use welcome functions. -------------------------------------------------------------------------------- /Old_Unused_Cogs/Admin.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import discord 3 | from discord.utils import get 4 | 5 | class Admin(commands.Cog): 6 | def __init__(self,bot): 7 | self.bot = bot 8 | 9 | 10 | @commands.command(name = 'kick', aliases = ['boot'],description="Used by admins to kick members") #working kick command 11 | @commands.has_permissions(ban_members=True, kick_members=True) 12 | async def kick_user(self,ctx, member : discord.Member, *, reason =None): 13 | await member.kick(reason=reason) 14 | await ctx.channel.send(member.display_name + " was kicked.") 15 | 16 | 17 | @commands.command(name = 'ban', aliases = ['block'],description="Used by admins to ban members") #Working ban command 18 | @commands.has_permissions(ban_members=True, kick_members=True) 19 | async def ban_user(self,ctx,member : discord.Member, *, reason=None): 20 | await member.ban(reason=reason) 21 | await ctx.channel.send(member.display_name + " has been banned from the server.") 22 | 23 | 24 | @commands.command(name='assign', description="Usable by Admins to assign roles to server members.") #command to assign roles to users 25 | @commands.has_any_role('Administrator','Admin') 26 | async def assign_role(self,ctx,member : discord.Member, *, role_name): 27 | role = get(ctx.guild.roles,name = role_name) 28 | # member : Member = get(ctx.guild.members,name = user_name) 29 | await member.add_roles(role) 30 | await ctx.channel.send(str(role) + " was successfully given to " + str(member.display_name)) 31 | 32 | 33 | @commands.command(name="log",description="Shows log for specific user. Provide name and number of entries to display") 34 | @commands.has_any_role("Administrator","Admin") 35 | async def get_log(self,ctx,member : discord.Member, limiter : int): 36 | logs = await ctx.guild.audit_logs(limit = limiter, user = member).flatten() 37 | for log in logs: 38 | await ctx.channel.send('Peformed {0.action} on {0.target}'.format(log)) 39 | 40 | await ctx.channel.send("All logs requested have been shown.") 41 | # async for entry in ctx.guild.audit_logs(limit=15): 42 | # two lines for all users await ctx.channel.send('{0.user} performed {0.action} on {0.target}'.format(entry)) 43 | 44 | 45 | @commands.command(name="move") 46 | @commands.has_any_role("Administrator","Admin") 47 | async def move_voice(self,ctx,member : discord.Member, new_channel_raw): 48 | if member.voice is not None: 49 | new_channel = get(member.guild.channels, name = new_channel_raw) 50 | await member.move_to(new_channel) 51 | await ctx.author.send("Moved user") 52 | else: 53 | await ctx.author.send("Couldnt move user, not connected to voice chat.") 54 | 55 | 56 | @commands.command(name="disconnectuser",aliases=['dcuser']) 57 | @commands.has_any_role("Administrator","Admin") 58 | async def disconnect_user(self,ctx, member: discord.Member): 59 | if member.voice is not None: 60 | await member.move_to(None) 61 | await ctx.author.send("User disconnected.") 62 | else: 63 | await ctx.author.send("Cannot disconnect user, they are not connected to voice chat.") 64 | 65 | # This catches errors but causes one when shutting down a bot. 66 | @commands.Cog.listener() #event listener for error handling 67 | async def on_command_error(self, ctx, error): 68 | if ctx.command is not None: 69 | print(ctx.command.name + " was used incorrectly") 70 | print(error) 71 | 72 | else: 73 | print("Command does not exist.") 74 | 75 | 76 | async def setup(bot): 77 | await bot.add_cog(Admin(bot)) -------------------------------------------------------------------------------- /fileProcessing.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path 3 | from os import path 4 | 5 | 6 | def logUpdate(ctx, songName): 7 | user_file = os.path.join("SongLog", str(ctx.author.id)) 8 | user_file = str(user_file) + ".txt" 9 | user_write = open(user_file, "a") 10 | user_write.write(str(songName) + "\n") 11 | user_write.close() 12 | 13 | 14 | def page_format(raw_input) -> list: 15 | list_collection = [] 16 | i = 0 17 | temp = '' 18 | for song in raw_input.splitlines(): 19 | temp = temp + '\n' + song 20 | i = i + 1 21 | if i % 10 == 0: 22 | list_collection.append(temp) 23 | temp = '' 24 | 25 | if i % 10 != 0: 26 | list_collection.append(temp) 27 | return list_collection 28 | 29 | 30 | def playlist_read(listname, ctx): 31 | userpath = os.path.join("Playlist", str(ctx.author.id)) 32 | userpath = str(userpath) + ".json" 33 | i = 1 34 | try: 35 | with open(userpath, "r") as fileRead: 36 | data = json.load(fileRead) 37 | specific = data[listname] 38 | final = "" 39 | for item in specific: 40 | final += str(i) + ": " + item + "\n" 41 | i = i + 1 42 | return page_format(final) 43 | except: 44 | return [] 45 | 46 | 47 | def list_playlists(ctx): 48 | userpath = os.path.join("Playlist", str(ctx.author.id)) 49 | userpath = str(userpath) + ".json" 50 | i = 1 51 | final = "" 52 | try: 53 | with open(userpath, "r") as file_read: 54 | data = json.load(file_read) 55 | for key in data: 56 | final += str(i) + ": " + key + "\n" 57 | i = i + 1 58 | 59 | return page_format(final) 60 | except: 61 | return [] 62 | 63 | # function to create a new playlist in the JSON file or make a JSON file if none exists for the user 64 | 65 | 66 | def new_playlist(ctx, playlist_name, now_playing): 67 | userpath = os.path.join("Playlist", str(ctx.author.id)) 68 | userpath = str(userpath) + ".json" 69 | if path.exists(userpath): 70 | with open(userpath, "r") as read_file: 71 | data = json.load(read_file) 72 | temp = [now_playing] 73 | data[playlist_name] = temp 74 | dataFinal = json.dumps(data, indent=1) 75 | write_out(ctx, dataFinal) 76 | else: 77 | dataStart = {playlist_name: [now_playing]} 78 | with open(userpath, "w") as write_file: 79 | json.dump(dataStart, write_file) 80 | 81 | 82 | def write_out(ctx, data): 83 | userpath = os.path.join("Playlist", str(ctx.author.id)) 84 | userpath = str(userpath) + ".json" 85 | file = open(userpath, "w") 86 | file.write(data) 87 | file.close() 88 | 89 | 90 | def delete_playlist(ctx, playlist_name): 91 | userpath = os.path.join("Playlist", str(ctx.author.id)) 92 | userpath = str(userpath) + ".json" 93 | if path.exists(userpath): 94 | with open(userpath, "r") as read_file: 95 | data = json.load(read_file) 96 | try: 97 | data.pop(playlist_name) 98 | dataFinal = json.dumps(data, indent=1) 99 | write_out(ctx, dataFinal) 100 | return "Done" 101 | except: 102 | return "Not-Found" 103 | else: 104 | return "No-Playlists" 105 | 106 | 107 | def delete_from_playlist(ctx, playlist_name, selection): 108 | userpath = os.path.join("Playlist", str(ctx.author.id)) 109 | userpath = str(userpath) + ".json" 110 | if path.exists(userpath): 111 | with open(userpath, "r") as read_file: 112 | try: 113 | data = json.load(read_file) 114 | data[playlist_name].pop(selection - 1) 115 | dataFinal = json.dumps(data, indent=1) 116 | write_out(ctx, dataFinal) 117 | return "Done" 118 | except: 119 | return "Not-Found" 120 | 121 | else: 122 | return "No-Playlists" 123 | 124 | # Reads json, finds playlists and add song then uses help_newplaylist to write back. 125 | 126 | 127 | def add_to_playlist(ctx, playlist_name, now_playing) -> bool: 128 | userpath = os.path.join("Playlist", str(ctx.author.id)) 129 | userpath = str(userpath) + ".json" 130 | if path.exists(userpath): 131 | try: 132 | with open(userpath, "r") as read_file: 133 | data = json.load(read_file) 134 | temp = [now_playing] 135 | data[playlist_name] += temp 136 | dataFinal = json.dumps(data, indent=1) 137 | write_out(ctx, dataFinal) 138 | return True 139 | 140 | except: 141 | return False 142 | 143 | # loads songs from a playlist to be parsed by the calling function 144 | 145 | 146 | def play_playlist(ctx, playlist_name): 147 | userpath = os.path.join("Playlist", str(ctx.author.id)) 148 | userpath = str(userpath) + ".json" 149 | if path.exists(userpath): 150 | # using with auto closes the file after. 151 | with open(userpath, "r") as read_file: 152 | data = json.load(read_file) 153 | if playlist_name in data: 154 | songlist = data[playlist_name] 155 | return songlist 156 | else: 157 | # return false if playlist doesnt exist which will be caught by music.py and output playlist doesnt exist. 158 | return False 159 | else: 160 | return False # same as above comment 161 | 162 | 163 | def rename_playlist(ctx, raw_input) -> bool: 164 | userpath = os.path.join("Playlist", str(ctx.author.id)) 165 | userpath = str(userpath) + ".json" 166 | splitNames = raw_input.split(',') 167 | try: 168 | if splitNames[0] is not None and splitNames[1] is not None: 169 | data = "" 170 | specific = "" 171 | try: 172 | with open(userpath, "r") as fileRead: 173 | data = json.load(fileRead) 174 | specific = data[splitNames[0].strip()] 175 | with open(userpath, "w") as fileRead: 176 | data.pop(splitNames[0].strip()) # pop off old playlist 177 | # store the same data as a new list. 178 | data[splitNames[1].strip()] = specific 179 | dataFinal = json.dumps(data, indent=1) 180 | write_out(ctx, dataFinal) 181 | return "Success" 182 | except: 183 | return "No-List" 184 | except: 185 | return "Invalid-Input" 186 | 187 | 188 | def read_config(): 189 | configPath = os.path.join("Resources", "config.json") 190 | try: 191 | with open(configPath, "r") as fileRead: 192 | data = json.load(fileRead) 193 | return data 194 | except: 195 | raise Exception("Config file not found!") 196 | -------------------------------------------------------------------------------- /playlist.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import discord 3 | import lavalink 4 | import asyncio 5 | from discord import utils 6 | from discord import Embed 7 | import re 8 | import fileProcessing 9 | 10 | url_rx = re.compile(r'https?://(?:www\.)?.+') 11 | 12 | config = fileProcessing.read_config() 13 | roles = config["roles"] 14 | 15 | 16 | class playlist(commands.Cog): 17 | def __init__(self, bot): 18 | self.bot = bot 19 | 20 | async def cog_command_error(self, ctx, error): 21 | if isinstance(error, commands.CommandInvokeError): 22 | await ctx.send(error.original) 23 | 24 | @commands.command(name="viewplaylist", aliases=["vpl"], description="Views all songs inside of a given playlist.") 25 | @commands.has_any_role(*roles) 26 | async def view_playlist(self, ctx, *, list_name): 27 | list_collection = fileProcessing.playlist_read(list_name, ctx) 28 | if list_collection: 29 | try: 30 | embed = Embed() 31 | double = '' 32 | x = 1 33 | for section in list_collection: 34 | double += section 35 | 36 | if x % 2 == 0: 37 | embed.description = double 38 | await ctx.send(embed=embed) 39 | await asyncio.sleep(1) 40 | double = '' 41 | x = x + 1 42 | 43 | if len(list_collection) % 2 != 0: 44 | embed.description = double 45 | await ctx.send(embed=embed) 46 | except: 47 | await ctx.send("Playlist not found.") 48 | else: 49 | raise commands.CommandInvokeError( 50 | "Playlist is empty or does not exist.") 51 | 52 | @commands.command(name="listplaylists", aliases=["lpl"], description="Lists all of a users playlists") 53 | @commands.has_any_role(*roles) 54 | async def list_playlists(self, ctx, page=1): 55 | # Stop here if the page is not a valid number (save processing time). 56 | if not isinstance(page, int): 57 | raise commands.CommandInvokeError("Please enter a valid number.") 58 | 59 | list_collection = fileProcessing.list_playlists(ctx) 60 | if list_collection: 61 | try: 62 | selection = page - 1 63 | embed = Embed() 64 | if int(selection) < 0: 65 | list_collection[0] += "'\n' + Page: 1/" + \ 66 | str(len(list_collection)) 67 | embed.description = list_collection[0] 68 | 69 | elif int(selection) > len(list_collection) - 1: 70 | list_collection[len(list_collection) - 1] += "'\n' + Page: " + str( 71 | len(list_collection)) + "/" + str(len(list_collection)) 72 | embed.description = list_collection[len( 73 | list_collection) - 1] 74 | else: # Valid input 75 | list_collection[selection] += '\n' + "Page: " + \ 76 | str(page) + "/" + str(len(list_collection)) 77 | embed.description = list_collection[selection] 78 | 79 | await ctx.send(embed=embed) 80 | except: 81 | raise commands.CommandInvokeError( 82 | "Failed to list playlists...") 83 | else: 84 | await ctx.send("No playlists found, do you have any?") 85 | 86 | @commands.command(name="deleteplaylist", aliases=["dpl"], description="Used to delete an entire playlist.") 87 | @commands.has_any_role(*roles) 88 | async def delete_playlist(self, ctx, *, playlist): 89 | result = fileProcessing.delete_playlist(ctx, playlist) 90 | if result == "Done": 91 | await ctx.send("Playlist deleted.") 92 | elif result == "Not-Found": 93 | await ctx.send("Playlist not found. Check that it is spelled correctly or if it has already been deleted.") 94 | elif result == "No-Playlists": 95 | await ctx.send("You have no playlists.") 96 | 97 | @commands.command(name="deletefromplaylist", aliases=["dfp"], description="Delete song from playlist based on its number in the playlist.") 98 | @commands.has_any_role(*roles) 99 | async def delete_from_playlist(self, ctx, value, *, playlist): 100 | try: 101 | result = fileProcessing.delete_from_playlist( 102 | ctx, playlist, int(value)) 103 | if result == "Done": 104 | await ctx.send("Song deleted from playlist.") 105 | elif result == "Not-Found": 106 | await ctx.send("Song not found.") 107 | elif result == "No-Playlists": 108 | await ctx.send("You have no playlists.") 109 | except: 110 | await ctx.send("Playlist not found.") 111 | 112 | @commands.command(name="createplaylist", aliases=['cpl'], description="Uses the currently playing song to start a new playlist with the inputted name") 113 | @commands.has_any_role(*roles) 114 | async def create_playlist(self, ctx, *, playlist_name): 115 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 116 | if player.is_playing: 117 | songname = player.current['title'] 118 | fileProcessing.new_playlist(ctx, playlist_name, songname) 119 | await ctx.send(playlist_name + ", created.") 120 | else: 121 | await ctx.send("Please have the first song you want to add playing to make a new playlist.") 122 | 123 | @commands.command(name="addtoplaylist", aliases=["atp"], description="Adds currently playing song to the given playlist name as long as it exists.") 124 | @commands.has_any_role(*roles) 125 | async def add_to_playlist(self, ctx, *, playlist_name): 126 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 127 | if player.is_playing: 128 | songname = player.current['title'] 129 | passfail = fileProcessing.add_to_playlist( 130 | ctx, playlist_name, songname) 131 | if passfail: 132 | await ctx.send("Song added") 133 | else: 134 | await ctx.send("Playlist needs to be created before you can add to it.") 135 | else: 136 | await ctx.send("Please have the first song you want to add playing to add it to the playlist.") 137 | 138 | @commands.command(name="renameplaylist", aliases=["rpl"], description="Renames a current list. Input as: current name,new name") 139 | @commands.has_any_role(*roles) 140 | async def rename_playlist(self, ctx, *, raw_name): 141 | status = fileProcessing.rename_playlist(ctx, raw_name) 142 | if status == "Success": 143 | await ctx.send("Playlist name updated.") 144 | elif status == "No-List": 145 | await ctx.send("Operation failed. You either have no playlists or no playlist by the given name.") 146 | elif status == "Invalid-Input": 147 | await ctx.send("Please format the command properly. .rpl current name,new name (MANDATORY COMMA)") 148 | 149 | @commands.command(name="addqueuetolist", aliases=["aqtp"], description="Adds the entire queue to a playlist.") 150 | @commands.has_any_role(*roles) 151 | async def add_queue_to_list(self, ctx, *, listname): 152 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 153 | if player.is_playing: 154 | songlist = player.queue 155 | for song in songlist: 156 | check = fileProcessing.add_to_playlist( 157 | ctx, listname, f"{song['title']}") 158 | if not check: 159 | return await ctx.send("Operation failed. Make sure the playlist name is valid.") 160 | await ctx.send("Queue added to " + str(listname) + ".") 161 | else: 162 | raise commands.CommandInvokeError("There is nothing playing.") 163 | 164 | 165 | async def setup(bot): 166 | await bot.add_cog(playlist(bot)) 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Music Bot 2 | 3 | ## General Setup 4 | 5 | ### Dependancies: 6 | 7 | You **must** have java 11+ for lavalink. 8 | The bot is written in and requires Python3. 9 | You must install python-dotenv, discord.py, lavalink.py and psutil, links are listed below. 10 | * [python-dotenv](https://pypi.org/project/python-dotenv/) 11 | * [discord.py](https://pypi.org/project/discord.py/) 12 | * [lavalink.py](https://github.com/Devoxin/Lavalink.py) 13 | * [psutil](https://pypi.org/project/psutil/) 14 | 15 | (psutil does not need to be installed if cpu functions are not needed, see more below.) 16 | ### Lavalink 17 | Lava link is not my creation and can be found at: 18 | https://github.com/Frederikam/Lavalink/releases 19 | (It can also be found in other Git branches. Any should be fine) 20 | The lavalink.jar, **MUST BE DOWNLOADED AND PLACED IN DIRECTORY** 21 | 22 | Keep the same file structure as the github repo for this to work, place Lavalink.jar at the "root" of the project. 23 | 24 | If you want to change the lavalink password you must change it in the application.yml 25 | and in both music.py and playlist.py on every line where 'changeme123' is located. 26 | 27 | ### Env File 28 | **Important:** You must also place your discord bot token in the .env file where prompted. 29 | 30 | ### Docker 31 | If you wish to run the bot in a docker container the Docker folder provides a 32 | dockerfile to do so. In order to use the file place the github files in a folder named Bot, 33 | then place the dockerfile on the same level as the Bot folder (not inside) then run a normal build 34 | command. First set the correct .env and Playlist folder, if you have existing playlists, and they 35 | will automatically be brought into the container. 36 | 37 | Docker start command: 38 | ``` 39 | docker run -it -m 2G --cpuset-cpus 0-1 --security-opt=no-new-privileges 40 | ``` 41 | In this command the -m and --cpuset-cpus are optional but means that the container can use at most 42 | two gigabytes of RAM and cpuset 0-1 means that the container can use threads 0 and 1. (Limiting resources) 43 | All of this can be adjusted to suit or removed entirely. Keep --security-opt=no-new-privileges for security. 44 | 45 | After this you can exit the container and rename it using 46 | ``` 47 | docker container ls -a 48 | ``` 49 | and then use the container id in the following command: 50 | ``` 51 | docker rename musicbot 52 | ``` 53 | Now the automatic start file will boot up the container and automatically run the bot inside, 54 | if the instructions below are followed. 55 | 56 | #### Note: 57 | You will be unable to update these containers from the inside so the command .backupPlaylists is here 58 | in order to send you the playlists (only new info in the container) so you can remake the container images 59 | often to get updates and changes to the bots code, simply place the .json lists back in the playlist folder 60 | before building the new image and they will be added to the new image. 61 | 62 | #### Automatic Docker Startup 63 | In order to auto start, create the docker container and name it "musicbot", 64 | then place the musicbotstart.sh on the containers host machine. In the host machine run the command: 65 | (use sudo -i first if you need sudo to run docker, you should.) 66 | ``` 67 | crontab -e 68 | ``` 69 | Then choose nano if you are unfamiliar with linux editors, or pick your favorite editor. Add the following line to the bottom of 70 | the file it opens 71 | ``` 72 | @reboot sh /pathtoyourfile/musicbotstart.sh 73 | ``` 74 | Now upon the main server restarting it will start up the docker container and run the bot inside. 75 | 76 | ## Full installation instructions: 77 | ### How to install packages 78 | 79 | First ensure that you have python3 installed on your system, to do so enter python3 into a terminal 80 | and you should be greeted by the python command line interface, to exit type quit() 81 | Now that you have made sure you have python3 install pip. 82 | 83 | ##### Ubuntu: 84 | ``` 85 | sudo apt-get install python3-pip 86 | ``` 87 | 88 | ##### Arch: 89 | ``` 90 | sudo pacman -S python-pip 91 | ``` 92 | 93 | Now that you have pip installed you can reference the links above on how to install each package with pip commands. 94 | For example to install lavalink it is as simple as: 95 | ``` 96 | pip3 install lavalink 97 | ``` 98 | (Some will only need pip while other OS will need pip3 to be specified.) 99 | 100 | The sections below will cover starting the bot with the Reboot command enabled or running it without. 101 | I personally use Reboot often as many of the issues you may run across only need a quick reboot to get working again. 102 | 103 | ### Reboot command configuration 104 | In order to use the new Reboot command you need to run the bot using tmux. 105 | Installing tmux is simple 106 | 107 | ##### Ubuntu: 108 | ``` 109 | sudo apt-get install tmux 110 | ``` 111 | 112 | ##### Arch: 113 | ``` 114 | sudo pacman -S tmux 115 | ``` 116 | 117 | ### Running the bot with reboot capabilities: 118 | 119 | In order to start the bot run 120 | ``` 121 | sh startup.sh 122 | ``` 123 | This will set up tmux with the proper session names in order to use the scripts provided. 124 | This is a helpful new feature thats saves the time of having to log into the bot and reboot it manually. 125 | Many issues are quickly solved with a reboot. 126 | 127 | 128 | ### Running the bot without reboot command capabilities: 129 | run the lavalink server using the command: 130 | ``` 131 | java -jar lavalink.jar 132 | ``` 133 | Then run the bot in a separate terminal using: 134 | ``` 135 | python3 bot.py 136 | ``` 137 | Both terminals must remain running for the bot to be live, consider using tmux. 138 | 139 | ## Preparing to use the bot: 140 | In order for music commands you must make a "Dj", "Administrator" or "DJ" role in discord 141 | and assign it to those you want to be able to play songs. 142 | 143 | ### Role configuration: 144 | The config.json file under resources can be altered to change the roles needed for music to your specific needs. 145 | ``` 146 | "roles": [ 147 | "Dj", 148 | "Administrator", 149 | "DJ" 150 | ] 151 | 152 | ``` 153 | You can replace any of the strings in the list with your custom role, remove extra roles, or add more by altering the json. 154 | 155 | [For more information on JSON formatting](https://www.w3schools.com/js/js_json_intro.asp) 156 | 157 | Admin functions will either need kick ban permissions for some commands or an "Admin" or "Administrator" 158 | role. Everything is essentially role based to keep unwanted users from flooding the bot. 159 | 160 | ## COMMAND DOCUMENTATION: 161 | ### NOTE: 162 | Anything in <> is an argument required by the function. Anything in () are alternate command shortcuts/names 163 | 164 | ``` 165 | .reboot 166 | ``` 167 | This command will reboot both Lavalink and the bot directly from discord. 168 | This command is tied to the owners discord ID so only the server owner may reboot. 169 | Please read more about how to set it up above. 170 | Note: This function should be used before trying clearcache to fix bot errors. 171 | *To not use this function remove line 4, and 40-44 in bot.py* 172 | 173 | ``` 174 | .backupPlaylists 175 | ``` 176 | This command will only work for the bot owner and will private message them a zip file of all playlists. 177 | This is especially useful for Docker containers where the only thing you would need out before making a new 178 | version are the playlists. 179 | 180 | ``` 181 | .play 182 | ``` 183 | If the person using the command is in a voice channel and the bot has access to that channel it will connect and play the song listed. 184 | This is also the command to continue adding songs to the queue, it covers both functions. The bot will auto disconnect 185 | when the end of the queue is reached. 186 | 187 | ``` 188 | .skip 189 | ``` 190 | If the bot is playing a song it will skip to the next song as long as the person is in the same 191 | voice channel as the bot. If there are no songs after the bot will automatically disconnect. 192 | The argument can be used to say how many songs to skip. 193 | 194 | ``` 195 | .clear 196 | ``` 197 | This will clear all songs including now playing and the queue. This is the best way to disconnect the bot, 198 | because it flushes everything first. 199 | 200 | ``` 201 | .pause (ps) 202 | ``` 203 | This command is a useful one to pause the bot. The command has now been update. 204 | It will now unpause automatically after 7 minutes of being paused. This can be changed 205 | manually under the pause command. change the sleep(number of seconds here) to any amount of time. 206 | Other commands can still be used including unpause during this "wait" period. 207 | 208 | ``` 209 | .unpause (resume,start,up) 210 | ``` 211 | This will unpause a currently paused bot. (Should come after a pause command) 212 | This will automatically happen after 7 minutes afte a pause command by default. 213 | 214 | ``` 215 | .queue (playlist,songlist,upnext) 216 | ``` 217 | This will list all songs to be played next in pages of 10 with the currently playing 218 | song at the top of page 1 labeled as NP. The current page and total pages are displayed 219 | at the bottom, like so, Page: 5/6 means you are on page 5 out of a total of 6 pages. 220 | No argument assumes page one, negative or 0 goes to page 1 and a page larger than the total 221 | goes to the last page. 222 | 223 | ``` 224 | .shuffle 225 | ``` 226 | Shuffles all currently queued songs. 227 | This no longer uses the given shuffle function from lavalink. 228 | It is a custom function that shuffles in a finalized form, 229 | viewable in the queue command. 230 | 231 | ## PLAYLIST COMMANDS: 232 | ### NOTE: 233 | All playlists are stored by the discord ID with file extension .json, also all servers will be stored in the same folder, 234 | in the future server ID specific folders will likely be added. Also all 235 | commands are "currently playing" based. Keep this in mind when working with playlists. 236 | 237 | ``` 238 | .viewplaylist (vpl) 239 | ``` 240 | This will list the specific songs contained inside the specified playlist. 241 | Nothing needs to be playing to use this command. If you have more than 20 songs in a playlist, 242 | they will now send in pages of 20 to avoid "invisible" playlists if they are too big. 243 | (No need to be in a voice channel to use this command) 244 | 245 | ``` 246 | .listplaylists (lpl) 247 | ``` 248 | This will list all of the users playlists whether or not the user is in a voice channel. 249 | The playlists will now show up in pages of 10, if you have more than 10 playlists 250 | provide the page number you wish to go to after lpl (page total listed on the bottom 251 | similar to the queue command.) 252 | 253 | ``` 254 | .deleteplaylist (dpl) 255 | ``` 256 | This will delete the entire playlist given with no confirmation and no reclamation. 257 | Be certain before you delete as it is permanant. 258 | 259 | ``` 260 | .deletefromplaylist 261 | ``` 262 | This function takes two paramaters, seperated by spaces. The first is the song number and 263 | the second is the name of the playlist to delete the song from. 264 | 265 | ``` 266 | .createplaylist (cpl) 267 | ``` 268 | This function will create a playlist of the given name. The first song will be the song currently playing 269 | from the bot. If nothing is playing the command will fail not work. 270 | 271 | ``` 272 | .addtoplaylist (atp) 273 | ``` 274 | Adds the currently playing song to the given playlist. Case sensitive. 275 | If the playlist does not exist or no song is playing this will fail not work. 276 | 277 | ``` 278 | .playfromlist (playl) 279 | ``` 280 | This will play the entire playlist name given. Case sensitive. It will take some time to load all songs, 281 | it will print a message when it is completely done. 282 | 283 | ``` 284 | .renameplaylist (rpl) 285 | ``` 286 | This function will rename an existing playlist. The names must be seperated by a comma 287 | and no spaces before or after the comma. 288 | 289 | ``` 290 | .addqueuetolist (aqtp) 291 | ``` 292 | This function will add the entire queue to a given playlist. 293 | It does not add the currently playing song, this way if you make a playlist just for the queue, 294 | it will not add the currently playing song twice. 295 | 296 | #### Note: All the functions in fileProcessing are used by commands and require no command to use (Helper functions). 297 | 298 | ## CPU Commands: 299 | ### NOTE: cpu.py is a new option and will require extra work to get working. 300 | ##### If this functionality is undesired you can delete cpu.py from the Cogs folder. You will also not need to install the psutil package. 301 | 302 | ``` 303 | .cpu_info <> (cpu) 304 | ``` 305 | This will show current cpu information such as % usage, load and temperature. 306 | The temperature will take some adjustment to get working. First change line 29 in cpu.py to: 307 | ``` 308 | embed.description += str(psutil.sensors_temperatures(fahrenheit=False)) + " C \n" 309 | ``` 310 | This will print a full JSON package of the available sensors then select the proper one as I did for one server in the actual code. 311 | 312 | ``` 313 | .server_info <> (serverinfo) 314 | ``` 315 | This provides more permanant information such as thread count, RAM, and currently available RAM. 316 | (Should be cross system compatible.) 317 | 318 | ## Misc. 319 | 320 | Check out our other project written in Node.js: https://github.com/RobertAndion/DiscordMusicBotNode 321 | 322 | Todo/Possible adds in the future: 323 | Add a delete from queue function that removes a specific song from the queue based on position. -------------------------------------------------------------------------------- /Cogs/music.py: -------------------------------------------------------------------------------- 1 | import re 2 | import random 3 | import discord 4 | import lavalink 5 | from discord.ext import commands 6 | import asyncio 7 | import fileProcessing 8 | url_rx = re.compile(r'https?://(?:www\.)?.+') 9 | 10 | config = fileProcessing.read_config() 11 | roles = config["roles"] 12 | voice_permissions_check_list = config["voice_permission_check_list"] 13 | 14 | 15 | class LavalinkVoiceClient(discord.VoiceClient): 16 | def __init__(self, client: discord.Client, channel: discord.abc.Connectable): 17 | self.client = client 18 | self.channel = channel 19 | # ensure a client already exists 20 | if hasattr(self.client, 'lavalink'): 21 | self.lavalink = self.client.lavalink 22 | else: 23 | self.client.lavalink = lavalink.Client(client.user.id) 24 | self.client.lavalink.add_node( 25 | 'localhost', 26 | 2333, 27 | 'changeme123', 28 | 'us', 29 | 'default-node' 30 | ) 31 | self.lavalink = self.client.lavalink 32 | 33 | async def on_voice_server_update(self, data): 34 | # the data needs to be transformed before being handed down to 35 | # voice_update_handler 36 | lavalink_data = { 37 | 't': 'VOICE_SERVER_UPDATE', 38 | 'd': data 39 | } 40 | await self.lavalink.voice_update_handler(lavalink_data) 41 | 42 | async def on_voice_state_update(self, data): 43 | # the data needs to be transformed before being handed down to 44 | # voice_update_handler 45 | lavalink_data = { 46 | 't': 'VOICE_STATE_UPDATE', 47 | 'd': data 48 | } 49 | await self.lavalink.voice_update_handler(lavalink_data) 50 | 51 | async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False, self_mute: bool = False) -> None: 52 | """ 53 | Connect the bot to the voice channel and create a player_manager 54 | if it doesn't exist yet. 55 | """ 56 | self.lavalink.player_manager.create(guild_id=self.channel.guild.id) 57 | await self.channel.guild.change_voice_state(channel=self.channel, self_mute=self_mute, self_deaf=self_deaf) 58 | 59 | async def disconnect(self, *, force: bool = False) -> None: 60 | """ 61 | Handles the disconnect. 62 | Cleans up running player and leaves the voice client. 63 | """ 64 | player = self.lavalink.player_manager.get(self.channel.guild.id) 65 | 66 | # no need to disconnect if we are not connected 67 | if not force and not player.is_connected: 68 | return 69 | 70 | # None means disconnect 71 | await self.channel.guild.change_voice_state(channel=None) 72 | 73 | # update the channel_id of the player to None 74 | # this must be done because the on_voice_state_update that would set channel_id 75 | # to None doesn't get dispatched after the disconnect 76 | player.channel_id = None 77 | self.cleanup() 78 | 79 | 80 | class music(commands.Cog): 81 | def __init__(self, bot): 82 | self.bot = bot 83 | 84 | # This ensures the client isn't overwritten during cog reloads. 85 | if not hasattr(bot, 'lavalink'): 86 | bot.lavalink = lavalink.Client(bot.user.id) 87 | # PASSWORD HERE MUST MATCH YML 88 | bot.lavalink.add_node( 89 | '127.0.0.1', 2333, 'changeme123', 'us', 'default-node') 90 | 91 | lavalink.add_event_hook(self.track_hook) 92 | 93 | def cog_unload(self): 94 | self.bot.lavalink._event_hooks.clear() 95 | 96 | async def cog_before_invoke(self, ctx): 97 | """ Command before-invoke handler. """ 98 | guild_check = ctx.guild is not None 99 | 100 | if guild_check: 101 | await self.ensure_voice(ctx) 102 | 103 | return guild_check 104 | 105 | async def cog_command_error(self, ctx, error): 106 | if isinstance(error, commands.CommandInvokeError): 107 | await ctx.send(error.original) 108 | # The above handles errors thrown in this cog and shows them to the user. 109 | # This shouldn't be a problem as the only errors thrown in this cog are from `ensure_voice` 110 | # which contain a reason string, such as "Join a voicechannel" etc. You can modify the above 111 | # if you want to do things differently. 112 | 113 | async def ensure_voice(self, ctx): 114 | """ This check ensures that the bot and command author are in the same voicechannel. """ 115 | player = self.bot.lavalink.player_manager.create(ctx.guild.id) 116 | 117 | should_connect = ctx.command.name in voice_permissions_check_list 118 | 119 | if not ctx.author.voice or not ctx.author.voice.channel: 120 | # Our cog_command_error handler catches this and sends it to the voicechannel. 121 | # Exceptions allow us to "short-circuit" command invocation via checks so the 122 | # execution state of the command goes no further. 123 | raise commands.CommandInvokeError('Join a voicechannel first.') 124 | 125 | v_client = ctx.voice_client 126 | if not v_client: 127 | if not should_connect: 128 | raise commands.CommandInvokeError('Not connected.') 129 | 130 | permissions = ctx.author.voice.channel.permissions_for(ctx.me) 131 | 132 | if not permissions.connect or not permissions.speak: # Check user limit too? 133 | raise commands.CommandInvokeError( 134 | 'I need the `CONNECT` and `SPEAK` permissions.') 135 | 136 | player.store('channel', ctx.channel.id) 137 | await ctx.author.voice.channel.connect(cls=LavalinkVoiceClient) 138 | else: 139 | if v_client.channel.id != ctx.author.voice.channel.id: 140 | raise commands.CommandInvokeError( 141 | 'You need to be in my voicechannel.') 142 | 143 | async def track_hook(self, event): 144 | if isinstance(event, lavalink.events.QueueEndEvent): 145 | # When this track_hook receives a "QueueEndEvent" from lavalink.py 146 | # it indicates that there are no tracks left in the player's queue. 147 | # To save on resources, we can tell the bot to disconnect from the voicechannel. 148 | guild_id = event.player.guild_id 149 | guild = self.bot.get_guild(guild_id) 150 | await guild.voice_client.disconnect(force=True) 151 | 152 | # Allows for a song to be played, does not make sure people are in the same chat. 153 | @commands.command(name='play', description=".play {song name} to play a song, will connect the bot.") 154 | @commands.has_any_role(*roles) 155 | async def play_song(self, ctx, *, query: str): 156 | fileProcessing.logUpdate(ctx, query) # Add song requested to log 157 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 158 | query = query.strip('<>') 159 | 160 | if not url_rx.match(query): 161 | query = f'ytsearch:{query}' 162 | 163 | results = await player.node.get_tracks(query) 164 | 165 | if not results or not results.tracks: 166 | return await ctx.send('Nothing found!') 167 | 168 | embed = discord.Embed(color=discord.Color.blurple()) 169 | 170 | # Valid loadTypes are: 171 | # TRACK_LOADED - single video/direct URL) 172 | # PLAYLIST_LOADED - direct URL to playlist) 173 | # SEARCH_RESULT - query prefixed with either ytsearch: or scsearch:. 174 | # NO_MATCHES - query yielded no results 175 | # LOAD_FAILED - most likely, the video encountered an exception during loading. 176 | if results.load_type == 'PLAYLIST_LOADED': 177 | tracks = results.tracks 178 | 179 | for track in tracks: 180 | # Add all of the tracks from the playlist to the queue. 181 | player.add(requester=ctx.author.id, track=track) 182 | 183 | embed.title = 'Playlist Enqueued!' 184 | embed.description = f'{results.playlist_info.name} - {len(tracks)} tracks' 185 | else: 186 | track = results.tracks[0] 187 | embed.title = 'Track Enqueued' 188 | embed.description = f'[{track.title}]({track.uri})' 189 | 190 | player.add(requester=ctx.author.id, track=track) 191 | 192 | await ctx.send(embed=embed) 193 | 194 | if not player.is_playing: 195 | await player.play() 196 | 197 | @commands.command(name="playfromlist", aliases=["pfpl", "playl"], description="Loads a playlist into the queue to be played.") 198 | @commands.has_any_role(*roles) 199 | async def play_from_list(self, ctx, *, playlist_name): 200 | """ Searches and plays a song from a given query. """ 201 | # Get the player for this guild from cache. 202 | # Add playlist name to log file 203 | fileProcessing.logUpdate(ctx, playlist_name) 204 | songlist = fileProcessing.play_playlist(ctx, playlist_name) 205 | if songlist == False: 206 | return await ctx.send("Playlist not found.") 207 | await ctx.invoke(self.bot.get_command('play'), query=songlist[0]) 208 | songlist.pop(0) 209 | 210 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 211 | for track in songlist: 212 | try: 213 | query = f'ytsearch:{track}' 214 | results = await player.node.get_tracks(query) 215 | track = results['tracks'][0] 216 | track = lavalink.models.AudioTrack( 217 | track, ctx.author.id, recommended=True) 218 | player.add(requester=ctx.author.id, track=track) 219 | except Exception as error: 220 | print(error) 221 | 222 | await ctx.send(str(playlist_name) + " loaded successfully.") 223 | 224 | if not player.is_playing: 225 | await player.play() 226 | 227 | @commands.command(name='skip', description="Skips currently playing song.") 228 | @commands.has_any_role(*roles) 229 | async def skip_song(self, ctx, amount: int = 1): 230 | try: 231 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 232 | while (amount > 0): 233 | amount -= 1 234 | if not player.is_playing: 235 | raise commands.CommandInvokeError( 236 | "Nothing playing to skip.") 237 | else: 238 | if amount % 2 == 0: 239 | # Buffering for performance, testing needed to see if still neccessary. 240 | await asyncio.sleep(.1) 241 | await player.skip() 242 | if amount == 0: # make sure song skipped only prints once. 243 | await ctx.send("Song skipped.") 244 | except: 245 | if amount > 0: 246 | return await ctx.send("All songs skipped") 247 | 248 | raise commands.CommandInvokeError("Something went wrong...") 249 | 250 | @commands.command(name="clear", description="Clears all of the currently playing songs and makes the bot disconnect.") 251 | @commands.has_any_role(*roles) 252 | async def clear_queue(self, ctx): 253 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 254 | 255 | if not ctx.voice_client: 256 | return await ctx.send('Not connected.') 257 | 258 | if not ctx.author.voice or (player.is_connected and ctx.author.voice.channel.id != int(player.channel_id)): 259 | return await ctx.send('You\'re not in my voicechannel!') 260 | 261 | player.queue.clear() 262 | 263 | await player.stop() 264 | 265 | await ctx.voice_client.disconnect(force=True) 266 | await ctx.send('Queue was cleared.') 267 | 268 | @commands.command(name='pause', aliases=["ps"], description="Pauses a song if one is playing.") 269 | @commands.has_any_role(*roles) 270 | async def pause_bot(self, ctx): 271 | try: 272 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 273 | if player.is_playing: 274 | status = True 275 | await ctx.send("Song has been paused.") 276 | await player.set_pause(True) 277 | i = 0 278 | while i < 84: # This will periodically check to see if it has been unpaused 279 | await asyncio.sleep(5) # (84 * 5 = 7 minutes) 280 | i = i + 1 281 | # If its been unpaused no need to keep counting. 282 | if not player.paused: 283 | status = False 284 | break 285 | 286 | if player.paused and player.is_playing and status is True: 287 | await player.set_pause(False) # If paused unpause. 288 | await ctx.send("Automatically unpaused.") 289 | 290 | else: 291 | await ctx.send("No song is playing to be paused.") 292 | except: 293 | # Add a disconnect here. 294 | raise commands.CommandInvokeError("Unable to retrieve player...") 295 | 296 | @commands.command(name='unpause', aliases=['resume', 'start', 'up'], description="Unpauses a paused song.") 297 | @commands.has_any_role(*roles) 298 | async def unpause_bot(self, ctx): 299 | try: 300 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 301 | if player.paused: 302 | await ctx.send("Resuming song.") 303 | await player.set_pause(False) 304 | else: 305 | raise commands.CommandInvokeError( 306 | "Nothing is paused to resume.") 307 | except: 308 | raise commands.CommandInvokeError("Nothing playing.") 309 | 310 | @commands.command(name='queue', aliases=['playlist', 'songlist', 'upnext'], description="Shows songs up next in order, with the currently playing at the top.") 311 | @commands.has_any_role(*roles) 312 | async def queue(self, ctx, page=1): 313 | 314 | if not isinstance(page, int): 315 | raise commands.CommandInvokeError("Please enter a valid number.") 316 | 317 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 318 | if player.is_playing: 319 | songlist = player.queue 320 | list_collection = [] 321 | complete_list = '' 322 | complete_list = complete_list + "NP: " + \ 323 | player.current['title'] + "\n" 324 | i = 0 325 | for song in songlist: 326 | complete_list = complete_list + f"{i + 1}: {song['title']}\n" 327 | i = i + 1 328 | if i % 10 == 0: 329 | list_collection.append(complete_list) 330 | complete_list = '' 331 | 332 | # Check for the case where it is not a perfect multiple, add "half page" (< 10) or if there is only one song playing 333 | if i % 10 != 0 or i == 0: 334 | list_collection.append(complete_list) 335 | 336 | selection = int(page - 1) 337 | embed = discord.Embed() 338 | embed.title = 'Queue' 339 | # add an inital if to check if it is an int then do page -1 if its not int default to page 0 340 | if selection < 0: # handle negative number 341 | list_collection[0] += "Page: 1/" + str(len(list_collection)) 342 | embed.description = list_collection[0] 343 | # Handle a case where the index is greater than page amount 344 | elif selection > len(list_collection) - 1: 345 | list_collection[len(list_collection) - 1] += "Page: " + \ 346 | str(len(list_collection)) + "/" + str(len(list_collection)) 347 | embed.description = list_collection[len(list_collection) - 1] 348 | else: # Handle a valid input case. 349 | list_collection[selection] += "Page: " + \ 350 | str(page) + "/" + str(len(list_collection)) 351 | embed.description = list_collection[selection] 352 | await ctx.send(embed=embed) 353 | else: 354 | await ctx.send("Nothing is queued.") 355 | 356 | @commands.command(name="shuffle", description="New shuffle function that has to be called once and makes a new queue. Result is shown on \"queue\" commands now..") 357 | @commands.has_any_role(*roles) 358 | async def shuffle(self, ctx): 359 | try: 360 | player = self.bot.lavalink.player_manager.get(ctx.guild.id) 361 | if player.is_playing: 362 | songlist = player.queue 363 | # random.shuffle(songlist) # This breaks my bot at times.. Custom shuffle to slow this down. 364 | size = len(songlist) 365 | for x in range(0, size): 366 | if (x % 8 == 0): 367 | await asyncio.sleep(0.1) 368 | temp = songlist[x] 369 | randnum = random.randint(0, size - 1) 370 | songlist[x] = songlist[randnum] 371 | songlist[randnum] = temp 372 | await ctx.send("Finished.") 373 | else: 374 | raise commands.CommandInvokeError("Nothing playing!") 375 | 376 | except Exception as error: 377 | print(error) 378 | 379 | 380 | async def setup(bot): 381 | await bot.add_cog(music(bot)) 382 | --------------------------------------------------------------------------------