675 lines
24 KiB
Python
675 lines
24 KiB
Python
import datetime
|
|
import discord
|
|
from discord.ext import commands
|
|
import random
|
|
import asyncio
|
|
import itertools
|
|
import sys
|
|
import traceback
|
|
import requests
|
|
import os
|
|
import validators
|
|
import threading
|
|
import pickle
|
|
from async_timeout import timeout
|
|
from functools import partial
|
|
import yt_dlp
|
|
from yt_dlp import YoutubeDL
|
|
import logging
|
|
|
|
import database
|
|
|
|
logger = logging.getLogger("music_player")
|
|
|
|
# Get API key for last.fm
|
|
LASTFM_API_KEY = os.getenv('LASTFM_API_KEY')
|
|
|
|
# TEMORARY LIST OF SONGS
|
|
songs = [
|
|
"I Love It - Icona Pop",
|
|
"Vanished - Crystal Castles",
|
|
"We Like To Party - Vengaboys",
|
|
"Gimme! Gimme! Gimme! - ABBA",
|
|
"Dancing Queen - ABBA",
|
|
"I Wanna Dance With Somebody - Whitney Houston",
|
|
"Dance in the Dark - Lady Gaga",
|
|
"Telephone - Lady Gaga",
|
|
"Just Dance - Lady Gaga",
|
|
"Rewind - Charli xcx",
|
|
"Nasty - Tinashe",
|
|
"Rush - Troy Sivan",
|
|
"360 - Charli xcx",
|
|
"Talk talk - Charli xcx",
|
|
"Von dutch - Charli xcx",
|
|
"365 - Charli xcx",
|
|
"HOT TO GO! - Chappell Roan",
|
|
"Super Graphic Ultra Modern Girl - Chappell Roan",
|
|
"Womanizer - Britney Spears",
|
|
"Red Wind Supernova - Chappell Roan",
|
|
"Toxic - Britney Spears",
|
|
"We Found Love - Rihanna",
|
|
"212 - Azealia Banks",
|
|
"Bad Romance - Lady Gaga",
|
|
"Girl, so confusing - Charli xcx",
|
|
"Alter Ego - Doechii",
|
|
"Break Free - Ariana Grande",
|
|
"Raingurl - Yaeji",
|
|
"Thot Shit - Megan Thee Stallion",
|
|
"BREAK MY SOUL - Beyonce",
|
|
"She Wolf - Shakira",
|
|
"Your Love Is My Drug - Ke$ha",
|
|
"365 featuring shygirl - Charli xcx",
|
|
"Applause - Lady Gaga",
|
|
"Lay All Your Love On Me - ABBA",
|
|
"Apple - Charli xcx",
|
|
"Pump It Up - Endor",
|
|
"Everytime We Touch - Cascada",
|
|
"Fantasy - Mariah Carey",
|
|
"Water - Tyla",
|
|
"Be The One - Eli Brown",
|
|
"3 - Britney Spears",
|
|
"Guess featuring billie ellish - Charli xcx",
|
|
"Bunny Is A Rider - Doss Remix - Caroline Polachek",
|
|
"Do You Miss Me? - PinkPantheress",
|
|
"Perfect (Exceeder) - Mason",
|
|
"Better Off Alone (Laidback Luke Remix) - Alice DJ",
|
|
"Beauty And A Beat - Justin Bieber",
|
|
"Girl, so confusing - Charli xcx",
|
|
"Got Me Started - Troy Sivan",
|
|
"Gimme More - Britney Spears",
|
|
"Around the World - Daft Punk",
|
|
"Harder, Better, Faster, Stronger - Daft Punk",
|
|
"Sweet Dreams - Eurythmics",
|
|
"Dancing Elephants - DJ Minx Remix - Rochelle Jordan",
|
|
"MADELINE - INJI",
|
|
"Baddy On The Floor - Jamix xx",
|
|
"SWEET HONEY BUCKIIN' - Beyonce",
|
|
"Boots & Boys - Ke$ha"
|
|
]
|
|
|
|
# Suppress noise about console usage from errors
|
|
yt_dlp.utils.bug_reports_message = lambda: ''
|
|
|
|
ytdlopts = {
|
|
'format': 'bestaudio/best',
|
|
'outtmpl': 'downloads/%(extractor)s-%(id)s-%(title)s.%(ext)s',
|
|
'restrictfilenames': True,
|
|
'noplaylist': True,
|
|
'nocheckcertificate': True,
|
|
'ignoreerrors': False,
|
|
'logtostderr': False,
|
|
'quiet': True,
|
|
'no_warnings': True,
|
|
'default_search': 'auto',
|
|
'source_address': '0.0.0.0', # ipv6 addresses cause issues sometimes
|
|
'retries': 5,
|
|
'ignoreerrors': True,
|
|
# 'throttled_rate': '1M',
|
|
'fragment_retries': 10 # Prevents seemingly random stream crashes
|
|
}
|
|
|
|
ffmpegopts = {
|
|
'before_options': '-nostdin',
|
|
'options': '-vn'
|
|
}
|
|
|
|
ytdl = YoutubeDL(ytdlopts)
|
|
|
|
|
|
class VoiceConnectionError(commands.CommandError):
|
|
"""Custom Exception class for connection errors."""
|
|
|
|
|
|
class InvalidVoiceChannel(VoiceConnectionError):
|
|
"""Exception for cases of invalid Voice Channels."""
|
|
|
|
|
|
class YTDLSource(discord.PCMVolumeTransformer):
|
|
|
|
def __init__(self, source, *, data, requester):
|
|
super().__init__(source)
|
|
self.requester = requester
|
|
|
|
# YouTube Metadata
|
|
self.title = data.get('title')
|
|
self.web_url = data.get('webpage_url')
|
|
self.thumbnail_url = data.get('thumbnail')
|
|
self.duration = data.get('duration')
|
|
|
|
# Song metadata
|
|
self.search_term = ""
|
|
self.artist = ""
|
|
self.song_title = ""
|
|
|
|
# YTDL info dicts (data) have other useful information you might want
|
|
# https://github.com/rg3/youtube-dl/blob/master/README.md
|
|
|
|
def __getitem__(self, item: str):
|
|
"""Allows us to access attributes similar to a dict.
|
|
This is only useful when you are NOT downloading.
|
|
"""
|
|
return self.__getattribute__(item)
|
|
|
|
@classmethod
|
|
async def create_source(cls, ctx, search: str, *, loop, download=False, silent=False, artist='Unknown', song_title='Unknown'):
|
|
loop = loop or asyncio.get_event_loop()
|
|
|
|
# If we got a YouTube link, get the video title for the song search
|
|
if validators.url(search):
|
|
with YoutubeDL() as ydl:
|
|
info = ydl.extract_info(search, download=False)
|
|
search_term = info.get('title', '')
|
|
else:
|
|
search_term = search
|
|
|
|
# Get song metadata
|
|
logger.info(f"Searching LastFM for: '{search_term}'")
|
|
url = f"http://ws.audioscrobbler.com/2.0/?method=track.search&"\
|
|
f"track={search_term}&api_key={LASTFM_API_KEY}&format=json"
|
|
response = requests.get(url)
|
|
lastfm_data = response.json()
|
|
|
|
# Let's get the first result, if any
|
|
if lastfm_data['results']['trackmatches']['track']:
|
|
track = lastfm_data['results']['trackmatches']['track'][0]
|
|
artist = track['artist']
|
|
song_title = track['name']
|
|
|
|
# Adjust search term if we didn't get a URL
|
|
if not validators.url(search):
|
|
search = f"{song_title} {artist} official audio"
|
|
|
|
# Get YouTube video source
|
|
logger.info(f"Getting YouTube video: {search}")
|
|
to_run = partial(ytdl.extract_info, url=search, download=download)
|
|
data = await loop.run_in_executor(None, to_run)
|
|
|
|
# There's an error with yt-dlp that throws a 403: Forbidden error, so
|
|
# only proceed if it returns anything
|
|
if data and 'entries' in data:
|
|
# take first item from a playlist
|
|
data = data['entries'][0]
|
|
|
|
if download:
|
|
source = ytdl.prepare_filename(data)
|
|
else:
|
|
return {'webpage_url': data['webpage_url'], 'requester': ctx.author, 'title': data['title']}
|
|
|
|
ffmpeg_source = cls(discord.FFmpegPCMAudio(source), data=data, requester=ctx.author)
|
|
# TODO: ADD THESE TO THE CONSTRUCTOR
|
|
ffmpeg_source.search_term = search_term
|
|
ffmpeg_source.artist = artist
|
|
ffmpeg_source.song_title = song_title
|
|
ffmpeg_source.filename = source
|
|
|
|
return ffmpeg_source
|
|
|
|
@classmethod
|
|
async def regather_stream(cls, data, *, loop):
|
|
"""Used for preparing a stream, instead of downloading.
|
|
Since Youtube Streaming links expire."""
|
|
loop = loop or asyncio.get_event_loop()
|
|
requester = data['requester']
|
|
|
|
to_run = partial(ytdl.extract_info, url=data['webpage_url'], download=False)
|
|
data = await loop.run_in_executor(None, to_run)
|
|
|
|
return cls(discord.FFmpegPCMAudio(data['url']), data=data, requester=requester)
|
|
|
|
|
|
class MusicPlayer:
|
|
"""A class which is assigned to each guild using the bot for Music.
|
|
This class implements a queue and loop, which allows for different guilds to listen to different playlists
|
|
simultaneously.
|
|
When the bot disconnects from the Voice it's instance will be destroyed.
|
|
"""
|
|
|
|
__slots__ = ('bot', '_guild', '_channel', '_cog', 'queue', 'next', 'current', 'np', 'volume', 'dj_mode')
|
|
|
|
# Each player is assiciated with a guild, so create a lock for when we do
|
|
# volatile things in the server like delete previous messages
|
|
guild_lock_ = asyncio.Lock()
|
|
|
|
def __init__(self, ctx):
|
|
self.bot = ctx.bot
|
|
self._guild = ctx.guild
|
|
self._channel = ctx.channel
|
|
self._cog = ctx.cog
|
|
|
|
self.queue = asyncio.Queue()
|
|
self.next = asyncio.Event()
|
|
|
|
self.np = None # Now playing message
|
|
self.volume = .5
|
|
self.current = None
|
|
self.dj_mode = False
|
|
|
|
ctx.bot.loop.create_task(self.player_loop())
|
|
|
|
async def player_loop(self):
|
|
"""Our main player loop."""
|
|
await self.bot.wait_until_ready()
|
|
|
|
while not self.bot.is_closed():
|
|
self.next.clear()
|
|
|
|
# Always get a song if there's one in the queue
|
|
if self.queue.qsize() > 0 or self.dj_mode is False:
|
|
logger.info("Getting song from play queue")
|
|
try:
|
|
# Wait for the next song. If we timeout cancel the player
|
|
# and disconnect...
|
|
async with timeout(300): # 5 minutes...
|
|
source = await self.queue.get()
|
|
except asyncio.TimeoutError:
|
|
return self.destroy(self._guild)
|
|
# Otherwise we're in DJ mode and a user hasn't requested one, so
|
|
# pick a song at random and create a source for it
|
|
else:
|
|
logger.info(
|
|
"Queue is empty and DJ mode is on. Picking song at random")
|
|
source = await YTDLSource.create_source(
|
|
None, random.choice(songs),
|
|
loop=self.bot.loop,
|
|
download=True
|
|
)
|
|
|
|
# For the time being, we're going to use 'None' to signal to the
|
|
# player that is should go back around and check for a song again,
|
|
# mainly because DJ mode was switched on and it should pick a song
|
|
# at random this time
|
|
if source is None:
|
|
continue
|
|
|
|
if not isinstance(source, YTDLSource):
|
|
# Source was probably a stream (not downloaded)
|
|
# So we should regather to prevent stream expiration
|
|
try:
|
|
source = await YTDLSource.regather_stream(source, loop=self.bot.loop)
|
|
except Exception as e:
|
|
await self._channel.send(f'There was an error processing your song.\n'
|
|
f'```css\n[{e}]\n```')
|
|
continue
|
|
|
|
source.volume = self.volume
|
|
self.current = source
|
|
|
|
logger.info(f"Playing '{source.song_title}' by '{source.artist}'")
|
|
self._guild.voice_client.play(source, after=lambda _: self.bot.loop.call_soon_threadsafe(self.next.set))
|
|
|
|
logger.info("Updating presense and 'now playing' message")
|
|
await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.custom, name="custom", state=f"🎵 {source.song_title} by {source.artist}"))
|
|
await self.update_now_playing_message(repost=True)
|
|
|
|
logger.info("Waiting for song to finish")
|
|
await self.next.wait()
|
|
|
|
if os.path.exists(source.filename):
|
|
os.remove(source.filename)
|
|
|
|
# Make sure the FFmpeg process is cleaned up.
|
|
try:
|
|
source.cleanup()
|
|
except:
|
|
pass
|
|
self.current = None
|
|
|
|
# Update bot statuses to match no song playing
|
|
await self.bot.change_presence(status=None)
|
|
await self.update_now_playing_message(repost=False)
|
|
|
|
async def update_now_playing_message(self, repost=False, emoji='▶️'):
|
|
await self.bot.wait_until_ready()
|
|
|
|
# Create and post new 'Now Playing' message
|
|
embed = discord.Embed(
|
|
title=f"{emoji} Now Playing", color=discord.Color.green())
|
|
|
|
# Get and add the thumbnail
|
|
if self.current:
|
|
embed.set_thumbnail(url=self.current.thumbnail_url)
|
|
embed.add_field(
|
|
name="",
|
|
value=f"[{self.current.song_title}]({self.current.web_url}) - "
|
|
f"{self.current.artist}",
|
|
inline=False
|
|
)
|
|
|
|
# Add all upcoming songs
|
|
# Possibly dangerous, but only obvious solution
|
|
queue = [s for s in self.queue._queue if s is not None]
|
|
if self.queue.qsize() > 0:
|
|
value_str = ""
|
|
for i, song in enumerate(queue):
|
|
value_str += f"{i+1}. [{song.song_title}]({song.web_url}) - "\
|
|
f"{song.artist}\n"
|
|
embed.add_field(name="Queue", value=value_str, inline=False)
|
|
|
|
# There's a chance we'll delete a message another coroutine is using, so
|
|
# lock the messages
|
|
async with self. guild_lock_:
|
|
async for message in self._channel.history(limit=50):
|
|
if len(message.embeds) == 1 and message.embeds[0].title and "Now Playing" in message.embeds[0].title:
|
|
if repost:
|
|
await message.delete()
|
|
else:
|
|
await message.edit(embed=embed)
|
|
break
|
|
|
|
if repost:
|
|
self.np = await self._channel.send(embed=embed)
|
|
|
|
|
|
def destroy(self, guild):
|
|
"""Disconnect and cleanup the player."""
|
|
return self.bot.loop.create_task(self._cog.cleanup(guild))
|
|
|
|
|
|
class Music(commands.Cog):
|
|
"""Music related commands."""
|
|
|
|
__slots__ = ('bot', 'players')
|
|
|
|
def __init__(self, bot):
|
|
self.bot = bot
|
|
self.players = {}
|
|
self.last_tag_play_time = datetime.datetime.now()
|
|
|
|
# Get a reference to the database
|
|
# TODO: MAKE THIS INJECTED
|
|
self.db = database.Database("boywife_bot.db")
|
|
|
|
# def update_cache():
|
|
# with yt_dlp.YoutubeDL({'quiet': True}) as ydl:
|
|
# self.boywife_tracks = ydl.extract_info('https://soundcloud.com/djboywife', download=False)['entries']
|
|
|
|
# with open('soundcloud-cache', 'w') as f:
|
|
# f.write(str(self.boywife_tracks))
|
|
# # pickle.dump(self.boywife_tracks, f)
|
|
|
|
# if os.path.exists('soundcloud-cache'):
|
|
# with open('soundcloud-cache', 'r') as f:
|
|
# exec(f'self.boywife_tracks = {f.read()}')
|
|
# # self.boywife_tracks = pickle.load(f)
|
|
# threading.Thread(target=update_cache).start()
|
|
# else:
|
|
# update_cache()
|
|
|
|
|
|
async def cleanup(self, guild):
|
|
try:
|
|
await guild.voice_client.disconnect()
|
|
except AttributeError:
|
|
pass
|
|
|
|
try:
|
|
del self.players[guild.id]
|
|
except KeyError:
|
|
pass
|
|
|
|
async def __local_check(self, ctx):
|
|
"""A local check which applies to all commands in this cog."""
|
|
if not ctx.guild:
|
|
raise commands.NoPrivateMessage
|
|
return True
|
|
|
|
async def __error(self, ctx, error):
|
|
"""A local error handler for all errors arising from commands in this cog."""
|
|
if isinstance(error, commands.NoPrivateMessage):
|
|
try:
|
|
return await ctx.send('This command can not be used in Private Messages.')
|
|
except discord.HTTPException:
|
|
pass
|
|
elif isinstance(error, InvalidVoiceChannel):
|
|
await ctx.send('Error connecting to Voice Channel. '
|
|
'Please make sure you are in a valid channel or provide me with one')
|
|
|
|
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
|
|
traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
|
|
|
|
def get_player(self, ctx):
|
|
"""Retrieve the guild player, or generate one."""
|
|
try:
|
|
player = self.players[ctx.guild.id]
|
|
except KeyError:
|
|
player = MusicPlayer(ctx)
|
|
self.players[ctx.guild.id] = player
|
|
|
|
return player
|
|
|
|
@commands.command(name='join', aliases=['connect', 'j'], description="connects to voice")
|
|
async def connect_(self, ctx, *, channel: discord.VoiceChannel=None):
|
|
"""Connect to voice.
|
|
Parameters
|
|
------------
|
|
channel: discord.VoiceChannel [Optional]
|
|
The channel to connect to. If a channel is not specified, an attempt to join the voice channel you are in
|
|
will be made.
|
|
This command also handles moving the bot to different channels.
|
|
"""
|
|
if not channel:
|
|
try:
|
|
channel = ctx.author.voice.channel
|
|
except AttributeError:
|
|
embed = discord.Embed(title="", description="No channel to join. Please call `,join` from a voice channel.", color=discord.Color.green())
|
|
await ctx.send(embed=embed)
|
|
raise InvalidVoiceChannel('No channel to join. Please either specify a valid channel or join one.')
|
|
|
|
vc = ctx.voice_client
|
|
|
|
if vc:
|
|
if vc.channel.id == channel.id:
|
|
return
|
|
try:
|
|
await vc.move_to(channel)
|
|
except asyncio.TimeoutError:
|
|
raise VoiceConnectionError(f'Moving to channel: <{channel}> timed out.')
|
|
else:
|
|
try:
|
|
await channel.connect()
|
|
except asyncio.TimeoutError:
|
|
raise VoiceConnectionError(f'Connecting to channel: <{channel}> timed out.')
|
|
# await ctx.message.add_reaction('👍')
|
|
|
|
@commands.command(name='play', aliases=['sing','p'], description="streams music")
|
|
async def play_(self, ctx, *, search: str = None):
|
|
"""Request a song and add it to the queue.
|
|
This command attempts to join a valid voice channel if the bot is not already in one.
|
|
Uses YTDL to automatically search and retrieve a song.
|
|
Parameters
|
|
------------
|
|
search: str [Required]
|
|
The song to search and retrieve using YTDL. This could be a simple search, an ID or URL.
|
|
"""
|
|
# Ensure we're connected to the proper voice channel
|
|
vc = ctx.voice_client
|
|
if not vc:
|
|
await ctx.invoke(self.connect_)
|
|
|
|
# Send message to say we're working on it
|
|
embed = discord.Embed(
|
|
title=f"🔎 Searching for:",
|
|
description=f"{search}",
|
|
color=discord.Color.green()
|
|
)
|
|
message = await ctx.channel.send(embed=embed)
|
|
|
|
# Create source
|
|
try:
|
|
source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop, download=True)
|
|
except:
|
|
pass
|
|
|
|
# Update previous message to show found song and video
|
|
embed = discord.Embed(
|
|
title=f"Queued",
|
|
description=f"[{source.song_title}]({source.web_url}) - {source.artist}",
|
|
color=discord.Color.green()
|
|
)
|
|
embed.set_thumbnail(url=source.thumbnail_url)
|
|
await message.edit(embed=embed)
|
|
|
|
# Add song to the corresponding player object
|
|
player = self.get_player(ctx)
|
|
await player.queue.put(source)
|
|
await player.update_now_playing_message()
|
|
|
|
@commands.command(name="djmode", aliases=['dj'], description="Turns DJ mode on or off.")
|
|
async def djmode_(self, ctx, *, mode: str = "on"):
|
|
"""Turns DJ mode on or off. When on, the bot will play songs
|
|
automatically."""
|
|
# Get desired mode
|
|
mode = mode.lower().strip()
|
|
if mode in ("true", "t", "yes", "y", "on"):
|
|
mode = True
|
|
elif mode in ("false", "f", "no", "n", "off"):
|
|
mode = False
|
|
else:
|
|
return
|
|
|
|
# Switch to desired mode
|
|
player = self.get_player(ctx)
|
|
player.dj_mode = mode
|
|
|
|
# Break player out of waiting on queue so it can pick a song at random
|
|
if player.dj_mode:
|
|
await player.queue.put(None)
|
|
|
|
@commands.command(name='pause', description="pauses music")
|
|
async def pause_(self, ctx):
|
|
"""Pause the currently playing song."""
|
|
vc = ctx.voice_client
|
|
|
|
if not vc or not vc.is_playing():
|
|
embed = discord.Embed(title="", description="I am currently not playing anything", color=discord.Color.green())
|
|
return await ctx.send(embed=embed)
|
|
elif vc.is_paused():
|
|
return
|
|
|
|
vc.pause()
|
|
|
|
# Update the 'Now Playing' message to reflect its paused
|
|
player = self.get_player(ctx)
|
|
await player.update_now_playing_message(emoji='⏸️')
|
|
|
|
@commands.command(name='resume', description="resumes music")
|
|
async def resume_(self, ctx):
|
|
"""Resume the currently paused song."""
|
|
vc = ctx.voice_client
|
|
|
|
if not vc or not vc.is_connected():
|
|
embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green())
|
|
return await ctx.send(embed=embed)
|
|
elif not vc.is_paused():
|
|
return
|
|
|
|
vc.resume()
|
|
|
|
# Update the 'Now Playing' message to reflect its resumed
|
|
player = self.get_player(ctx)
|
|
await player.update_now_playing_message()
|
|
|
|
@commands.command(name='skip', description="skips to next song in queue")
|
|
async def skip_(self, ctx):
|
|
"""Skip the song."""
|
|
vc = ctx.voice_client
|
|
|
|
if not vc or not vc.is_connected():
|
|
embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green())
|
|
return await ctx.send(embed=embed)
|
|
|
|
if vc.is_paused():
|
|
pass
|
|
elif not vc.is_playing():
|
|
return
|
|
|
|
vc.stop()
|
|
|
|
@commands.command(name='remove', aliases=['rm'], description="removes specified song from queue")
|
|
async def remove_(self, ctx, pos : int=None):
|
|
"""Removes specified song from queue"""
|
|
|
|
vc = ctx.voice_client
|
|
|
|
if not vc or not vc.is_connected():
|
|
embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green())
|
|
return await ctx.send(embed=embed)
|
|
|
|
player = self.get_player(ctx)
|
|
if pos == None:
|
|
player.queue._queue.pop()
|
|
else:
|
|
try:
|
|
s = player.queue._queue[pos-1]
|
|
del player.queue._queue[pos-1]
|
|
embed = discord.Embed(title="", description=f"Removed [{s['title']}]({s['webpage_url']}) [{s['requester'].mention}]", color=discord.Color.green())
|
|
await ctx.send(embed=embed)
|
|
except:
|
|
embed = discord.Embed(title="", description=f'Could not find a track for "{pos}"', color=discord.Color.green())
|
|
await ctx.send(embed=embed)
|
|
|
|
@commands.command(name='clear', aliases=['clr', 'cl', 'cr'], description="clears entire queue")
|
|
async def clear_(self, ctx):
|
|
"""Deletes entire queue of upcoming songs."""
|
|
|
|
vc = ctx.voice_client
|
|
|
|
if not vc or not vc.is_connected():
|
|
embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green())
|
|
return await ctx.send(embed=embed)
|
|
|
|
player = self.get_player(ctx)
|
|
player.queue._queue.clear()
|
|
await ctx.send('**Cleared**')
|
|
|
|
@commands.command(name='volume', aliases=['vol', 'v'], description="changes Kermit's volume")
|
|
async def change_volume(self, ctx, *, vol: float=None):
|
|
"""Change the player volume.
|
|
Parameters
|
|
------------
|
|
volume: float or int [Required]
|
|
The volume to set the player to in percentage. This must be between 1 and 100.
|
|
"""
|
|
vc = ctx.voice_client
|
|
|
|
if not vc or not vc.is_connected():
|
|
embed = discord.Embed(title="", description="I am not currently connected to voice", color=discord.Color.green())
|
|
return await ctx.send(embed=embed)
|
|
|
|
if not vol:
|
|
embed = discord.Embed(title="", description=f"🔊 **{(vc.source.volume)*100}%**", color=discord.Color.green())
|
|
return await ctx.send(embed=embed)
|
|
|
|
if not 0 < vol < 101:
|
|
embed = discord.Embed(title="", description="Please enter a value between 1 and 100", color=discord.Color.green())
|
|
return await ctx.send(embed=embed)
|
|
|
|
player = self.get_player(ctx)
|
|
|
|
if vc.source:
|
|
vc.source.volume = vol / 100
|
|
|
|
player.volume = vol / 100
|
|
embed = discord.Embed(title="", description=f'**`{ctx.author}`** set the volume to **{vol}%**', color=discord.Color.green())
|
|
await ctx.send(embed=embed)
|
|
|
|
@commands.command(name='leave', aliases=["stop", "dc", "disconnect", "bye"], description="stops music and disconnects from voice")
|
|
async def leave_(self, ctx):
|
|
"""Stop the currently playing song and destroy the player.
|
|
!Warning!
|
|
This will destroy the player assigned to your guild, also deleting any queued songs and settings.
|
|
"""
|
|
vc = ctx.voice_client
|
|
|
|
if not vc or not vc.is_connected():
|
|
embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green())
|
|
return await ctx.send(embed=embed)
|
|
|
|
await ctx.message.add_reaction('👋')
|
|
|
|
await self.cleanup(ctx.guild)
|
|
|
|
|
|
async def setup(bot):
|
|
await bot.add_cog(Music(bot))
|