From 3a6aba954566355d2af02ca237dbd28475b9154e Mon Sep 17 00:00:00 2001 From: azlux Date: Thu, 12 Jul 2018 00:40:29 +0200 Subject: [PATCH] Playlist and colume management into db #27 #16 #11 --- README.md | 1 + configuration.default.ini | 5 + db.ini | 4 + media.py | 2 +- mumbleBot.py | 200 +++++++++++++++++++++++++++++--------- util.py | 6 +- variables.py | 4 +- 7 files changed, 170 insertions(+), 52 deletions(-) create mode 100644 db.ini diff --git a/README.md b/README.md index 795f180..c068bd5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Bot the can play : You need to create a folder for all your music. Organize your music by subfolder. The main folder needs to be declared in the config (with a '/' at the end) +You can enable the web interface into the configuration.ini file. ### Installation 1. You need python 3 with opuslib and protobuf (look at the requirement of pymumble) diff --git a/configuration.default.ini b/configuration.default.ini index d7dbea3..9176984 100644 --- a/configuration.default.ini +++ b/configuration.default.ini @@ -24,6 +24,9 @@ announce_current_music = True allow_other_channel_message = False allow_private_message = True +# Maximum track played when a playlist is added. +max_track_playlist = 20 + [webinterface] enabled = False is_web_proxified = True @@ -34,6 +37,7 @@ listening_port = 8181 play_file = file play_url = url play_radio = radio +play_playlist = playlist help = help stop = stop @@ -74,6 +78,7 @@ pm_not_allowed = Private message aren't allowed. help = Command available:
!file [path]
!url [url] - youtube or soundcloud +
!playlist [url] [offset] - youtube or soundcloud playlist (the offset is the track number the bot will start to play - 1 by default)
!radio [url] - url of a stream
!list - display list of available tracks
!queue - display items in queue diff --git a/db.ini b/db.ini new file mode 100644 index 0000000..c5cf426 --- /dev/null +++ b/db.ini @@ -0,0 +1,4 @@ +[bot] +volume = 0.1 +ban_music = [] +ban_user = [] \ No newline at end of file diff --git a/media.py b/media.py index d403132..6d030e8 100644 --- a/media.py +++ b/media.py @@ -108,7 +108,7 @@ def clear_tmp_folder(path, size): size_tp += os.path.getsize(file) if int(size_tp/(1024*1024)) > size: logging.info("Cleaning tmp folder") - to_remove = all_files[idx:] + to_remove = all_files[:idx] print(to_remove) for f in to_remove: logging.debug("Removing " + f) diff --git a/mumbleBot.py b/mumbleBot.py index 9eb6723..e788661 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -39,19 +39,21 @@ class MumbleBot: ###### ## Format of the Playlist : ## [("","")] - ## types : file, radio, url + ## types : file, radio, url, is_playlist, number_music_to_play ###### ###### ## Format of the current_music variable # var.current_music = { "type" : str, - # "path" : str, - # "title" : str, - # "user" : str } - # len(var.current_music) = 4 - # var.current_music["type"] = - # var.current_music["path"] = if youtube/radio, if file - # var.current_music["title"] = + # "path" : str, # path of the file to play + # "url" : str # url to download + # "title" : str, + # "user" : str, + # "is_playlist": boolean, + # "number_track_to_play": int, # FOR PLAYLIST ONLY + # "start_index" : int, # FOR PLAYLIST ONLY + # "current_index" : int} # FOR PLAYLIST ONLY + # len(var.current_music) = 6 var.playlist = [] @@ -161,14 +163,24 @@ class MumbleBot: self.mumble.users[text.actor].send_message(msg) else: self.mumble.users[text.actor].send_message(var.config.get('strings', 'bad_file')) + self.async_download_next() elif command == var.config.get('command', 'play_url') and parameter: var.playlist.append(["url", parameter, user]) + self.async_download_next() + + elif command == var.config.get('command', 'play_playlist') and parameter: + offset = 1 + if len(message) > 2: + offset = int(message[2]) + var.playlist.append(["playlist", parameter, user, var.config.getint('bot', 'max_track_playlist'), offset]) + self.async_download_next() elif command == var.config.get('command', 'play_radio') and parameter: if var.config.has_option('radio', parameter): parameter = var.config.get('radio', parameter) var.playlist.append(["radio", parameter, user]) + self.async_download_next() elif command == var.config.get('command', 'help'): self.send_msg_channel(var.config.get('strings', 'help')) @@ -186,15 +198,15 @@ class MumbleBot: elif command == var.config.get('command', 'update'): if not self.is_admin(user): self.mumble.users[text.actor].send_message("Starting the update") - tp = sp.check_output([var.config.get('bot', 'pip3_path'), 'install','--upgrade','youtube-dl']).decode() - msg="" + tp = sp.check_output([var.config.get('bot', 'pip3_path'), 'install', '--upgrade', 'youtube-dl']).decode() + msg = "" if "Requirement already up-to-date" in tp: msg += "Youtube-dl is up-to-date" - else : + else: msg += "Update done : " + tp.split('Successfully installed')[1] - if 'Your branch is up-to-date' in sp.check_output(['/usr/bin/env','git','status']).decode(): + if 'Your branch is up-to-date' in sp.check_output(['/usr/bin/env', 'git', 'status']).decode(): msg += "<br /> Botamusique is up-to-date" - else : + else: msg += "<br /> Botamusique have available update" self.mumble.users[text.actor].send_message(msg) else: @@ -210,7 +222,7 @@ class MumbleBot: self.volume = float(float(parameter) / 100) self.send_msg_channel(var.config.get('strings', 'change_volume') % ( int(self.volume * 100), self.mumble.users[text.actor]['name'])) - var.config.set('bot', 'volume', str(self.volume)) + var.db.set('bot', 'volume', str(self.volume)) else: self.send_msg_channel(var.config.get('strings', 'current_volume') % int(self.volume * 100)) @@ -233,6 +245,13 @@ class MumbleBot: reply = "[file] {title} by {user}".format( title=var.current_music["title"], user=var.current_music["user"]) + elif source == "playlist": + reply = "[playlist] {title} (from the playlist <a href=\"{url}\">{playlist}</a> by {user}".format( + title=var.current_music["title"], + url=var.current_music["path"], + playlist=var.current_music["playlist_title"], + user=var.current_music["user"] + ) else: reply = "(?)[{}] {} {} by {}".format( var.current_music["type"], @@ -246,13 +265,9 @@ class MumbleBot: self.mumble.users[text.actor].send_message(reply) elif command == var.config.get('command', 'next'): - if var.playlist: - var.current_music = {'type': var.playlist[0][0], - 'path': var.playlist[0][1], - 'title': None, - 'user': var.playlist[0][2]} - var.playlist.pop(0) + if self.get_next(): self.launch_next() + self.async_download_next() else: self.mumble.users[text.actor].send_message(var.config.get('strings', 'queue_empty')) self.stop() @@ -292,25 +307,68 @@ class MumbleBot: self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480) self.playing = True - def is_admin(self, user): + @staticmethod + def is_admin(user): list_admin = var.config.get('bot', 'admin').split(';') if user in list_admin: return True else: return False + @staticmethod + def get_next(): + # Return True is next is possible + if var.current_music and var.current_music['type'] == "playlist": + var.current_music['current_index'] += 1 + if var.current_music['current_index'] <= (var.current_music['start_index'] + var.current_music['number_track_to_play']): + return True + + if not var.playlist: + return False + + if var.playlist[0][0] == "playlist": + var.current_music = {'type': var.playlist[0][0], + 'url': var.playlist[0][1], + 'title': None, + 'user': var.playlist[0][2], + 'is_playlist': True, + 'number_track_to_play': var.playlist[0][3], + 'start_index': var.playlist[0][4], + 'current_index': var.playlist[0][4] + } + else: + var.current_music = {'type': var.playlist[0][0], + 'url': var.playlist[0][1], + 'title': None, + 'user': var.playlist[0][2]} + var.playlist.pop(0) + return True + def launch_next(self): path = "" title = "" - if var.current_music["type"] == "url": - url = media.get_url(var.current_music["path"]) + var.next_downloaded = False + logging.debug(var.current_music) + if var.current_music["type"] == "url" or var.current_music["type"] == "playlist": + url = media.get_url(var.current_music["url"]) + if not url: return - media.clear_tmp_folder(var.config.get('bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size')) - path, title = self.download_music(url) - var.current_music["path"] = url - path_thumbnail = var.config.get('bot', 'tmp_folder') + hashlib.md5(url.encode()).hexdigest() + '.jpg' + media.clear_tmp_folder(var.config.get('bot', 'tmp_folder'), var.config.getint('bot', 'tmp_folder_max_size')) + + if var.current_music["type"] == "playlist": + path, title = self.download_music(url, var.current_music["current_index"]) + var.current_music["playlist_title"] = title + else: + path, title = self.download_music(url) + var.current_music["path"] = path + + audio = EasyID3(path) + if audio["title"]: + title = audio["title"][0] + + path_thumbnail = var.config.get('bot', 'tmp_folder') + hashlib.md5(path.encode()).hexdigest() + '.jpg' thumbnail_html = "" if os.path.isfile(path_thumbnail): im = Image.open(path_thumbnail) @@ -336,6 +394,8 @@ class MumbleBot: path = url title = media.get_radio_server_description(url) + var.current_music["title"] = title + if var.config.getboolean('debug', 'ffmpeg'): ffmpeg_debug = "debug" else: @@ -343,28 +403,44 @@ class MumbleBot: command = ["ffmpeg", '-v', ffmpeg_debug, '-nostdin', '-i', path, '-ac', '1', '-f', 's16le', '-ar', '48000', '-'] self.thread = sp.Popen(command, stdout=sp.PIPE, bufsize=480) - var.current_music["title"] = title - def download_music(self, url): + @staticmethod + def download_music(url, index=None): url_hash = hashlib.md5(url.encode()).hexdigest() + if index: + url_hash = url_hash + "-" + str(index) path = var.config.get('bot', 'tmp_folder') + url_hash + ".%(ext)s" mp3 = path.replace(".%(ext)s", ".mp3") if os.path.isfile(mp3): audio = EasyID3(mp3) video_title = audio["title"][0] else: - ydl_opts = { - 'format': 'bestaudio/best', - 'outtmpl': path, - 'noplaylist': True, - 'writethumbnail': True, - 'updatetime': False, - 'postprocessors': [{ - 'key': 'FFmpegExtractAudio', - 'preferredcodec': 'mp3', - 'preferredquality': '192'}, - {'key': 'FFmpegMetadata'}] - } + if index: + ydl_opts = { + 'format': 'bestaudio/best', + 'outtmpl': path, + 'writethumbnail': True, + 'updatetime': False, + 'playlist_items': str(index), + 'postprocessors': [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192'}, + {'key': 'FFmpegMetadata'}] + } + else: + ydl_opts = { + 'format': 'bestaudio/best', + 'outtmpl': path, + 'noplaylist': True, + 'writethumbnail': True, + 'updatetime': False, + 'postprocessors': [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192'}, + {'key': 'FFmpegMetadata'}] + } video_title = "" with youtube_dl.YoutubeDL(ydl_opts) as ydl: for i in range(2): @@ -377,6 +453,31 @@ class MumbleBot: break return mp3, video_title + def async_download_next(self): + if not var.next_downloaded: + var.next_downloaded = True + logging.info("Start download in thread") + th = threading.Thread(target=self.download_next, args=()) + th.daemon = True + th.start() + + def download_next(self): + if not var.current_music: + return + else: + if var.current_music["type"] == "playlist": + if var.current_music['current_index'] + 1 <= (var.current_music['start_index'] + var.current_music['number_track_to_play']): + self.download_music(media.get_url(var.current_music['url']), var.current_music["current_index"] + 1) + + if var.playlist: + url = media.get_url(var.playlist[0][1]) + if not url: + return + if var.playlist[0][0] == "playlist": + self.download_music(url, var.current_music["current_index"]) + elif var.playlist[0][0] == "playlist": + self.download_music(url) + def loop(self): raw_music = "" while not self.exit and self.mumble.isAlive(): @@ -393,10 +494,9 @@ class MumbleBot: time.sleep(0.1) if self.thread is None or not raw_music: - if len(var.playlist) != 0: - var.current_music = {'type': var.playlist[0][0], 'path': var.playlist[0][1], 'user': var.playlist[0][2]} - var.playlist.pop(0) + if self.get_next(): self.launch_next() + self.async_download_next() else: var.current_music = None @@ -405,7 +505,7 @@ class MumbleBot: time.sleep(0.5) if self.exit: - util.write_config() + util.write_db() def stop(self): if self.thread: @@ -433,6 +533,8 @@ if __name__ == '__main__': # General arguments parser.add_argument("--config", dest='config', type=str, default='configuration.ini', help='Load configuration from this file. Default: configuration.ini') + parser.add_argument("--db", dest='db', type=str, default='db.ini', help='database file. Default db.ini') + parser.add_argument("-q", "--quiet", dest="quiet", action="store_true", help="Only Error logs") # Mumble arguments @@ -443,13 +545,17 @@ if __name__ == '__main__': parser.add_argument("-c", "--channel", dest="channel", type=str, help="Default channel for the bot") args = parser.parse_args() - var.configfile = args.config + var.dbfile = args.db config = configparser.ConfigParser(interpolation=None, allow_no_value=True) - parsed_configs = config.read(['configuration.default.ini', var.configfile], encoding='latin-1') + parsed_configs = config.read(['configuration.default.ini', args.config, var.dbfile], encoding='latin-1') + + db = configparser.ConfigParser(interpolation=None, allow_no_value=True) + db.read([var.dbfile], encoding='latin-1') if len(parsed_configs) == 0: print('Could not read configuration from file \"{}\"'.format(args.config), file=sys.stderr) sys.exit() var.config = config + var.db = db botamusique = MumbleBot(args) diff --git a/util.py b/util.py index 8df6dfc..4bec3f3 100644 --- a/util.py +++ b/util.py @@ -66,9 +66,9 @@ def zipdir(zippath, zipname_prefix=None): return zipname -def write_config(): - with open(var.configfile, 'w') as f: - var.config.write(f) +def write_db(): + with open(var.dbfile, 'w') as f: + var.db.write(f) class Dir(object): diff --git a/variables.py b/variables.py index be2ea54..9ac5948 100644 --- a/variables.py +++ b/variables.py @@ -3,5 +3,7 @@ playlist = [] user = "" music_folder = "" is_proxified = False -configfile = None +dbfile = None +db = None config = None +next_downloaded = False