mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
assumes server-specific settings are stored uhhhhh shoot, lemme just give an example.
{
"MAX_CACHE": 50,
"SERVER_SFX_ON": {
"server1_id_here":True,
"server2_id_here":False
}
}
996 lines
42 KiB
Python
996 lines
42 KiB
Python
import discord
|
|
from discord.ext import commands
|
|
import asyncio
|
|
import threading
|
|
import os
|
|
from random import choice as rndchoice
|
|
from random import shuffle
|
|
from .utils.dataIO import fileIO
|
|
from .utils import checks
|
|
from __main__ import send_cmd_help
|
|
from __main__ import settings as bot_settings
|
|
import glob
|
|
import re
|
|
import aiohttp
|
|
import json
|
|
import time
|
|
|
|
try:
|
|
import youtube_dl
|
|
except:
|
|
youtube_dl = None
|
|
|
|
try:
|
|
if not discord.opus.is_loaded():
|
|
discord.opus.load_opus('libopus-0.dll')
|
|
except OSError: # Incorrect bitness
|
|
opus = False
|
|
except: # Missing opus
|
|
opus = None
|
|
else:
|
|
opus = True
|
|
|
|
youtube_dl_options = {
|
|
'format': 'bestaudio/best',
|
|
'extractaudio': True,
|
|
'audioformat': "mp3",
|
|
'outtmpl': '%(id)s',
|
|
'noplaylist': True,
|
|
'nocheckcertificate': True,
|
|
'ignoreerrors': True,
|
|
'quiet': True,
|
|
'no_warnings': True,
|
|
'outtmpl': "data/audio/cache/%(id)s",
|
|
'default_search' : 'auto'}
|
|
|
|
class Audio:
|
|
"""Music streaming."""
|
|
|
|
def __init__(self, bot):
|
|
self.bot = bot
|
|
self.music_player = EmptyPlayer()
|
|
self.sfx_player = EmptyPlayer()
|
|
self.settings = fileIO("data/audio/settings.json", "load")
|
|
self.queue_mode = False
|
|
self.queue = []
|
|
self.playlist = []
|
|
self.current = -1 #current track index in self.playlist
|
|
self.downloader = {"DONE" : False, "TITLE" : False, "ID" : False, "URL" : False, "DURATION" : False, "DOWNLOADING" : False}
|
|
self.skip_votes = []
|
|
self.cleanup_timer = int(time.perf_counter())
|
|
self.past_titles = [] # This is to prevent the audio module from setting the status to None if a status other than a track's title gets set
|
|
|
|
self.sing = ["https://www.youtube.com/watch?v=zGTkAVsrfg8", "https://www.youtube.com/watch?v=cGMWL8cOeAU",
|
|
"https://www.youtube.com/watch?v=vFrjMq4aL-g", "https://www.youtube.com/watch?v=WROI5WYBU_A",
|
|
"https://www.youtube.com/watch?v=41tIUr_ex3g", "https://www.youtube.com/watch?v=f9O2Rjn1azc"]
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
async def play(self, ctx, *link : str):
|
|
"""Plays videos (links/search terms)
|
|
"""
|
|
if self.downloader["DOWNLOADING"]:
|
|
await self.bot.say("I'm already downloading a track.")
|
|
return
|
|
msg = ctx.message
|
|
if await self.check_voice(msg.author, msg):
|
|
if link != ():
|
|
link = " ".join(link)
|
|
if "http" not in link and "www." not in link:
|
|
link = "[SEARCH:]" + link
|
|
else:
|
|
if not self.is_playlist_valid([link]):
|
|
await self.bot.say("Invalid link.")
|
|
return
|
|
if await self.is_alone_or_admin(msg):
|
|
self.queue = []
|
|
self.current = -1
|
|
self.playlist = []
|
|
self.queue.append(link)
|
|
self.music_player.paused = False
|
|
if not self.sfx_player.is_done(): self.sfx_player.stop()
|
|
if not self.music_player.is_done(): self.music_player.stop()
|
|
await self.bot.say("Playing requested link...")
|
|
else:
|
|
self.playlist = []
|
|
self.current = -1
|
|
if not self.queue: await self.bot.say("The link has been put into queue.")
|
|
self.queue.append(link)
|
|
else:
|
|
await self.bot.say("You need to add a link or search terms.")
|
|
|
|
@commands.command(aliases=["title"])
|
|
async def song(self):
|
|
"""Shows song title
|
|
"""
|
|
if self.downloader["TITLE"] and "localtracks" not in self.downloader["TITLE"]:
|
|
url = ""
|
|
if self.downloader["URL"]: url = 'Link : "' + self.downloader["URL"] + '"'
|
|
await self.bot.say(self.downloader["TITLE"] + "\n" + url)
|
|
else:
|
|
await self.bot.say("No title available.")
|
|
|
|
@commands.command(name="playlist", pass_context=True, no_pm=True)
|
|
async def _playlist(self, ctx, name : str): #some checks here
|
|
"""Plays saved playlist
|
|
"""
|
|
await self.start_playlist(ctx, name, random=False)
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
async def mix(self, ctx, name : str): #some checks here
|
|
"""Plays saved playlist (shuffled)
|
|
"""
|
|
await self.start_playlist(ctx, name, random=True)
|
|
|
|
async def start_playlist(self, ctx, name, random=None):
|
|
if self.downloader["DOWNLOADING"]:
|
|
await self.bot.say("I'm already downloading a track.")
|
|
return
|
|
msg = ctx.message
|
|
name += ".txt"
|
|
if await self.check_voice(msg.author, msg):
|
|
if os.path.isfile("data/audio/playlists/" + name):
|
|
self.queue = []
|
|
self.current = -1
|
|
self.playlist = fileIO("data/audio/playlists/" + name, "load")["playlist"]
|
|
if random: shuffle(self.playlist)
|
|
self.music_player.paused = False
|
|
if self.music_player.is_playing(): self.music_player.stop()
|
|
else:
|
|
await self.bot.say("There's no playlist with that name.")
|
|
|
|
@commands.command(pass_context=True, aliases=["next"], no_pm=True)
|
|
async def skip(self, ctx):
|
|
"""Skips song
|
|
"""
|
|
msg = ctx.message
|
|
if self.music_player.is_playing():
|
|
if await self.is_alone_or_admin(msg):
|
|
self.music_player.paused = False
|
|
self.music_player.stop()
|
|
else:
|
|
await self.vote_skip(msg)
|
|
elif self.sfx_player.is_playing():
|
|
self.sfx_player.stop()
|
|
|
|
async def vote_skip(self, msg):
|
|
v_channel = msg.server.me.voice_channel
|
|
if msg.author.voice_channel.id == v_channel.id:
|
|
if msg.author.id in self.skip_votes:
|
|
await self.bot.say("You already voted.")
|
|
return
|
|
self.skip_votes.append(msg.author.id)
|
|
if msg.server.me.id not in self.skip_votes: self.skip_votes.append(msg.server.me.id)
|
|
current_users = []
|
|
for m in v_channel.voice_members:
|
|
current_users.append(m.id)
|
|
|
|
clean_skip_votes = [] #Removes votes of people no longer in the channel
|
|
for m_id in self.skip_votes:
|
|
if m_id in current_users:
|
|
clean_skip_votes.append(m_id)
|
|
self.skip_votes = clean_skip_votes
|
|
|
|
votes_needed = int((len(current_users)-1) / 2)
|
|
|
|
if len(self.skip_votes)-1 >= votes_needed:
|
|
self.music_player.paused = False
|
|
if self.music_player.is_playing(): self.music_player.stop()
|
|
self.skip_votes = []
|
|
return
|
|
await self.bot.say("You voted to skip. Votes: [{0}/{1}]".format(str(len(self.skip_votes)-1), str(votes_needed)))
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
async def local(self, ctx, name : str):
|
|
"""Plays a local playlist
|
|
|
|
For bot's owner:
|
|
https://github.com/Twentysix26/Red-DiscordBot/wiki/Audio-module"""
|
|
help_link = "https://github.com/Twentysix26/Red-DiscordBot/wiki/Audio-module"
|
|
if self.downloader["DOWNLOADING"]:
|
|
await self.bot.say("I'm already downloading a track.")
|
|
return
|
|
msg = ctx.message
|
|
localplaylists = self.get_local_playlists()
|
|
if localplaylists and ("data/audio/localtracks/" not in name and "\\" not in name):
|
|
if name in localplaylists:
|
|
files = []
|
|
if glob.glob("data/audio/localtracks/" + name + "/*.mp3"):
|
|
files.extend(glob.glob("data/audio/localtracks/" + name + "/*.mp3"))
|
|
if glob.glob("data/audio/localtracks/" + name + "/*.flac"):
|
|
files.extend(glob.glob("data/audio/localtracks/" + name + "/*.flac"))
|
|
if await self.is_alone_or_admin(msg):
|
|
if await self.check_voice(msg.author, ctx.message):
|
|
self.queue = []
|
|
self.current = -1
|
|
self.playlist = files
|
|
self.music_player.paused = False
|
|
if self.music_player.is_playing(): self.music_player.stop()
|
|
else:
|
|
await self.bot.say("I'm in queue mode. Controls are disabled if you're in a room with multiple people.")
|
|
else:
|
|
await self.bot.say("There is no local playlist with that name.")
|
|
else:
|
|
await self.bot.say("There are no valid playlists in the localtracks folder.\nIf you're the owner, see {}".format(help_link))
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
async def sfx(self, ctx, *, name : str):
|
|
"""Plays a local sound file
|
|
|
|
For bot's owner:
|
|
audio files must be placed in the data/audio/sfx folder.
|
|
Supported files are mp3, flac, wav"""
|
|
#sound effects default enabled
|
|
server = ctx.message.server
|
|
if self.settings["SERVER_SFX_ON"].get(server.id,None) is None:
|
|
self.settings["SERVER_SFX_ON"][server.id] = True
|
|
if not self.settings["SERVER_SFX_ON"][server.id]:
|
|
await self.bot.say("Sound effects are not enabled on this server")
|
|
return
|
|
|
|
msg = ctx.message
|
|
localsfx = self.get_local_sfx()
|
|
if localsfx and ("data/audio/sfx/" not in name and "\\" not in name):
|
|
if name in localsfx.keys():
|
|
file = "data/audio/sfx/{}{}".format(name,localsfx[name])
|
|
|
|
if await self.check_voice(msg.author, ctx.message):
|
|
try:
|
|
if self.music_player.is_playing():
|
|
self.music_player.paused = True
|
|
self.music_player.pause()
|
|
|
|
if self.sfx_player.is_playing():
|
|
self.sfx_player.stop()
|
|
|
|
self.sfx_player = self.bot.voice.create_ffmpeg_player(file, options='''-filter:a "volume={}"'''.format(self.settings["VOLUME"]))
|
|
self.sfx_player.start()
|
|
while self.sfx_player.is_playing():
|
|
await asyncio.sleep(.5)
|
|
|
|
if not self.music_player.is_playing():
|
|
self.music_player.paused = False
|
|
self.music_player.resume()
|
|
except AttributeError:
|
|
#music_player not used yet. Still an EmptyPlayer.
|
|
#better to ask for forgiveness?
|
|
pass
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
else:
|
|
await self.bot.say("There is no sound effect with that name.")
|
|
else:
|
|
await self.bot.say("There are no valid sound effects in the `data/audio/sfx` folder.")
|
|
|
|
def get_local_sfx(self):
|
|
filenames = {}
|
|
files = os.listdir("data/audio/sfx/")
|
|
#only reason why these are the only supported ext is cause I haven't tried any others
|
|
supported = [".mp3",".flac",".wav"]
|
|
for f in files:
|
|
item = os.path.splitext(f)
|
|
if item[1] in supported:
|
|
filenames[item[0]] = item[1]
|
|
if filenames != {}:
|
|
return filenames
|
|
else:
|
|
return False
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
async def loop(self, ctx):
|
|
"""Loops single song
|
|
"""
|
|
msg = ctx.message
|
|
if self.music_player.is_playing():
|
|
if await self.is_alone_or_admin(msg):
|
|
self.current = -1
|
|
if self.downloader["URL"]:
|
|
self.playlist = [self.downloader["URL"]]
|
|
else: # local
|
|
self.playlist = [self.downloader["ID"]]
|
|
await self.bot.say("I will play this song on repeat.")
|
|
else:
|
|
await self.bot.say("I'm in queue mode. Controls are disabled if you're in a room with multiple people.")
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
async def shuffle(self, ctx):
|
|
"""Shuffle playlist
|
|
"""
|
|
msg = ctx.message
|
|
if self.music_player.is_playing():
|
|
if await self.is_alone_or_admin(msg):
|
|
if self.playlist:
|
|
shuffle(self.playlist)
|
|
await self.bot.say("The order of this playlist has been mixed")
|
|
else:
|
|
await self.bot.say("I'm in queue mode. Controls are disabled if you're in a room with multiple people.")
|
|
|
|
@commands.command(pass_context=True, aliases=["previous"], no_pm=True) #TODO, PLAYLISTS
|
|
async def prev(self, ctx):
|
|
"""Previous song
|
|
"""
|
|
msg = ctx.message
|
|
if self.music_player.is_playing() and self.playlist:
|
|
if await self.is_alone_or_admin(msg):
|
|
self.current -= 2
|
|
if self.current == -1:
|
|
self.current = len(self.playlist) -3
|
|
elif self.current == -2:
|
|
self.current = len(self.playlist) -2
|
|
self.music_player.paused = False
|
|
self.music_player.stop()
|
|
|
|
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
async def stop(self, ctx):
|
|
"""Stops audio activity
|
|
"""
|
|
msg = ctx.message
|
|
if self.music_player.is_playing() or self.sfx_player.is_playing():
|
|
if await self.is_alone_or_admin(msg):
|
|
await self.close_audio()
|
|
else:
|
|
await self.bot.say("You can't stop music when there are other people in the channel! Vote to skip instead.")
|
|
else:
|
|
await self.close_audio()
|
|
|
|
async def close_audio(self):
|
|
self.queue = []
|
|
self.playlist = []
|
|
self.current = -1
|
|
if not self.music_player.is_done(): self.music_player.stop()
|
|
if not self.sfx_player.is_done(): self.sfx_player.stop()
|
|
await asyncio.sleep(1)
|
|
if self.bot.voice: await self.bot.voice.disconnect()
|
|
|
|
@commands.command(name="queue", pass_context=True, no_pm=True) #check that author is in the same channel as the bot
|
|
async def _queue(self, ctx, *link : str):
|
|
"""Add links or search terms to queue
|
|
|
|
Shows queue list if no links are provided.
|
|
"""
|
|
if link == ():
|
|
queue_list = await self.queue_titles()
|
|
await self.bot.say("Videos in queue: \n" + queue_list + "\n\nType queue <link> to add a link or search terms to the queue.")
|
|
elif await self.check_voice(ctx.message.author, ctx.message):
|
|
if not self.playlist:
|
|
link = " ".join(link)
|
|
if "http" not in link or "." not in link:
|
|
link = "[SEARCH:]" + link
|
|
else:
|
|
if not self.is_playlist_valid([link]):
|
|
await self.bot.say("Invalid link.")
|
|
return
|
|
self.queue.append(link)
|
|
msg = ctx.message
|
|
result = await self.get_song_metadata(link)
|
|
try: # In case of invalid SOUNDCLOUD ID
|
|
if result["title"] != []:
|
|
await self.bot.say("{} has been put into the queue by {}.".format(result["title"], msg.author))
|
|
else:
|
|
await self.bot.say("The song has been put into the queue by {}, however it may error.".format(msg.author))
|
|
except:
|
|
await self.bot.say("A song has been put into the queue by {}.".format(msg.author))
|
|
|
|
else:
|
|
await self.bot.say("I'm already playing a playlist.")
|
|
else:
|
|
await self.bot.say("That link is now allowed.")
|
|
|
|
async def is_alone_or_admin(self, message): #Direct control. fix everything
|
|
author = message.author
|
|
server = message.server
|
|
if not self.settings["QUEUE_MODE"]:
|
|
return True
|
|
elif author.id == bot_settings.owner:
|
|
return True
|
|
elif discord.utils.get(author.roles, name=bot_settings.get_server_admin(server)) is not None:
|
|
return True
|
|
elif discord.utils.get(author.roles, name=bot_settings.get_server_mod(server)) is not None:
|
|
return True
|
|
elif len(author.voice_channel.voice_members) in (1, 2):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@commands.command(name="sing", pass_context=True, no_pm=True)
|
|
async def _sing(self, ctx):
|
|
"""Makes Red sing"""
|
|
if self.downloader["DOWNLOADING"]:
|
|
await self.bot.say("I'm already downloading a track.")
|
|
return
|
|
msg = ctx.message
|
|
if await self.check_voice(msg.author, msg):
|
|
if not self.music_player.is_playing():
|
|
self.queue = []
|
|
await self.play_video(rndchoice(self.sing))
|
|
else:
|
|
if await self.is_alone_or_admin(msg):
|
|
self.queue = []
|
|
await self.play_video(rndchoice(self.sing))
|
|
else:
|
|
await self.bot.say("I'm already playing music for someone else at the moment.")
|
|
|
|
@commands.command()
|
|
async def pause(self):
|
|
"""Pauses the current song"""
|
|
if self.music_player.is_playing():
|
|
self.music_player.paused = True
|
|
self.music_player.pause()
|
|
await self.bot.say("Song paused.")
|
|
|
|
@commands.command()
|
|
async def resume(self):
|
|
"""Resumes paused song."""
|
|
if self.sfx_player.is_playing():
|
|
self.sfx_player.stop()
|
|
elif not self.music_player.is_playing():
|
|
self.music_player.paused = False
|
|
self.music_player.resume()
|
|
await self.bot.say("Resuming song.")
|
|
|
|
@commands.group(name="list", pass_context=True)
|
|
async def _list(self, ctx):
|
|
"""Lists playlists"""
|
|
if ctx.invoked_subcommand is None:
|
|
await send_cmd_help(ctx)
|
|
|
|
@_list.command(name="playlist", pass_context=True)
|
|
async def list_playlist(self, ctx):
|
|
msg = "Available playlists: \n\n```"
|
|
files = os.listdir("data/audio/playlists/")
|
|
if files:
|
|
for i, f in enumerate(files):
|
|
if f.endswith(".txt"):
|
|
if i % 4 == 0 and i != 0:
|
|
msg = msg + f.replace(".txt", "") + "\n"
|
|
else:
|
|
msg = msg + f.replace(".txt", "") + "\t"
|
|
msg += "```"
|
|
await self.bot.send_message(ctx.message.author, msg)
|
|
else:
|
|
await self.bot.say("There are no playlists.")
|
|
|
|
@_list.command(name="local", pass_context=True)
|
|
async def list_local(self, ctx):
|
|
msg = "Available local playlists: \n\n```"
|
|
dirs = self.get_local_playlists()
|
|
if dirs:
|
|
for i, d in enumerate(dirs):
|
|
if i % 4 == 0 and i != 0:
|
|
msg = msg + d + "\n"
|
|
else:
|
|
msg = msg + d + "\t"
|
|
msg += "```"
|
|
await self.bot.send_message(ctx.message.author, msg)
|
|
else:
|
|
await self.bot.say("There are no local playlists.")
|
|
|
|
@_list.command(name="sfx", pass_context=True)
|
|
async def list_sfx(self, ctx):
|
|
msgs = ["Available local sound effects: \n\n```\n"]
|
|
files = self.get_local_sfx()
|
|
m = 0
|
|
maxm = 1980
|
|
if files:
|
|
for i, d in enumerate(files.keys()):
|
|
if len(d) < maxm: #how did you get a filename this large?
|
|
if len(msgs[m]) + len(d) > maxm:
|
|
msgs[m] += "```"
|
|
m += 1
|
|
msgs.append("```\n")
|
|
if i % 4 == 0 and i != 0:
|
|
msgs[m] += d + "\n"
|
|
else:
|
|
msgs[m] += d + "\t"
|
|
msgs[m] += "```"
|
|
for msg in msgs:
|
|
await self.bot.send_message(ctx.message.author, msg)
|
|
await asyncio.sleep(1)
|
|
else:
|
|
await self.bot.say("There are no local sound effects.")
|
|
|
|
@_list.command(name="queue", pass_context=True)
|
|
async def list_queue(self, ctx):
|
|
queue_list = await self.queue_titles()
|
|
await self.bot.say("Videos in queue: \n" + queue_list)
|
|
|
|
async def queue_titles(self):
|
|
song_names = []
|
|
song_names.append(self.downloader["TITLE"])
|
|
if len(self.queue) > 0:
|
|
for song_url in self.queue:
|
|
try:
|
|
result = await self.get_song_metadata(song_url)
|
|
if result["title"] != []:
|
|
song_names.append(result["title"])
|
|
else:
|
|
song_names.append("Could not get song title")
|
|
except:
|
|
song_names.append("Could not get song title")
|
|
song_list = "\n".join(["{}: {}".format(str(i+1), s) for i, s in enumerate(song_names)])
|
|
elif self.music_player.is_playing():
|
|
song_list = "1: {}".format(song_names[0])
|
|
else:
|
|
song_list = "None"
|
|
return song_list
|
|
|
|
@commands.group(pass_context=True)
|
|
@checks.mod_or_permissions(manage_roles=True)
|
|
async def audioset(self, ctx):
|
|
"""Changes audio module settings"""
|
|
if ctx.invoked_subcommand is None:
|
|
server = ctx.message.server
|
|
await send_cmd_help(ctx)
|
|
msg = "```"
|
|
for k, v in self.settings.items():
|
|
if type(v) is dict and server.id in v:
|
|
msg += str(k) + ": " + str(v[server.id]) + "\n"
|
|
else:
|
|
msg += str(k) + ": " + str(v) + "\n"
|
|
msg += "```"
|
|
await self.bot.say(msg)
|
|
|
|
@audioset.command(name="queue")
|
|
async def queueset(self):
|
|
"""Enables/disables forced queue"""
|
|
self.settings["QUEUE_MODE"] = not self.settings["QUEUE_MODE"]
|
|
if self.settings["QUEUE_MODE"]:
|
|
await self.bot.say("Queue mode is now on.")
|
|
else:
|
|
await self.bot.say("Queue mode is now off.")
|
|
fileIO("data/audio/settings.json", "save", self.settings)
|
|
|
|
@audioset.command(name="status")
|
|
async def songstatus(self):
|
|
"""Enables/disables songs' titles as status"""
|
|
self.settings["TITLE_STATUS"] = not self.settings["TITLE_STATUS"]
|
|
if self.settings["TITLE_STATUS"]:
|
|
await self.bot.say("Songs' titles will show up as status.")
|
|
else:
|
|
await self.bot.say("Songs' titles will no longer show up as status.")
|
|
fileIO("data/audio/settings.json", "save", self.settings)
|
|
|
|
@audioset.command()
|
|
async def maxlength(self, length : int):
|
|
"""Maximum track length (seconds) for requested links"""
|
|
self.settings["MAX_LENGTH"] = length
|
|
await self.bot.say("Maximum length is now " + str(length) + " seconds.")
|
|
fileIO("data/audio/settings.json", "save", self.settings)
|
|
|
|
@audioset.command()
|
|
async def volume(self, level : float):
|
|
"""Sets the volume (0-1)"""
|
|
if level >= 0 and level <= 1:
|
|
self.settings["VOLUME"] = level
|
|
await self.bot.say("Volume is now set at " + str(level) + ". It will take effect after the current track.")
|
|
fileIO("data/audio/settings.json", "save", self.settings)
|
|
else:
|
|
await self.bot.say("Volume must be between 0 and 1. Example: 0.40")
|
|
|
|
@audioset.command(name="sfx", pass_context=True, no_pm=True)
|
|
async def _sfx(self, ctx):
|
|
"""Enables/Disables sound effects usage in the server"""
|
|
#default on.
|
|
server = ctx.message.server
|
|
if self.settings["SERVER_SFX_ON"].get(server.id,None) is None:
|
|
self.settings["SERVER_SFX_ON"][server.id] = True
|
|
else:
|
|
self.settings["SERVER_SFX_ON"][server.id] = not self.settings["SERVER_SFX_ON"][server.id]
|
|
#for a toggle, settings should save here in case bot fails to send message
|
|
fileIO("data/audio/settings.json", "save", self.settings)
|
|
if self.settings["SERVER_SFX_ON"][server.id]:
|
|
await self.bot.say("Sound effects are now enabled on this server.")
|
|
else:
|
|
await self.bot.say("Sound effects are now disabled on this server.")
|
|
|
|
@audioset.command()
|
|
@checks.is_owner()
|
|
async def maxcache(self, size : int):
|
|
"""Sets the maximum audio cache size (megabytes)
|
|
|
|
If set to 0, auto cleanup is disabled."""
|
|
self.settings["MAX_CACHE"] = size
|
|
fileIO("data/audio/settings.json", "save", self.settings)
|
|
if not size:
|
|
await self.bot.say("Auto audio cache cleanup disabled.")
|
|
else:
|
|
await self.bot.say("Maximum audio cache size has been set to " + str(size) + "MB.")
|
|
|
|
@audioset.command()
|
|
@checks.is_owner()
|
|
async def soundcloud(self, ID : str=None):
|
|
"""Sets the SoundCloud Client ID
|
|
"""
|
|
self.settings["SOUNDCLOUD_CLIENT_ID"] = ID
|
|
fileIO("data/audio/settings.json", "save", self.settings)
|
|
if not ID:
|
|
await self.bot.say("SoundCloud API intergration has been disabled")
|
|
else:
|
|
await self.bot.say("SoundCloud Client ID has been set")
|
|
|
|
@commands.group(pass_context=True)
|
|
@checks.is_owner()
|
|
async def cache(self, ctx):
|
|
"""Audio cache management"""
|
|
if ctx.invoked_subcommand is None:
|
|
await self.bot.say("Current audio cache size: " + str(self.cache_size()) + "MB" )
|
|
|
|
@cache.command(name="empty")
|
|
async def cache_delete(self):
|
|
"""Empties audio cache"""
|
|
self.empty_cache()
|
|
await self.bot.say("Cache emptied.")
|
|
|
|
def empty_cache(self):
|
|
files = os.listdir("data/audio/cache")
|
|
for f in files:
|
|
try:
|
|
os.unlink("data/audio/cache/" + f)
|
|
except PermissionError: # In case it tries to delete the file that it's currently playing
|
|
pass
|
|
|
|
def cache_size(self):
|
|
total = [os.path.getsize("data/audio/cache/" + f) for f in os.listdir("data/audio/cache")]
|
|
size = 0
|
|
for f in total:
|
|
size += f
|
|
return int(size / (1024*1024.0))
|
|
|
|
async def play_video(self, link):
|
|
self.downloader = {"DONE" : False, "TITLE" : False, "ID" : False, "URL": False, "DURATION" : False, "DOWNLOADING" : False}
|
|
if "https://" in link or "http://" in link or "[SEARCH:]" in link:
|
|
path = "data/audio/cache/"
|
|
t = threading.Thread(target=self.get_video, args=(link,self,))
|
|
t.start()
|
|
else: #local
|
|
path = ""
|
|
self.downloader = {"DONE" : True, "TITLE" : link, "ID" : link, "URL": False, "DURATION" : False, "DOWNLOADING" : False}
|
|
while not self.downloader["DONE"]:
|
|
await asyncio.sleep(1)
|
|
if self.downloader["ID"]:
|
|
try:
|
|
# sfx_player should only ever get stuck as much as music_player does
|
|
# when it happens to music_player, the reponsibility is put on the user to !stop
|
|
# so we'll do the same with sfx_player. a timeout could be placed here though.
|
|
while self.sfx_player.is_playing():
|
|
await asyncio.sleep(.5)
|
|
if not self.music_player.is_done(): self.music_player.stop()
|
|
self.music_player = self.bot.voice.create_ffmpeg_player(path + self.downloader["ID"], options='''-filter:a "volume={}"'''.format(self.settings["VOLUME"]))
|
|
self.music_player.paused = False
|
|
self.music_player.start()
|
|
if path != "" and self.settings["TITLE_STATUS"]:
|
|
self.past_titles.append(self.downloader["TITLE"])
|
|
await self.bot.change_status(discord.Game(name=self.downloader["TITLE"]))
|
|
except discord.errors.ClientException:
|
|
print("Error: I can't play music without ffmpeg. Install it.")
|
|
self.downloader = {"DONE" : False, "TITLE" : False, "ID" : False, "URL": False, "DURATION" : False, "DOWNLOADING" : False}
|
|
self.queue = []
|
|
self.playlist = []
|
|
except Exception as e:
|
|
print(e)
|
|
else:
|
|
pass
|
|
|
|
|
|
async def check_voice(self, author, message):
|
|
if self.bot.is_voice_connected():
|
|
v_channel = self.bot.voice.channel
|
|
if author.voice_channel == v_channel:
|
|
return True
|
|
elif len(v_channel.voice_members) == 1:
|
|
if author.voice_channel:
|
|
if author.voice_channel.permissions_for(message.server.me).connect:
|
|
wait = await self.close_audio()
|
|
await self.bot.join_voice_channel(author.voice_channel)
|
|
return True
|
|
else:
|
|
await self.bot.say("I need permissions to join that voice channel.")
|
|
return False
|
|
else:
|
|
await self.bot.say("You need to be in a voice channel.")
|
|
return False
|
|
else:
|
|
if not self.playlist and not self.queue:
|
|
return True
|
|
else:
|
|
await self.bot.say("I'm already playing music for other people.")
|
|
return False
|
|
elif author.voice_channel:
|
|
if author.voice_channel.permissions_for(message.server.me).connect:
|
|
await self.bot.join_voice_channel(author.voice_channel)
|
|
return True
|
|
else:
|
|
await self.bot.say("I need permissions to join that voice channel.")
|
|
return False
|
|
else:
|
|
await self.bot.say("You need to be in a voice channel.")
|
|
return False
|
|
|
|
async def queue_manager(self):
|
|
while self == self.bot.get_cog("Audio"):
|
|
if not self.music_player.paused:
|
|
if self.queue and not self.music_player.is_playing():
|
|
new_link = self.queue[0]
|
|
self.queue.pop(0)
|
|
self.skip_votes = []
|
|
await self.play_video(new_link)
|
|
elif self.playlist and not self.music_player.is_playing():
|
|
if not self.current == len(self.playlist)-1:
|
|
self.current += 1
|
|
else:
|
|
self.current = 0
|
|
new_link = self.playlist[self.current]
|
|
self.skip_votes = []
|
|
await self.play_video(new_link)
|
|
await asyncio.sleep(1)
|
|
|
|
def get_video(self, url, audio):
|
|
try:
|
|
self.downloader["DOWNLOADING"] = True
|
|
yt = youtube_dl.YoutubeDL(youtube_dl_options)
|
|
if "[SEARCH:]" not in url:
|
|
v = yt.extract_info(url, download=False)
|
|
else:
|
|
url = url.replace("[SEARCH:]", "")
|
|
url = "https://youtube.com/watch?v=" + yt.extract_info(url, download=False)["entries"][0]["id"]
|
|
v = yt.extract_info(url, download=False)
|
|
if v["duration"] > self.settings["MAX_LENGTH"]: raise MaximumLength("Track exceeded maximum length. See help audioset maxlength")
|
|
if not os.path.isfile("data/audio/cache/" + v["id"]):
|
|
v = yt.extract_info(url, download=True)
|
|
audio.downloader = {"DONE" : True, "TITLE" : v["title"], "ID" : v["id"], "URL" : url, "DURATION" : v["duration"], "DOWNLOADING" : False} #Errors out here if invalid link
|
|
except Exception as e:
|
|
print(e) # TODO
|
|
audio.downloader = {"DONE" : True, "TITLE" : False, "ID" : False, "URL" : False, "DOWNLOADING" : False}
|
|
|
|
async def incoming_messages(self, msg):
|
|
if msg.author.id != self.bot.user.id:
|
|
|
|
if self.settings["MAX_CACHE"] != 0:
|
|
if abs(self.cleanup_timer - int(time.perf_counter())) >= 900: # checks cache's size every 15 minutes
|
|
self.cleanup_timer = int(time.perf_counter())
|
|
if self.cache_size() >= self.settings["MAX_CACHE"]:
|
|
self.empty_cache()
|
|
print("Cache emptied.")
|
|
|
|
if msg.channel.is_private and msg.attachments != []:
|
|
await self.transfer_playlist(msg)
|
|
if not msg.channel.is_private:
|
|
if not self.playlist and not self.queue and not self.music_player.is_playing() and str(msg.server.me.game) in self.past_titles:
|
|
self.past_titles = []
|
|
await self.bot.change_status(None)
|
|
|
|
def get_local_playlists(self):
|
|
dirs = []
|
|
files = os.listdir("data/audio/localtracks/")
|
|
for f in files:
|
|
if os.path.isdir("data/audio/localtracks/" + f) and " " not in f:
|
|
if glob.glob("data/audio/localtracks/" + f + "/*.mp3") != []:
|
|
dirs.append(f)
|
|
elif glob.glob("data/audio/localtracks/" + f + "/*.flac") != []:
|
|
dirs.append(f)
|
|
if dirs != []:
|
|
return dirs
|
|
else:
|
|
return False
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
async def addplaylist(self, ctx, name : str, link : str): #CHANGE COMMAND NAME
|
|
"""Adds tracks from youtube / soundcloud playlist link"""
|
|
if self.is_playlist_name_valid(name) and len(name) < 25:
|
|
if fileIO("playlists/" + name + ".txt", "check"):
|
|
await self.bot.say("`A playlist with that name already exists.`")
|
|
return False
|
|
if "youtube" in link.lower():
|
|
links = await self.parse_yt_playlist(link)
|
|
elif "soundcloud" in link.lower():
|
|
links = await self.parse_sc_playlist(link)
|
|
if links:
|
|
data = { "author" : ctx.message.author.id,
|
|
"playlist": links,
|
|
"link" : link}
|
|
fileIO("data/audio/playlists/" + name + ".txt", "save", data)
|
|
await self.bot.say("Playlist added. Name: {}, songs: {}".format(name, str(len(links))))
|
|
else:
|
|
await self.bot.say("Something went wrong. Either the link was incorrect or I was unable to retrieve the page.")
|
|
else:
|
|
await self.bot.say("Something is wrong with the playlist's link or its filename. Remember, the name must be with only numbers, letters and underscores.")
|
|
|
|
@commands.command(pass_context=True, no_pm=True)
|
|
async def delplaylist(self, ctx, name : str):
|
|
"""Deletes playlist
|
|
|
|
Limited to owner, admins and author of the playlist."""
|
|
file_path = "data/audio/playlists/" + name + ".txt"
|
|
author = ctx.message.author
|
|
if fileIO(file_path, "check"):
|
|
playlist_author_id = fileIO(file_path, "load")["author"]
|
|
check = await self.admin_or_owner(ctx.message)
|
|
if check or author.id == playlist_author_id:
|
|
os.remove(file_path)
|
|
await self.bot.say("Playlist {} has been removed.".format(name))
|
|
else:
|
|
await self.bot.say("Only owner, admins and the author of the playlist can delete it.")
|
|
else:
|
|
await self.bot.say("There's no playlist with that name.")
|
|
|
|
async def admin_or_owner(self, message):
|
|
if message.author.id == bot_settings.owner:
|
|
return True
|
|
elif discord.utils.get(message.author.roles, name=bot_settings.get_server_admin(message.server)) is not None:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
async def transfer_playlist(self, message):
|
|
msg = message.attachments[0]
|
|
if msg["filename"].endswith(".txt"):
|
|
if not fileIO("data/audio/playlists/" + msg["filename"], "check"): #returns false if file already exists
|
|
r = await aiohttp.get(msg["url"])
|
|
r = await r.text()
|
|
data = r.replace("\r", "")
|
|
data = data.split()
|
|
if self.is_playlist_valid(data) and self.is_playlist_name_valid(msg["filename"].replace(".txt", "")):
|
|
data = { "author" : message.author.id,
|
|
"playlist": data,
|
|
"link" : False}
|
|
fileIO("data/audio/playlists/" + msg["filename"], "save", data)
|
|
await self.bot.send_message(message.channel, "Playlist added. Name: {}".format(msg["filename"].replace(".txt", "")))
|
|
else:
|
|
await self.bot.send_message(message.channel, "Something is wrong with the playlist or its filename.") # Add formatting info
|
|
else:
|
|
await self.bot.send_message(message.channel, "A playlist with that name already exists. Change the filename and resubmit it.")
|
|
|
|
def is_playlist_valid(self, data):
|
|
data = [y for y in data if y != ""] # removes all empty elements
|
|
data = [y for y in data if y != "\n"]
|
|
pattern = "|".join(fileIO("data/audio/accepted_links.json", "load"))
|
|
for link in data:
|
|
rr = re.search(pattern, link, re.I | re.U)
|
|
if rr == None:
|
|
return False
|
|
return True
|
|
|
|
def is_playlist_link_valid(self, link):
|
|
pattern = "^https:\/\/www.youtube.com\/playlist\?list=(.[^:/]*)"
|
|
rr = re.search(pattern, link, re.I | re.U)
|
|
if not rr == None:
|
|
return rr.group(1)
|
|
else:
|
|
return False
|
|
|
|
def is_playlist_name_valid(self, name):
|
|
for l in name:
|
|
if l.isdigit() or l.isalpha() or l == "_":
|
|
pass
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
async def parse_yt_playlist(self, url):
|
|
try:
|
|
if not "www.youtube.com/playlist?list=" in url:
|
|
url = url.split("&")
|
|
url = "https://www.youtube.com/playlist?" + [x for x in url if "list=" in x][0]
|
|
playlist = []
|
|
yt = youtube_dl.YoutubeDL(youtube_dl_options)
|
|
for entry in yt.extract_info(url, download=False, process=False)["entries"]:
|
|
playlist.append("https://www.youtube.com/watch?v=" + entry["id"])
|
|
return playlist
|
|
except:
|
|
return False
|
|
|
|
async def parse_sc_playlist(self, link):
|
|
try:
|
|
playlist = []
|
|
yt = youtube_dl.YoutubeDL(youtube_dl_options)
|
|
for i in yt.extract_info(link, download=False, process=False)["entries"]:
|
|
playlist.append(i['url'][:4] + 's' + i['url'][4:])
|
|
return playlist
|
|
except:
|
|
return False
|
|
|
|
async def get_json(self, url):
|
|
"""
|
|
Returns the JSON from an URL.
|
|
Expects the url to be valid and return a JSON object.
|
|
"""
|
|
async with aiohttp.get(url) as r:
|
|
result = await r.json()
|
|
return result
|
|
|
|
async def get_song_metadata(self, song_url):
|
|
"""
|
|
Returns JSON object containing metadata about the song.
|
|
Expects song_url to be valid url and in acepted_list
|
|
"""
|
|
|
|
youtube_regex = (
|
|
r'(https?://)?(www\.)?'
|
|
'(youtube|youtu|youtube-nocookie)\.(com|be)/'
|
|
'(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})')
|
|
soundcloud_regex = "^(https:\\/\\/soundcloud\\.com\\/.*)"
|
|
is_youtube_link = re.match(youtube_regex, song_url)
|
|
is_soundcloud_link = re.match(soundcloud_regex, song_url)
|
|
|
|
if is_youtube_link:
|
|
url = "http://www.youtube.com/oembed?url={0}&format=json".format(song_url)
|
|
result = await self.get_json(url)
|
|
elif is_soundcloud_link and (self.settings["SOUNDCLOUD_CLIENT_ID"] is not None):
|
|
url = "http://api.soundcloud.com/resolve.json?url={0}&client_id={1}".format(song_url, self.settings["SOUNDCLOUD_CLIENT_ID"])
|
|
result = await self.get_json(url)
|
|
else:
|
|
result = {"title": "A song "}
|
|
return result
|
|
|
|
class EmptyPlayer(): #dummy player
|
|
def __init__(self):
|
|
self.paused = False
|
|
|
|
def stop(self):
|
|
pass
|
|
|
|
def is_playing(self):
|
|
return False
|
|
|
|
def is_done(self):
|
|
return True
|
|
|
|
class MaximumLength(Exception):
|
|
def __init__(self, m):
|
|
self.message = m
|
|
def __str__(self):
|
|
return self.message
|
|
|
|
def check_folders():
|
|
folders = ("data/audio", "data/audio/cache", "data/audio/playlists", "data/audio/localtracks", "data/audio/sfx")
|
|
for folder in folders:
|
|
if not os.path.exists(folder):
|
|
print("Creating " + folder + " folder...")
|
|
os.makedirs(folder)
|
|
|
|
def check_files():
|
|
|
|
default = {"VOLUME" : 0.5, "MAX_LENGTH" : 3700, "QUEUE_MODE" : True, "MAX_CACHE" : 0, "SOUNDCLOUD_CLIENT_ID": None, "TITLE_STATUS" : True, "SERVER_SFX_ON" : {}}
|
|
settings_path = "data/audio/settings.json"
|
|
|
|
if not os.path.isfile(settings_path):
|
|
print("Creating default audio settings.json...")
|
|
fileIO(settings_path, "save", default)
|
|
else: #consistency check
|
|
current = fileIO(settings_path, "load")
|
|
if current.keys() != default.keys():
|
|
for key in default.keys():
|
|
if key not in current.keys():
|
|
current[key] = default[key]
|
|
print("Adding " + str(key) + " field to audio settings.json")
|
|
fileIO(settings_path, "save", current)
|
|
|
|
|
|
allowed = ["^(https:\/\/www\\.youtube\\.com\/watch\\?v=...........*)", "^(https:\/\/youtu.be\/...........*)",
|
|
"^(https:\/\/youtube\\.com\/watch\\?v=...........*)", "^(https:\/\/soundcloud\\.com\/.*)"]
|
|
|
|
if not os.path.isfile("data/audio/accepted_links.json"):
|
|
print("Creating accepted_links.json...")
|
|
fileIO("data/audio/accepted_links.json", "save", allowed)
|
|
|
|
def setup(bot):
|
|
check_folders()
|
|
check_files()
|
|
if youtube_dl is None:
|
|
raise RuntimeError("You need to run `pip3 install youtube_dl`")
|
|
return
|
|
if opus is False:
|
|
raise RuntimeError("Your opus library's bitness must match your python installation's bitness. They both must be either 32bit or 64bit.")
|
|
return
|
|
elif opus is None:
|
|
raise RuntimeError("You need to install ffmpeg and opus. See \"https://github.com/Twentysix26/Red-DiscordBot/wiki/Requirements\"")
|
|
return
|
|
loop = asyncio.get_event_loop()
|
|
n = Audio(bot)
|
|
loop.create_task(n.queue_manager())
|
|
bot.add_listener(n.incoming_messages, "on_message")
|
|
bot.add_cog(n)
|