Untitled
unknown
plain_text
2 years ago
25 kB
5
Indexable
import discord
import asyncio
import random
import youtube_dl
import string
import os
from discord.ext import commands
from googleapiclient.discovery import build
from discord.ext.commands import command
# import pymongo
# NOTE: Import pymongo if you are using the database function commands
# NOTE: Also add `pymongo` and `dnspython` inside the requirements.txt file if you are using pymongo
# TODO: CREATE PLAYLIST SUPPORT FOR MUSIC
# NOTE: Without database, the music bot will not save your volume
# flat-playlist:True?
# extract_flat:True
# audioquality 0 best 9 worst
# format bestaudio/best or worstaudio
# 'noplaylist': None
ytdl_format_options = {
'audioquality': 5,
'format': 'bestaudio',
'outtmpl': '{}',
'restrictfilenames': True,
'flatplaylist': True,
'nocheckcertificate': True,
'ignoreerrors': True,
'logtostderr': False,
"extractaudio": True,
"audioformat": "opus",
'quiet': True,
'no_warnings': True,
'default_search': 'auto',
# bind to ipv4 since ipv6 addresses cause issues sometimes
'source_address': '0.0.0.0'
}
# Download youtube-dl options
ytdl_download_format_options = {
'format': 'bestaudio/best',
'outtmpl': 'downloads/%(title)s.mp3',
'reactrictfilenames': True,
'noplaylist': True,
'nocheckcertificate': True,
'ignoreerrors': False,
'logtostderr': False,
'quiet': True,
'no_warnings': True,
'default_search': 'auto',
# bind to ipv4 since ipv6 addreacses cause issues sometimes
'source_addreacs': '0.0.0.0',
'output': r'youtube-dl',
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '320',
}]
}
stim = {
'default_search': 'auto',
"ignoreerrors": True,
'quiet': True,
"no_warnings": True,
"simulate": True, # do not keep the video files
"nooverwrites": True,
"keepvideo": False,
"noplaylist": True,
"skip_download": False,
# bind to ipv4 since ipv6 addresses cause issues sometimes
'source_address': '0.0.0.0'
}
ffmpeg_options = {
'options': '-vn',
# 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5'
}
class Downloader(discord.PCMVolumeTransformer):
def __init__(self, source, *, data, volume=0.5):
super().__init__(source, volume)
self.data = data
self.title = data.get('title')
self.url = data.get("url")
self.thumbnail = data.get('thumbnail')
self.duration = data.get('duration')
self.views = data.get('view_count')
self.playlist = {}
@classmethod
async def video_url(cls, url, ytdl, *, loop=None, stream=False):
"""
Download the song file and data
"""
loop = loop or asyncio.get_event_loop()
data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream))
song_list = {'queue': []}
if 'entries' in data:
if len(data['entries']) > 1:
playlist_titles = [title['title'] for title in data['entries']]
song_list = {'queue': playlist_titles}
song_list['queue'].pop(0)
data = data['entries'][0]
filename = data['url'] if stream else ytdl.prepare_filename(data)
return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data), song_list
async def get_info(self, url):
"""
Get the info of the next song by not downloading the actual file but just the data of song/query
"""
yt = youtube_dl.YoutubeDL(stim)
down = yt.extract_info(url, download=False)
data1 = {'queue': []}
if 'entries' in down:
if len(down['entries']) > 1:
playlist_titles = [title['title'] for title in down['entries']]
data1 = {'title': down['title'], 'queue': playlist_titles}
down = down['entries'][0]['title']
return down, data1
class MusicPlayer(commands.Cog, name='Music'):
def __init__(self, bot):
self.bot = bot
# self.music=self.database.find_one('music')
self.player = {
"audio_files": []
}
# self.database_setup()
def database_setup(self):
URL = os.getenv("MONGO")
if URL is None:
return False
@property
def random_color(self):
return discord.Color.from_rgb(random.randint(1, 255), random.randint(1, 255), random.randint(1, 255))
async def yt_info(self, song):
"""
Get info from youtube
"""
API_KEY = 'API_KEY'
youtube = build('youtube', 'v3', developerKey=API_KEY)
song_data = youtube.search().list(part='snippet').execute()
return song_data[0]
@commands.Cog.listener('on_voice_state_update')
async def music_voice(self, user, before, after):
"""
Clear the server's playlist after bot leave the voice channel
"""
if after.channel is None and user.id == self.bot.user.id:
try:
self.player[user.guild.id]['queue'].clear()
except KeyError:
# NOTE: server ID not in bot's local self.player dict
# Server ID lost or was not in data before disconnecting
print(f"Failed to get guild id {user.guild.id}")
async def filename_generator(self):
"""
Generate a unique file name for the song file to be named as
"""
chars = list(string.ascii_letters+string.digits)
name = ''
for i in range(random.randint(9, 25)):
name += random.choice(chars)
if name not in self.player['audio_files']:
return name
return await self.filename_generator()
async def playlist(self, data, msg):
"""
THIS FUNCTION IS FOR WHEN YOUTUBE LINK IS A PLAYLIST
Add song into the server's playlist inside the self.player dict
"""
for i in data['queue']:
print(i)
self.player[msg.guild.id]['queue'].append(
{'title': i, 'author': msg})
async def queue(self, msg, song):
"""
Add the query/song to the queue of the server
"""
title1 = await Downloader.get_info(self, url=song)
title = title1[0]
data = title1[1]
# NOTE:needs fix here
if data['queue']:
await self.playlist(data, msg)
# NOTE: needs to be embeded to make it better output
return await msg.send(f"Added playlist {data['title']} to queue")
self.player[msg.guild.id]['queue'].append(
{'title': title, 'author': msg})
return await msg.send(f"**{title} added to queue**".title())
async def voice_check(self, msg):
"""
function used to make bot leave voice channel if music not being played for longer than 2 minutes
"""
if msg.voice_client is not None:
await asyncio.sleep(120)
if msg.voice_client is not None and msg.voice_client.is_playing() is False and msg.voice_client.is_paused() is False:
await msg.voice_client.disconnect()
async def clear_data(self, msg):
"""
Clear the local dict data
name - remove file name from dict
remove file and filename from directory
remove filename from global audio file names
"""
name = self.player[msg.guild.id]['name']
os.remove(name)
self.player['audio_files'].remove(name)
async def loop_song(self, msg):
"""
Loop the currently playing song by replaying the same audio file via `discord.PCMVolumeTransformer()`
"""
source = discord.PCMVolumeTransformer(
discord.FFmpegPCMAudio(self.player[msg.guild.id]['name']))
loop = asyncio.get_event_loop()
try:
msg.voice_client.play(
source, after=lambda a: loop.create_task(self.done(msg)))
msg.voice_client.source.volume = self.player[msg.guild.id]['volume']
# if str(msg.guild.id) in self.music:
# msg.voice_client.source.volume=self.music['vol']/100
except Exception as Error:
# Has no attribute play
print(Error) # NOTE: output back the error for later debugging
async def done(self, msg, msgId: int = None):
"""
Function to run once song completes
Delete the "Now playing" message via ID
"""
if msgId:
try:
message = await msg.channel.fetch_message(msgId)
await message.delete()
except Exception as Error:
print("Failed to get the message")
if self.player[msg.guild.id]['reset'] is True:
self.player[msg.guild.id]['reset'] = False
return await self.loop_song(msg)
if msg.guild.id in self.player and self.player[msg.guild.id]['repeat'] is True:
return await self.loop_song(msg)
await self.clear_data(msg)
if self.player[msg.guild.id]['queue']:
queue_data = self.player[msg.guild.id]['queue'].pop(0)
return await self.start_song(msg=queue_data['author'], song=queue_data['title'])
else:
await self.voice_check(msg)
async def start_song(self, msg, song):
new_opts = ytdl_format_options.copy()
audio_name = await self.filename_generator()
self.player['audio_files'].append(audio_name)
new_opts['outtmpl'] = new_opts['outtmpl'].format(audio_name)
ytdl = youtube_dl.YoutubeDL(new_opts)
download1 = await Downloader.video_url(song, ytdl=ytdl, loop=self.bot.loop)
download = download1[0]
data = download1[1]
self.player[msg.guild.id]['name'] = audio_name
emb = discord.Embed(colour=self.random_color, title='Now Playing',
description=download.title, url=download.url)
emb.set_thumbnail(url=download.thumbnail)
emb.set_footer(
text=f'Requested by {msg.author.display_name}', icon_url=msg.author.avatar_url)
loop = asyncio.get_event_loop()
if data['queue']:
await self.playlist(data, msg)
msgId = await msg.send(embed=emb)
self.player[msg.guild.id]['player'] = download
self.player[msg.guild.id]['author'] = msg
msg.voice_client.play(
download, after=lambda a: loop.create_task(self.done(msg, msgId.id)))
# if str(msg.guild.id) in self.music: #NOTE adds user's default volume if in database
# msg.voice_client.source.volume=self.music[str(msg.guild.id)]['vol']/100
msg.voice_client.source.volume = self.player[msg.guild.id]['volume']
return msg.voice_client
@command()
async def play(self, msg, *, song):
"""
Play a song with given url or title from Youtube
`Ex:` s.play Titanium David Guetta
`Command:` play(song_name)
"""
if msg.guild.id in self.player:
if msg.voice_client.is_playing() is True: # NOTE: SONG CURRENTLY PLAYING
return await self.queue(msg, song)
if self.player[msg.guild.id]['queue']:
return await self.queue(msg, song)
if msg.voice_client.is_playing() is False and not self.player[msg.guild.id]['queue']:
return await self.start_song(msg, song)
else:
# IMPORTANT: THE ONLY PLACE WHERE NEW `self.player[msg.guild.id]={}` IS CREATED
self.player[msg.guild.id] = {
'player': None,
'queue': [],
'author': msg,
'name': None,
"reset": False,
'repeat': False,
'volume': 0.5
}
return await self.start_song(msg, song)
@play.before_invoke
async def before_play(self, msg):
"""
Check voice_client
- User voice = None:
please join a voice channel
- bot voice == None:
joins the user's voice channel
- user and bot voice NOT SAME:
- music NOT Playing AND queue EMPTY
join user's voice channel
- items in queue:
please join the same voice channel as the bot to add song to queue
"""
if msg.author.voice is None:
return await msg.send('**Please join a voice channel to play music**'.title())
if msg.voice_client is None:
return await msg.author.voice.channel.connect()
if msg.voice_client.channel != msg.author.voice.channel:
# NOTE: Check player and queue
if msg.voice_client.is_playing() is False and not self.player[msg.guild.id]['queue']:
return await msg.voice_client.move_to(msg.author.voice.channel)
# NOTE: move bot to user's voice channel if queue does not exist
if self.player[msg.guild.id]['queue']:
# NOTE: user must join same voice channel if queue exist
return await msg.send("Please join the same voice channel as the bot to add song to queue")
@commands.has_permissions(manage_channels=True)
@command()
async def repeat(self, msg):
"""
Repeat the currently playing or turn off by using the command again
`Ex:` .repeat
`Command:` repeat()
"""
if msg.guild.id in self.player:
if msg.voice_client.is_playing() is True:
if self.player[msg.guild.id]['repeat'] is True:
self.player[msg.guild.id]['repeat'] = False
return await msg.message.add_reaction(emoji='✅')
self.player[msg.guild.id]['repeat'] = True
return await msg.message.add_reaction(emoji='✅')
return await msg.send("No audio currently playing")
return await msg.send("Bot not in voice channel or playing music")
@commands.has_permissions(manage_channels=True)
@command(aliases=['restart-loop'])
async def reset(self, msg):
"""
Restart the currently playing song from the begining
`Ex:` s.reset
`Command:` reset()
"""
if msg.voice_client is None:
return await msg.send(f"**{msg.author.display_name}, there is no audio currently playing from the bot.**")
if msg.author.voice is None or msg.author.voice.channel != msg.voice_client.channel:
return await msg.send(f"**{msg.author.display_name}, you must be in the same voice channel as the bot.**")
if self.player[msg.guild.id]['queue'] and msg.voice_client.is_playing() is False:
return await msg.send("**No audio currently playing or songs in queue**".title(), delete_after=25)
self.player[msg.guild.id]['reset'] = True
msg.voice_client.stop()
@commands.has_permissions(manage_channels=True)
@command()
async def skip(self, msg):
"""
Skip the current playing song
`Ex:` s.skip
`Command:` skip()
"""
if msg.voice_client is None:
return await msg.send("**No music currently playing**".title(), delete_after=60)
if msg.author.voice is None or msg.author.voice.channel != msg.voice_client.channel:
return await msg.send("Please join the same voice channel as the bot")
if not self.player[msg.guild.id]['queue'] and msg.voice_client.is_playing() is False:
return await msg.send("**No songs in queue to skip**".title(), delete_after=60)
self.player[msg.guild.id]['repeat'] = False
msg.voice_client.stop()
return await msg.message.add_reaction(emoji='✅')
@commands.has_permissions(manage_channels=True)
@command()
async def stop(self, msg):
"""
Stop the current playing songs and clear the queue
`Ex:` s.stop
`Command:` stop()
"""
if msg.voice_client is None:
return await msg.send("Bot is not connect to a voice channel")
if msg.author.voice is None:
return await msg.send("You must be in the same voice channel as the bot")
if msg.author.voice is not None and msg.voice_client is not None:
if msg.voice_client.is_playing() is True or self.player[msg.guild.id]['queue']:
self.player[msg.guild.id]['queue'].clear()
self.player[msg.guild.id]['repeat'] = False
msg.voice_client.stop()
return await msg.message.add_reaction(emoji='✅')
return await msg.send(f"**{msg.author.display_name}, there is no audio currently playing or songs in queue**")
@commands.has_permissions(manage_channels=True)
@command(aliases=['get-out', 'disconnect', 'leave-voice'])
async def leave(self, msg):
"""
Disconnect the bot from the voice channel
`Ex:` s.leave
`Command:` leave()
"""
if msg.author.voice is not None and msg.voice_client is not None:
if msg.voice_client.is_playing() is True or self.player[msg.guild.id]['queue']:
self.player[msg.guild.id]['queue'].clear()
msg.voice_client.stop()
return await msg.voice_client.disconnect(), await msg.message.add_reaction(emoji='✅')
return await msg.voice_client.disconnect(), await msg.message.add_reaction(emoji='✅')
if msg.author.voice is None:
return await msg.send("You must be in the same voice channel as bot to disconnect it via command")
@commands.has_permissions(manage_channels=True)
@command()
async def pause(self, msg):
"""
Pause the currently playing audio
`Ex:` s.pause
`Command:` pause()
"""
if msg.author.voice is not None and msg.voice_client is not None:
if msg.voice_client.is_paused() is True:
return await msg.send("Song is already paused")
if msg.voice_client.is_paused() is False:
msg.voice_client.pause()
await msg.message.add_reaction(emoji='✅')
@commands.has_permissions(manage_channels=True)
@command()
async def resume(self, msg):
"""
Resume the currently paused audio
`Ex:` s.resume
`Command:` resume()
"""
if msg.author.voice is not None and msg.voice_client is not None:
if msg.voice_client.is_paused() is False:
return await msg.send("Song is already playing")
if msg.voice_client.is_paused() is True:
msg.voice_client.resume()
return await msg.message.add_reaction(emoji='✅')
@command(name='queue', aliases=['song-list', 'q', 'current-songs'])
async def _queue(self, msg):
"""
Show the current songs in queue
`Ex:` s.queue
`Command:` queue()
"""
if msg.voice_client is not None:
if msg.guild.id in self.player:
if self.player[msg.guild.id]['queue']:
emb = discord.Embed(
colour=self.random_color, title='queue')
emb.set_footer(
text=f'Command used by {msg.author.name}', icon_url=msg.author.avatar_url)
for i in self.player[msg.guild.id]['queue']:
emb.add_field(
name=f"**{i['author'].author.name}**", value=i['title'], inline=False)
return await msg.send(embed=emb, delete_after=120)
return await msg.send("No songs in queue")
@command(name='song-info', aliases=['song?', 'nowplaying', 'current-song'])
async def song_info(self, msg):
"""
Show information about the current playing song
`Ex:` s.song-info
`Command:` song-into()
"""
if msg.voice_client is not None and msg.voice_client.is_playing() is True:
emb = discord.Embed(colour=self.random_color, title='Currently Playing',
description=self.player[msg.guild.id]['player'].title)
emb.set_footer(
text=f"{self.player[msg.guild.id]['author'].author.name}", icon_url=msg.author.avatar_url)
emb.set_thumbnail(
url=self.player[msg.guild.id]['player'].thumbnail)
return await msg.send(embed=emb, delete_after=120)
return await msg.send(f"**No songs currently playing**".title(), delete_after=30)
@command(aliases=['move-bot', 'move-b', 'mb', 'mbot'])
async def join(self, msg, *, channel: discord.VoiceChannel = None):
"""
Make bot join a voice channel you are in if no channel is mentioned
`Ex:` .join (If voice channel name is entered, it'll join that one)
`Command:` join(channel:optional)
"""
if msg.voice_client is not None:
return await msg.send(f"Bot is already in a voice channel\nDid you mean to use {msg.prefix}moveTo")
if msg.voice_client is None:
if channel is None:
return await msg.author.voice.channel.connect(), await msg.message.add_reaction(emoji='✅')
return await channel.connect(), await msg.message.add_reaction(emoji='✅')
else:
if msg.voice_client.is_playing() is False and not self.player[msg.guild.id]['queue']:
return await msg.author.voice.channel.connect(), await msg.message.add_reaction(emoji='✅')
@join.before_invoke
async def before_join(self, msg):
if msg.author.voice is None:
return await msg.send("You are not in a voice channel")
@join.error
async def join_error(self, msg, error):
if isinstance(error, commands.BadArgument):
return msg.send(error)
if error.args[0] == 'Command raised an exception: Exception: playing':
return await msg.send("**Please join the same voice channel as the bot to add song to queue**".title())
@commands.has_permissions(manage_channels=True)
@command(aliases=['vol'])
async def volume(self, msg, vol: int):
"""
Change the volume of the bot
`Ex:` .vol 100 (200 is the max)
`Permission:` manage_channels
`Command:` volume(amount:integer)
"""
if vol > 200:
vol = 200
vol = vol/100
if msg.author.voice is not None:
if msg.voice_client is not None:
if msg.voice_client.channel == msg.author.voice.channel and msg.voice_client.is_playing() is True:
msg.voice_client.source.volume = vol
self.player[msg.guild.id]['volume'] = vol
# if (msg.guild.id) in self.music:
# self.music[str(msg.guild.id)]['vol']=vol
return await msg.message.add_reaction(emoji='✅')
return await msg.send("**Please join the same voice channel as the bot to use the command**".title(), delete_after=30)
@commands.command(brief='Download songs', description='[prefix]download <video url or title> Downloads the song')
async def download(self, ctx, *, song):
"""
Downloads the audio from given URL source and sends the audio source back to user to download from URL, the file will be removed from storage once sent.
`Ex`: .download I'll Show you K/DA
`Command`: download(url:required)
`NOTE`: file size can't exceed 8MB, otherwise it will fail to upload and cause error
"""
try:
with youtube_dl.YoutubeDL(ytdl_download_format_options) as ydl:
if "https://www.youtube.com/" in song:
download = ydl.extract_info(song, True)
else:
infosearched = ydl.extract_info(
"ytsearch:"+song, False)
download = ydl.extract_info(
infosearched['entries'][0]['webpage_url'], True)
filename = ydl.prepare_filename(download)
embed = discord.Embed(
title="Your download is ready", description="Please wait a moment while the file is beeing uploaded")
await ctx.send(embed=embed, delete_after=30)
await ctx.send(file=discord.File(filename))
os.remove(filename)
except (youtube_dl.utils.ExtractorError, youtube_dl.utils.DownloadError):
embed = discord.Embed(title="Song couldn't be downloaded", description=("Song:"+song))
await ctx.send(embed=embed)
@volume.error
async def volume_error(self, msg,error):
if isinstance(error, commands.MissingPermissions):
return await msg.send("Manage channels or admin perms required to change volume", delete_after=30)
def setup(bot):
bot.add_cog(MusicPlayer(bot))Editor is loading...