Comment for the code and Fix for #46

This commit is contained in:
Azlux 2019-04-17 01:40:01 +02:00
parent db703f5e98
commit 7df7dff7ea
3 changed files with 90 additions and 13 deletions

View File

@ -31,7 +31,7 @@ You can enable the web interface into the configuration.ini file.
Example installation commands for Debian and Ubuntu: Example installation commands for Debian and Ubuntu:
``` ```
apt install python3-venv ffmpeg libjpeg-dev apt install python3-venv ffmpeg libjpeg-dev zlibc zlib1g zlib1g-dev
git clone --recurse-submodules https://github.com/azlux/botamusique.git git clone --recurse-submodules https://github.com/azlux/botamusique.git
cd botamusique cd botamusique
python3 -m venv venv python3 -m venv venv

View File

@ -12,9 +12,15 @@ def get_playlist_info(url, start_index=1, user=""):
info = ydl.extract_info(url, download=False) info = ydl.extract_info(url, download=False)
playlist_title = info['title'] playlist_title = info['title']
for j in range(start_index, min(len(info['entries']), start_index + var.config.getint('bot', 'max_track_playlist'))): for j in range(start_index, min(len(info['entries']), start_index + var.config.getint('bot', 'max_track_playlist'))):
# Unknow String if No title into the json
title = info['entries'][j]['title'] if 'title' in info['entries'][j] else "Unknown Title"
# Add youtube url if the url in the json isn't a full url
url = info['entries'][j]['url'] if info['entries'][j]['url'][0:4] == 'http' else "https://www.youtube.com/watch?v=" + info['entries'][j]['url']
# append the music to a list of futur music to play
music = {'type': 'url', music = {'type': 'url',
'title': info['entries'][j]['title'], 'title': title,
'url': "https://www.youtube.com/watch?v=" + info['entries'][j]['url'], 'url': url,
'user': user, 'user': user,
'from_playlist': True, 'from_playlist': True,
'playlist_title': playlist_title, 'playlist_title': playlist_title,
@ -36,9 +42,11 @@ def get_music_info(index=0):
for i in range(2): for i in range(2):
try: try:
info = ydl.extract_info(var.playlist[0]['url'], download=False) info = ydl.extract_info(var.playlist[0]['url'], download=False)
# Check if the Duration is longer than the config
if var.playlist[0]['current_index'] == index: if var.playlist[0]['current_index'] == index:
var.playlist[0]['current_duration'] = info['entries'][0]['duration'] / 60 var.playlist[0]['current_duration'] = info['entries'][0]['duration'] / 60
var.playlist[0]['current_title'] = info['entries'][0]['title'] var.playlist[0]['current_title'] = info['entries'][0]['title']
# Check if the Duration of the next music is longer than the config (async download)
elif var.playlist[0]['current_index'] == index - 1: elif var.playlist[0]['current_index'] == index - 1:
var.playlist[0]['next_duration'] = info['entries'][0]['duration'] / 60 var.playlist[0]['next_duration'] = info['entries'][0]['duration'] / 60
var.playlist[0]['next_title'] = info['entries'][0]['title'] var.playlist[0]['next_title'] = info['entries'][0]['title']

View File

@ -28,6 +28,33 @@ import media.playlist
import media.radio import media.radio
import media.system import media.system
"""
FORMAT OF A MUSIC INTO THE PLAYLIST
type : url
url
title
path
duration
thundnail
user
ready (validation, no, downloading, yes)
from_playlist (yes,no)
playlist_title
playlist_url
type : radio
url
name
current_title
user
type : file
path
title
duration
user
"""
class MumbleBot: class MumbleBot:
def __init__(self, args): def __init__(self, args):
@ -38,6 +65,7 @@ class MumbleBot:
self.channel = args.channel self.channel = args.channel
# Set specific format for the log
FORMAT = '%(asctime)s: %(message)s' FORMAT = '%(asctime)s: %(message)s'
if args.verbose: if args.verbose:
logging.basicConfig(format=FORMAT, level=logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S') logging.basicConfig(format=FORMAT, level=logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S')
@ -49,6 +77,7 @@ class MumbleBot:
logging.basicConfig(format=FORMAT, level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S') logging.basicConfig(format=FORMAT, level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S')
logging.info("Starting in INFO loglevel") logging.info("Starting in INFO loglevel")
# the playlist is... a list (Surprise !!)
var.playlist = [] var.playlist = []
var.user = args.user var.user = args.user
@ -99,7 +128,7 @@ class MumbleBot:
self.username = var.config.get("bot", "username") self.username = var.config.get("bot", "username")
self.mumble = pymumble.Mumble(host, user=self.username, port=port, password=password, tokens=tokens, self.mumble = pymumble.Mumble(host, user=self.username, port=port, password=password, tokens=tokens,
debug=var.config.getboolean('debug', 'mumbleConnection'), certfile=args.certificate) debug=var.config.getboolean('debug', 'mumbleConnection'), certfile=certificate)
self.mumble.callbacks.set_callback("text_received", self.message_received) self.mumble.callbacks.set_callback("text_received", self.message_received)
self.mumble.set_codec_profile("audio") self.mumble.set_codec_profile("audio")
@ -113,6 +142,7 @@ class MumbleBot:
self.loop() self.loop()
# Set the CTRL+C shortcut
def ctrl_caught(self, signal, frame): def ctrl_caught(self, signal, frame):
logging.info("\nSIGINT caught, quitting, {} more to kill".format(2 - self.nb_exit)) logging.info("\nSIGINT caught, quitting, {} more to kill".format(2 - self.nb_exit))
self.exit = True self.exit = True
@ -122,13 +152,21 @@ class MumbleBot:
sys.exit(0) sys.exit(0)
self.nb_exit += 1 self.nb_exit += 1
# All text send to the chat is analysed by this function
def message_received(self, text): def message_received(self, text):
message = text.message.strip() message = text.message.strip()
user = self.mumble.users[text.actor]['name'] user = self.mumble.users[text.actor]['name']
if var.config.getboolean('command', 'split_username_at_space'): if var.config.getboolean('command', 'split_username_at_space'):
# in can you use https://github.com/Natenom/mumblemoderator-module-collection/tree/master/os-suffixes , you want to split the username
user = user.split()[0] user = user.split()[0]
if message[0] == var.config.get('command', 'command_symbol'): if message[0] == var.config.get('command', 'command_symbol'):
# remove the symbol from the message
message = message[1:].split(' ', 1) message = message[1:].split(' ', 1)
# use the first word as a command, the others one as parameters
if len(message) > 0: if len(message) > 0:
command = message[0] command = message[0]
parameter = '' parameter = ''
@ -144,6 +182,7 @@ class MumbleBot:
self.mumble.users.myself.move_in(self.mumble.users[text.actor]['channel_id'], token=parameter) self.mumble.users.myself.move_in(self.mumble.users[text.actor]['channel_id'], token=parameter)
return return
# Anti stupid guy function
if not self.is_admin(user) and not var.config.getboolean('bot', 'allow_other_channel_message') and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself['channel_id']: if not self.is_admin(user) and not var.config.getboolean('bot', 'allow_other_channel_message') and self.mumble.users[text.actor]['channel_id'] != self.mumble.users.myself['channel_id']:
self.mumble.users[text.actor].send_message(var.config.get('strings', 'not_in_my_channel')) self.mumble.users[text.actor].send_message(var.config.get('strings', 'not_in_my_channel'))
return return
@ -152,6 +191,9 @@ class MumbleBot:
self.mumble.users[text.actor].send_message(var.config.get('strings', 'pm_not_allowed')) self.mumble.users[text.actor].send_message(var.config.get('strings', 'pm_not_allowed'))
return return
###
# Admin command
###
for i in var.db.items("user_ban"): for i in var.db.items("user_ban"):
if user.lower() == i[0]: if user.lower() == i[0]:
self.mumble.users[text.actor].send_message(var.config.get('strings', 'user_ban')) self.mumble.users[text.actor].send_message(var.config.get('strings', 'user_ban'))
@ -199,6 +241,9 @@ class MumbleBot:
self.mumble.users[text.actor].send_message(var.config.get('strings', 'url_ban')) self.mumble.users[text.actor].send_message(var.config.get('strings', 'url_ban'))
return return
###
# everyday commands
###
if command == var.config.get('command', 'play_file') and parameter: if command == var.config.get('command', 'play_file') and parameter:
music_folder = var.config.get('bot', 'music_folder') music_folder = var.config.get('bot', 'music_folder')
# sanitize "../" and so on # sanitize "../" and so on
@ -230,7 +275,7 @@ class MumbleBot:
elif command == var.config.get('command', 'play_url') and parameter: elif command == var.config.get('command', 'play_url') and parameter:
music = {'type': 'url', music = {'type': 'url',
'url': self.get_url_from_input(parameter), 'url': self.get_url_from_input(parameter), # grab the real URL
'user': user, 'user': user,
'ready': 'validation'} 'ready': 'validation'}
var.playlist.append(music) var.playlist.append(music)
@ -241,7 +286,6 @@ class MumbleBot:
self.send_msg(var.config.get('strings', 'too_long'), text) self.send_msg(var.config.get('strings', 'too_long'), text)
else: else:
for i in var.db.options("url_ban"): for i in var.db.options("url_ban"):
print(i, ' -> ', {var.playlist[-1]["url"]})
if var.playlist[-1]['url'] == i: if var.playlist[-1]['url'] == i:
self.mumble.users[text.actor].send_message(var.config.get('strings', 'url_ban')) self.mumble.users[text.actor].send_message(var.config.get('strings', 'url_ban'))
var.playlist.pop() var.playlist.pop()
@ -253,7 +297,7 @@ class MumbleBot:
self.send_msg(var.config.get('strings', 'unable_download'), text) self.send_msg(var.config.get('strings', 'unable_download'), text)
elif command == var.config.get('command', 'play_playlist') and parameter: elif command == var.config.get('command', 'play_playlist') and parameter:
offset = 1 offset = 1 # if you want to start the playlist at a specific index
try: try:
offset = int(parameter.split(" ")[-1]) offset = int(parameter.split(" ")[-1])
except ValueError: except ValueError:
@ -286,6 +330,7 @@ class MumbleBot:
elif command == var.config.get('command', 'update'): elif command == var.config.get('command', 'update'):
if self.is_admin(user): if self.is_admin(user):
self.mumble.users[text.actor].send_message("Starting the update") self.mumble.users[text.actor].send_message("Starting the update")
# Need to be improved
tp = sp.check_output([var.config.get('bot', 'pip3_path'), 'install', '--upgrade', 'youtube-dl']).decode() tp = sp.check_output([var.config.get('bot', 'pip3_path'), 'install', '--upgrade', 'youtube-dl']).decode()
msg = "" msg = ""
if "Requirement already up-to-date" in tp: if "Requirement already up-to-date" in tp:
@ -293,6 +338,7 @@ class MumbleBot:
else: else:
msg += "Update done : " + tp.split('Successfully installed')[1] msg += "Update done : " + tp.split('Successfully installed')[1]
if 'up-to-date' not in sp.check_output(['/usr/bin/env', 'git', 'pull']).decode(): if 'up-to-date' not in sp.check_output(['/usr/bin/env', 'git', 'pull']).decode():
# Need to change it with release tag
msg += "<br /> I'm up-to-date" msg += "<br /> I'm up-to-date"
else: else:
msg += "<br /> I have available updates, need to do it manually" msg += "<br /> I have available updates, need to do it manually"
@ -306,6 +352,7 @@ class MumbleBot:
self.mumble.channels.find_by_name(self.channel).move_in() self.mumble.channels.find_by_name(self.channel).move_in()
elif command == var.config.get('command', 'volume'): elif command == var.config.get('command', 'volume'):
# The volume is a percentage
if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100: if parameter is not None and parameter.isdigit() and 0 <= int(parameter) <= 100:
self.volume = float(float(parameter) / 100) self.volume = float(float(parameter) / 100)
self.send_msg(var.config.get('strings', 'change_volume') % ( self.send_msg(var.config.get('strings', 'change_volume') % (
@ -349,13 +396,15 @@ class MumbleBot:
self.send_msg(reply, text) self.send_msg(reply, text)
elif command == var.config.get('command', 'skip'): elif command == var.config.get('command', 'skip'):
if parameter is not None and parameter.isdigit() and int(parameter) > 0: if parameter is not None and parameter.isdigit() and int(parameter) > 0: # Allow to remove specific music into the queue with a number
if int(parameter) < len(var.playlist): if int(parameter) < len(var.playlist):
removed = var.playlist.pop(int(parameter)) removed = var.playlist.pop(int(parameter))
# the Title isn't here if the music wasn't downloaded
self.send_msg(var.config.get('strings', 'removing_item') % (removed['title'] if 'title' in removed else removed['url']), text) self.send_msg(var.config.get('strings', 'removing_item') % (removed['title'] if 'title' in removed else removed['url']), text)
else: else:
self.send_msg(var.config.get('strings', 'no_possible'), text) self.send_msg(var.config.get('strings', 'no_possible'), text)
elif self.next(): elif self.next(): # Is no number send, just skip the current music
self.launch_music() self.launch_music()
self.async_download_next() self.async_download_next()
else: else:
@ -413,24 +462,28 @@ class MumbleBot:
uri = "" uri = ""
logging.debug("launch_music asked" + str(var.playlist[0])) logging.debug("launch_music asked" + str(var.playlist[0]))
if var.playlist[0]["type"] == "url": if var.playlist[0]["type"] == "url":
# Delete older music is the tmp folder is too big
media.system.clear_tmp_folder(var.config.get('bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size')) media.system.clear_tmp_folder(var.config.get('bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size'))
# Check if the music is ready to be played
if var.playlist[0]["ready"] == "downloading": if var.playlist[0]["ready"] == "downloading":
return return
elif var.playlist[0]["ready"] != "yes": elif var.playlist[0]["ready"] != "yes":
logging.info("Current music wasn't ready, Downloading...") logging.info("Current music wasn't ready, Downloading...")
self.download_music(index=0) self.download_music(index=0)
# get the Path
uri = var.playlist[0]['path'] uri = var.playlist[0]['path']
if os.path.isfile(uri): if os.path.isfile(uri):
audio = EasyID3(uri) audio = EasyID3(uri)
title = "" title = ""
if audio["title"]: if audio["title"]:
title = audio["title"][0] title = audio["title"][0] # take the title from the file tag
path_thumbnail = var.playlist[0]['path'][:-4] + '.jpg' # Remove .mp3 and add .jpg path_thumbnail = var.playlist[0]['path'][:-4] + '.jpg' # Remove .mp3 and add .jpg
thumbnail_html = "" thumbnail_html = ""
if os.path.isfile(path_thumbnail): if os.path.isfile(path_thumbnail):
# Create the image message
im = Image.open(path_thumbnail) im = Image.open(path_thumbnail)
im.thumbnail((100, 100), Image.ANTIALIAS) im.thumbnail((100, 100), Image.ANTIALIAS)
buffer = BytesIO() buffer = BytesIO()
@ -460,13 +513,14 @@ class MumbleBot:
command = ["ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-'] command = ["ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-']
logging.info("FFmpeg command : " + " ".join(command)) logging.info("FFmpeg command : " + " ".join(command))
self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480) self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480) # The ffmpeg process is a thread
self.is_playing = True self.is_playing = True
def download_music(self, index): def download_music(self, index):
if var.playlist[index]['type'] == 'url' and var.playlist[index]['ready'] == "validation": if var.playlist[index]['type'] == 'url' and var.playlist[index]['ready'] == "validation":
if media.url.get_url_info(index=index): if media.url.get_url_info(index=index):
if var.playlist[index]['duration'] > var.config.getint('bot', 'max_track_duration'): if var.playlist[index]['duration'] > var.config.getint('bot', 'max_track_duration'):
# Check the length, useful in case of playlist, it wasn't checked before)
var.playlist.pop() var.playlist.pop()
logging.info("the music " + var.playlist[index]["url"] + " has a duration of " + var.playlist[index]['duration'] + "s -- too long") logging.info("the music " + var.playlist[index]["url"] + " has a duration of " + var.playlist[index]['duration'] + "s -- too long")
self.send_msg(var.config.get('strings', 'too_long')) self.send_msg(var.config.get('strings', 'too_long'))
@ -479,6 +533,7 @@ class MumbleBot:
self.send_msg(var.config.get('strings', 'unable_download')) self.send_msg(var.config.get('strings', 'unable_download'))
if var.playlist[index]['type'] == 'url' and var.playlist[index]['ready'] == "no": if var.playlist[index]['type'] == 'url' and var.playlist[index]['ready'] == "no":
# download the music
var.playlist[index]['ready'] = "downloading" var.playlist[index]['ready'] = "downloading"
logging.debug("Download index:" + str(index)) logging.debug("Download index:" + str(index))
@ -512,7 +567,7 @@ class MumbleBot:
logging.info("Information before start downloading :" + str(var.playlist[index])) logging.info("Information before start downloading :" + str(var.playlist[index]))
with youtube_dl.YoutubeDL(ydl_opts) as ydl: with youtube_dl.YoutubeDL(ydl_opts) as ydl:
for i in range(2): for i in range(2): # Always try 2 times
try: try:
ydl.extract_info(url) ydl.extract_info(url)
if 'ready' in var.playlist[index] and var.playlist[index]['ready'] == "downloading": if 'ready' in var.playlist[index] and var.playlist[index]['ready'] == "downloading":
@ -524,6 +579,8 @@ class MumbleBot:
return return
def async_download_next(self): def async_download_next(self):
# Function start if the next music isn't ready
# Do nothing in case the next music is already downloaded
logging.info("Async download next asked") logging.info("Async download next asked")
if len(var.playlist) > 1 and var.playlist[1]['type'] == 'url' and var.playlist[1]['ready'] in ["no", "validation"]: if len(var.playlist) > 1 and var.playlist[1]['type'] == 'url' and var.playlist[1]['ready'] in ["no", "validation"]:
th = threading.Thread(target=self.download_music, kwargs={'index': 1}) th = threading.Thread(target=self.download_music, kwargs={'index': 1})
@ -534,6 +591,7 @@ class MumbleBot:
th.start() th.start()
@staticmethod @staticmethod
# Parse the html from the message to get the URL
def get_url_from_input(string): def get_url_from_input(string):
if string.startswith('http'): if string.startswith('http'):
return string return string
@ -544,15 +602,19 @@ class MumbleBot:
else: else:
return False return False
# Main loop of the Bot
def loop(self): def loop(self):
raw_music = "" raw_music = ""
while not self.exit and self.mumble.isAlive(): while not self.exit and self.mumble.isAlive():
while self.mumble.sound_output.get_buffer_size() > 0.5 and not self.exit: while self.mumble.sound_output.get_buffer_size() > 0.5 and not self.exit:
# If the buffer isn't empty, I cannot send new music part, so I wait
time.sleep(0.01) time.sleep(0.01)
if self.thread: if self.thread:
# I get raw from ffmpeg thread
raw_music = self.thread.stdout.read(480) raw_music = self.thread.stdout.read(480)
if raw_music: if raw_music:
# Adjust the volume and send it to mumble
self.mumble.sound_output.add_sound(audioop.mul(raw_music, 2, self.volume)) self.mumble.sound_output.add_sound(audioop.mul(raw_music, 2, self.volume))
else: else:
time.sleep(0.1) time.sleep(0.1)
@ -560,23 +622,29 @@ class MumbleBot:
time.sleep(0.1) time.sleep(0.1)
if self.thread is None or not raw_music: if self.thread is None or not raw_music:
# Not music into the buffet
if self.is_playing: if self.is_playing:
# get next music
self.is_playing = False self.is_playing = False
self.next() self.next()
if len(var.playlist) > 0: if len(var.playlist) > 0:
if var.playlist[0]['type'] in ['radio', 'file'] \ if var.playlist[0]['type'] in ['radio', 'file'] \
or (var.playlist[0]['type'] == 'url' and var.playlist[0]['ready'] not in ['validation', 'downloading']): or (var.playlist[0]['type'] == 'url' and var.playlist[0]['ready'] not in ['validation', 'downloading']):
# Check if the music can be start before launch the music
self.launch_music() self.launch_music()
self.async_download_next() self.async_download_next()
while self.mumble.sound_output.get_buffer_size() > 0: while self.mumble.sound_output.get_buffer_size() > 0:
# Empty the buffer before exit
time.sleep(0.01) time.sleep(0.01)
time.sleep(0.5) time.sleep(0.5)
if self.exit: if self.exit:
# The db is not fixed config like url/user ban and volume
util.write_db() util.write_db()
def stop(self): def stop(self):
# Kill the ffmpeg thread and empty the playlist
if self.thread: if self.thread:
self.thread.kill() self.thread.kill()
self.thread = None self.thread = None
@ -587,6 +655,7 @@ class MumbleBot:
self.mumble.users.myself.comment(var.config.get('bot', 'comment')) self.mumble.users.myself.comment(var.config.get('bot', 'comment'))
def send_msg(self, msg, text=None): def send_msg(self, msg, text=None):
# text if the object message, contain information if direct message or channel message
if not text or not text.session: if not text or not text.session:
own_channel = self.mumble.channels[self.mumble.users.myself['channel_id']] own_channel = self.mumble.channels[self.mumble.users.myself['channel_id']]
own_channel.send_text_message(msg) own_channel.send_text_message(msg)