diff --git a/README.md b/README.md index da5f0c6..4af0155 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ You can enable the web interface into the configuration.ini file. 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 cd botamusique python3 -m venv venv diff --git a/media/playlist.py b/media/playlist.py index f927221..d70a974 100644 --- a/media/playlist.py +++ b/media/playlist.py @@ -11,10 +11,16 @@ def get_playlist_info(url, start_index=1, user=""): try: info = ydl.extract_info(url, download=False) 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', - 'title': info['entries'][j]['title'], - 'url': "https://www.youtube.com/watch?v=" + info['entries'][j]['url'], + 'title': title, + 'url': url, 'user': user, 'from_playlist': True, 'playlist_title': playlist_title, @@ -36,9 +42,11 @@ def get_music_info(index=0): for i in range(2): try: 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: var.playlist[0]['current_duration'] = info['entries'][0]['duration'] / 60 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: var.playlist[0]['next_duration'] = info['entries'][0]['duration'] / 60 var.playlist[0]['next_title'] = info['entries'][0]['title'] diff --git a/mumbleBot.py b/mumbleBot.py index be840a7..1f601f9 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -28,6 +28,33 @@ import media.playlist import media.radio 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: def __init__(self, args): @@ -38,6 +65,7 @@ class MumbleBot: self.channel = args.channel + # Set specific format for the log FORMAT = '%(asctime)s: %(message)s' if args.verbose: 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.info("Starting in INFO loglevel") + # the playlist is... a list (Surprise !!) var.playlist = [] var.user = args.user @@ -99,7 +128,7 @@ class MumbleBot: self.username = var.config.get("bot", "username") 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.set_codec_profile("audio") @@ -113,6 +142,7 @@ class MumbleBot: self.loop() + # Set the CTRL+C shortcut def ctrl_caught(self, signal, frame): logging.info("\nSIGINT caught, quitting, {} more to kill".format(2 - self.nb_exit)) self.exit = True @@ -122,13 +152,21 @@ class MumbleBot: sys.exit(0) self.nb_exit += 1 + # All text send to the chat is analysed by this function def message_received(self, text): + message = text.message.strip() user = self.mumble.users[text.actor]['name'] + 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] + if message[0] == var.config.get('command', 'command_symbol'): + # remove the symbol from the message message = message[1:].split(' ', 1) + + # use the first word as a command, the others one as parameters if len(message) > 0: command = message[0] parameter = '' @@ -144,6 +182,7 @@ class MumbleBot: self.mumble.users.myself.move_in(self.mumble.users[text.actor]['channel_id'], token=parameter) 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']: self.mumble.users[text.actor].send_message(var.config.get('strings', 'not_in_my_channel')) return @@ -152,6 +191,9 @@ class MumbleBot: self.mumble.users[text.actor].send_message(var.config.get('strings', 'pm_not_allowed')) return + ### + # Admin command + ### for i in var.db.items("user_ban"): if user.lower() == i[0]: 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')) return + ### + # everyday commands + ### if command == var.config.get('command', 'play_file') and parameter: music_folder = var.config.get('bot', 'music_folder') # sanitize "../" and so on @@ -230,7 +275,7 @@ class MumbleBot: elif command == var.config.get('command', 'play_url') and parameter: music = {'type': 'url', - 'url': self.get_url_from_input(parameter), + 'url': self.get_url_from_input(parameter), # grab the real URL 'user': user, 'ready': 'validation'} var.playlist.append(music) @@ -241,7 +286,6 @@ class MumbleBot: self.send_msg(var.config.get('strings', 'too_long'), text) else: for i in var.db.options("url_ban"): - print(i, ' -> ', {var.playlist[-1]["url"]}) if var.playlist[-1]['url'] == i: self.mumble.users[text.actor].send_message(var.config.get('strings', 'url_ban')) var.playlist.pop() @@ -253,7 +297,7 @@ class MumbleBot: self.send_msg(var.config.get('strings', 'unable_download'), text) 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: offset = int(parameter.split(" ")[-1]) except ValueError: @@ -286,6 +330,7 @@ class MumbleBot: elif command == var.config.get('command', 'update'): if self.is_admin(user): 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() msg = "" if "Requirement already up-to-date" in tp: @@ -293,6 +338,7 @@ class MumbleBot: else: msg += "Update done : " + tp.split('Successfully installed')[1] if 'up-to-date' not in sp.check_output(['/usr/bin/env', 'git', 'pull']).decode(): + # Need to change it with release tag msg += "
I'm up-to-date" else: msg += "
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() 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: self.volume = float(float(parameter) / 100) self.send_msg(var.config.get('strings', 'change_volume') % ( @@ -349,13 +396,15 @@ class MumbleBot: self.send_msg(reply, text) 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): 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) else: 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.async_download_next() else: @@ -413,24 +462,28 @@ class MumbleBot: uri = "" logging.debug("launch_music asked" + str(var.playlist[0])) 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')) + # Check if the music is ready to be played if var.playlist[0]["ready"] == "downloading": return elif var.playlist[0]["ready"] != "yes": logging.info("Current music wasn't ready, Downloading...") self.download_music(index=0) + # get the Path uri = var.playlist[0]['path'] if os.path.isfile(uri): audio = EasyID3(uri) 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 thumbnail_html = "" if os.path.isfile(path_thumbnail): + # Create the image message im = Image.open(path_thumbnail) im.thumbnail((100, 100), Image.ANTIALIAS) buffer = BytesIO() @@ -460,13 +513,14 @@ class MumbleBot: command = ["ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', uri, '-ac', '1', '-f', 's16le', '-ar', '48000', '-'] 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 def download_music(self, index): if var.playlist[index]['type'] == 'url' and var.playlist[index]['ready'] == "validation": if media.url.get_url_info(index=index): 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() 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')) @@ -479,6 +533,7 @@ class MumbleBot: self.send_msg(var.config.get('strings', 'unable_download')) if var.playlist[index]['type'] == 'url' and var.playlist[index]['ready'] == "no": + # download the music var.playlist[index]['ready'] = "downloading" logging.debug("Download index:" + str(index)) @@ -512,7 +567,7 @@ class MumbleBot: logging.info("Information before start downloading :" + str(var.playlist[index])) with youtube_dl.YoutubeDL(ydl_opts) as ydl: - for i in range(2): + for i in range(2): # Always try 2 times try: ydl.extract_info(url) if 'ready' in var.playlist[index] and var.playlist[index]['ready'] == "downloading": @@ -524,6 +579,8 @@ class MumbleBot: return 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") 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}) @@ -534,6 +591,7 @@ class MumbleBot: th.start() @staticmethod + # Parse the html from the message to get the URL def get_url_from_input(string): if string.startswith('http'): return string @@ -544,15 +602,19 @@ class MumbleBot: else: return False + # Main loop of the Bot def loop(self): raw_music = "" while not self.exit and self.mumble.isAlive(): 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) if self.thread: + # I get raw from ffmpeg thread raw_music = self.thread.stdout.read(480) if raw_music: + # Adjust the volume and send it to mumble self.mumble.sound_output.add_sound(audioop.mul(raw_music, 2, self.volume)) else: time.sleep(0.1) @@ -560,23 +622,29 @@ class MumbleBot: time.sleep(0.1) if self.thread is None or not raw_music: + # Not music into the buffet if self.is_playing: + # get next music self.is_playing = False self.next() if len(var.playlist) > 0: if var.playlist[0]['type'] in ['radio', 'file'] \ 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.async_download_next() while self.mumble.sound_output.get_buffer_size() > 0: + # Empty the buffer before exit time.sleep(0.01) time.sleep(0.5) if self.exit: + # The db is not fixed config like url/user ban and volume util.write_db() def stop(self): + # Kill the ffmpeg thread and empty the playlist if self.thread: self.thread.kill() self.thread = None @@ -587,6 +655,7 @@ class MumbleBot: self.mumble.users.myself.comment(var.config.get('bot', 'comment')) 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: own_channel = self.mumble.channels[self.mumble.users.myself['channel_id']] own_channel.send_text_message(msg)