]> git.0d.be Git - botaradio.git/blobdiff - command.py
also accept files according to their extension
[botaradio.git] / command.py
index da821dba7124a42d824205cb133049c5a9efde33..28c38d85c45e1ce5148dc3f693e2d6758ea5b4cf 100644 (file)
@@ -1,6 +1,5 @@
 # coding=utf-8
 import logging
-import os.path
 import pymumble.pymumble_py3 as pymumble
 import re
 
@@ -10,20 +9,20 @@ import util
 import variables as var
 from librb import radiobrowser
 from database import SettingsDatabase, MusicDatabase
-from media.playlist import get_item_wrapper, get_item_wrapper_by_id
-from media.file import FileItem
-from media.url_from_playlist import PlaylistURLItem, get_playlist_info
-from media.url import URLItem
-from media.radio import RadioItem
+from media.item import item_id_generators, dict_to_item, dicts_to_items
+from media.cache import get_cached_wrapper_from_scrap, get_cached_wrapper_by_id, get_cached_wrappers_by_tags
+from media.url_from_playlist import get_playlist_info
 
 log = logging.getLogger("bot")
 
+
 def register_all_commands(bot):
-    bot.register_command(constants.commands('joinme'), cmd_joinme)
-    bot.register_command(constants.commands('user_ban'), cmd_user_ban)
-    bot.register_command(constants.commands('user_unban'), cmd_user_unban)
-    bot.register_command(constants.commands('url_ban'), cmd_url_ban)
-    bot.register_command(constants.commands('url_unban'), cmd_url_unban)
+    bot.register_command(constants.commands('joinme'), cmd_joinme, no_partial_match=False, access_outside_channel=True)
+    bot.register_command(constants.commands('user_ban'), cmd_user_ban, no_partial_match=True)
+    bot.register_command(constants.commands('user_unban'), cmd_user_unban, no_partial_match=True)
+    bot.register_command(constants.commands('url_ban_list'), cmd_url_ban_list, no_partial_match=True)
+    bot.register_command(constants.commands('url_ban'), cmd_url_ban, no_partial_match=True)
+    bot.register_command(constants.commands('url_unban'), cmd_url_unban, no_partial_match=True)
     bot.register_command(constants.commands('play'), cmd_play)
     bot.register_command(constants.commands('pause'), cmd_pause)
     bot.register_command(constants.commands('play_file'), cmd_play_file)
@@ -31,15 +30,16 @@ def register_all_commands(bot):
     bot.register_command(constants.commands('play_url'), cmd_play_url)
     bot.register_command(constants.commands('play_playlist'), cmd_play_playlist)
     bot.register_command(constants.commands('play_radio'), cmd_play_radio)
+    bot.register_command(constants.commands('play_tag'), cmd_play_tags)
     bot.register_command(constants.commands('rb_query'), cmd_rb_query)
     bot.register_command(constants.commands('rb_play'), cmd_rb_play)
     bot.register_command(constants.commands('yt_search'), cmd_yt_search)
     bot.register_command(constants.commands('yt_play'), cmd_yt_play)
-    bot.register_command(constants.commands('help'), cmd_help)
+    bot.register_command(constants.commands('help'), cmd_help, no_partial_match=False, access_outside_channel=True)
     bot.register_command(constants.commands('stop'), cmd_stop)
     bot.register_command(constants.commands('clear'), cmd_clear)
     bot.register_command(constants.commands('kill'), cmd_kill)
-    bot.register_command(constants.commands('update'), cmd_update)
+    bot.register_command(constants.commands('update'), cmd_update, no_partial_match=True)
     bot.register_command(constants.commands('stop_and_getout'), cmd_stop_and_getout)
     bot.register_command(constants.commands('volume'), cmd_volume)
     bot.register_command(constants.commands('ducking'), cmd_ducking)
@@ -54,14 +54,21 @@ def register_all_commands(bot):
     bot.register_command(constants.commands('random'), cmd_random)
     bot.register_command(constants.commands('repeat'), cmd_repeat)
     bot.register_command(constants.commands('mode'), cmd_mode)
-    bot.register_command(constants.commands('drop_database'), cmd_drop_database, True)
-    bot.register_command(constants.commands('recache'), cmd_refresh_cache, True)
+    bot.register_command(constants.commands('add_tag'), cmd_add_tag)
+    bot.register_command(constants.commands('remove_tag'), cmd_remove_tag)
+    bot.register_command(constants.commands('find_tagged'), cmd_find_tagged)
+    bot.register_command(constants.commands('search'), cmd_search_library)
+    bot.register_command(constants.commands('add_from_shortlist'), cmd_shortlist)
+    bot.register_command(constants.commands('delete_from_library'), cmd_delete_from_library)
+    bot.register_command(constants.commands('drop_database'), cmd_drop_database, no_partial_match=True)
+    bot.register_command(constants.commands('rescan'), cmd_refresh_cache, no_partial_match=True)
 
     # Just for debug use
     bot.register_command('rtrms', cmd_real_time_rms, True)
     bot.register_command('loop', cmd_loop_state, True)
     bot.register_command('item', cmd_item, True)
 
+
 def send_multi_lines(bot, lines, text, linebreak="<br />"):
     global log
 
@@ -70,16 +77,22 @@ def send_multi_lines(bot, lines, text, linebreak="<br />"):
     for newline in lines:
         msg += br
         br = linebreak
-        if (len(msg) + len(newline)) > (bot.mumble.get_max_message_length() - 4) != 0: # 4 == len("<br>")
+        if bot.mumble.get_max_message_length()\
+                    and (len(msg) + len(newline)) > (bot.mumble.get_max_message_length() - 4):  # 4 == len("<br>")
             bot.send_msg(msg, text)
             msg = ""
         msg += newline
 
     bot.send_msg(msg, text)
 
-# ---------------- Commands ------------------
+
+# ---------------- Variables -----------------
+
+song_shortlist = []
 
 
+# ---------------- Commands ------------------
+
 def cmd_joinme(bot, user, text, command, parameter):
     global log
 
@@ -117,8 +130,26 @@ def cmd_url_ban(bot, user, text, command, parameter):
     if bot.is_admin(user):
         if parameter:
             bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter)))
+
+            id = item_id_generators['url'](url=parameter)
+            var.cache.free_and_delete(id)
+            var.playlist.remove_by_id(id)
         else:
-            bot.mumble.users[text.actor].send_text_message(util.get_url_ban())
+            if var.playlist.current_item() and var.playlist.current_item().type == 'url':
+                item = var.playlist.current_item().item()
+                bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(item.url)))
+                var.cache.free_and_delete(item.id)
+                var.playlist.remove_by_id(item.id)
+            else:
+                bot.send_msg(constants.strings('bad_parameter', command=command))
+    else:
+        bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
+    return
+
+
+def cmd_url_ban_list(bot, user, text, command, parameter):
+    if bot.is_admin(user):
+        bot.mumble.users[text.actor].send_text_message(util.get_url_ban())
     else:
         bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
     return
@@ -141,10 +172,15 @@ def cmd_play(bot, user, text, command, parameter):
     if len(var.playlist) > 0:
         if parameter:
             if parameter.isdigit() and 1 <= int(parameter) <= len(var.playlist):
-                var.playlist.point_to(int(parameter) - 1 - 1) # First "-1" transfer 12345 to 01234, second "-1"
-                                                            # point to the previous item. the loop will next to
-                                                            # the one you want
-                bot.interrupt()
+                # First "-1" transfer 12345 to 01234, second "-1"
+                # point to the previous item. the loop will next to
+                # the one you want
+                var.playlist.point_to(int(parameter) - 1 - 1)
+
+                if not bot.is_pause:
+                    bot.interrupt()
+                else:
+                    bot.is_pause = False
             else:
                 bot.send_msg(constants.strings('invalid_index', index=parameter), text)
 
@@ -165,16 +201,16 @@ def cmd_pause(bot, user, text, command, parameter):
 
 
 def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=False):
-    global log
+    global log, song_shortlist
 
     # if parameter is {index}
     if parameter.isdigit():
-        files = var.library.files
+        files = var.cache.files
         if int(parameter) < len(files):
-            music_wrapper = get_item_wrapper_by_id(bot, var.library.file_id_lookup[files[int(parameter)]], user)
+            music_wrapper = get_cached_wrapper_by_id(bot, var.cache.file_id_lookup[files[int(parameter)]], user)
             var.playlist.append(music_wrapper)
             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
-            bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
+            bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
             return
 
     # if parameter is {path}
@@ -185,64 +221,69 @@ def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=Fals
         #     bot.send_msg(constants.strings('no_file'), text)
         #     return
 
-        if parameter in var.library.files:
-            music_wrapper = get_item_wrapper(bot, type='file', path=parameter, user=user)
+        if parameter in var.cache.files:
+            music_wrapper = get_cached_wrapper_from_scrap(bot, type='file', path=parameter, user=user)
             var.playlist.append(music_wrapper)
             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
-            bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
+            bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
             return
 
         # if parameter is {folder}
-        files = var.library.dir.get_files(parameter)
+        files = var.cache.dir.get_files(parameter)
         if files:
             msgs = [constants.strings('multiple_file_added')]
             count = 0
 
             for file in files:
                 count += 1
-                music_wrapper = get_item_wrapper_by_id(bot, var.library.file_id_lookup[file],user)
+                music_wrapper = get_cached_wrapper_by_id(bot, var.cache.file_id_lookup[file], user)
                 var.playlist.append(music_wrapper)
                 log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
                 msgs.append("{} ({})".format(music_wrapper.item().title, music_wrapper.item().path))
 
             if count != 0:
-                send_multi_lines(bot, msgs, text)
+                send_multi_lines(bot, msgs, None)
                 return
 
         else:
             # try to do a partial match
-            files = var.library.files
-            matches = [(index, file) for index, file in enumerate(files) if parameter.lower() in file.lower()]
+            files = var.cache.files
+            matches = [file for file in files if parameter.lower() in file.lower()]
             if len(matches) == 1:
-                file = matches[0][1]
-                music_wrapper = get_item_wrapper_by_id(bot, var.library.file_id_lookup[file],user)
+                file = matches[0]
+                music_wrapper = get_cached_wrapper_by_id(bot, var.cache.file_id_lookup[file], user)
                 var.playlist.append(music_wrapper)
                 log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
-                bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
+                bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
                 return
             elif len(matches) > 1:
-                msgs = [ constants.strings('multiple_matches') ]
-                for match in matches:
-                    music_wrapper = get_item_wrapper_by_id(bot, var.library.file_id_lookup[match[1]], user)
-                    msgs.append("<b>{:0>3d}</b> - <b>{:s}</b> ({:s})".format(
-                        match[0], music_wrapper.item().title, match[1]))
+                msgs = [constants.strings('multiple_matches')]
+                song_shortlist = []
+                for index, match in enumerate(matches):
+                    id = var.cache.file_id_lookup[match]
+                    music_dict = var.music_db.query_music_by_id(id)
+                    item = dict_to_item(bot, music_dict)
+
+                    song_shortlist.append(music_dict)
+
+                    msgs.append("<b>{:d}</b> - <b>{:s}</b> ({:s})".format(
+                        index + 1, item.title, match))
                 send_multi_lines(bot, msgs, text)
                 return
 
     if do_not_refresh_cache:
         bot.send_msg(constants.strings("no_file"), text)
     else:
-        var.library.build_dir_cache(bot)
+        var.cache.build_dir_cache(bot)
         cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=True)
 
 
 def cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cache=False):
     global log
 
-    music_folder = var.music_folder
     if parameter:
-        files = var.library.files
-        msgs = [ constants.strings('multiple_file_added') + "<ul>"]
+        files = var.cache.files
+        msgs = [constants.strings('multiple_file_added') + "<ul>"]
         count = 0
         try:
             music_wrappers = []
@@ -250,26 +291,26 @@ def cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cach
                 match = re.search(parameter, file)
                 if match and match[0]:
                     count += 1
-                    music_wrapper = get_item_wrapper_by_id(bot, var.library.file_id_lookup[file], user)
+                    music_wrapper = get_cached_wrapper_by_id(bot, var.cache.file_id_lookup[file], user)
                     music_wrappers.append(music_wrapper)
                     log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
                     msgs.append("<li><b>{}</b> ({})</li>".format(music_wrapper.item().title,
-                                                 file[:match.span()[0]]
-                                                 + "<b style='color:pink'>"
-                                                 + file[match.span()[0]: match.span()[1]]
-                                                 + "</b>"
-                                                 + file[match.span()[1]:]
-                                                 ))
+                                                                 file[:match.span()[0]]
+                                                                 + "<b style='color:pink'>"
+                                                                 + file[match.span()[0]: match.span()[1]]
+                                                                 + "</b>"
+                                                                 + file[match.span()[1]:]
+                                                                 ))
 
             if count != 0:
                 msgs.append("</ul>")
                 var.playlist.extend(music_wrappers)
-                send_multi_lines(bot, msgs, text, "")
+                send_multi_lines(bot, msgs, None, "")
             else:
                 if do_not_refresh_cache:
                     bot.send_msg(constants.strings("no_file"), text)
                 else:
-                    var.library.build_dir_cache(bot)
+                    var.cache.build_dir_cache(bot)
                     cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cache=True)
 
         except re.error as e:
@@ -283,14 +324,17 @@ def cmd_play_url(bot, user, text, command, parameter):
     global log
 
     url = util.get_url_from_input(parameter)
-    music_wrapper = get_item_wrapper(bot, type='url', url=url, user=user)
-    var.playlist.append(music_wrapper)
-
-    log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
-    bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
-    if len(var.playlist) == 2:
-        # If I am the second item on the playlist. (I am the next one!)
-        bot.async_download_next()
+    if url:
+        music_wrapper = get_cached_wrapper_from_scrap(bot, type='url', url=url, user=user)
+        var.playlist.append(music_wrapper)
+
+        log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
+        bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
+        if len(var.playlist) == 2:
+            # If I am the second item on the playlist. (I am the next one!)
+            bot.async_download_next()
+    else:
+        bot.send_msg(constants.strings('bad_parameter', command=command))
 
 
 def cmd_play_playlist(bot, user, text, command, parameter):
@@ -307,7 +351,7 @@ def cmd_play_playlist(bot, user, text, command, parameter):
     items = get_playlist_info(url=url, start_index=offset, user=user)
     if len(items) > 0:
         items = var.playlist.extend(list(map(
-            lambda item: get_item_wrapper(bot, **item), items)))
+            lambda item: get_cached_wrapper_from_scrap(bot, **item), items)))
         for music in items:
             log.info("cmd: add to playlist: " + music.format_debug_string())
     else:
@@ -332,10 +376,11 @@ def cmd_play_radio(bot, user, text, command, parameter):
             parameter = parameter.split()[0]
         url = util.get_url_from_input(parameter)
         if url:
-            music_wrapper = get_item_wrapper(bot, type='radio', url=url)
+            music_wrapper = get_cached_wrapper_from_scrap(bot, type='radio', url=url, user=user)
 
             var.playlist.append(music_wrapper)
             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
+            bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
         else:
             bot.send_msg(constants.strings('bad_url'))
 
@@ -431,7 +476,7 @@ def cmd_rb_play(bot, user, text, command, parameter):
         url = radiobrowser.geturl_byid(parameter)
         if url != "-1":
             log.info('cmd: Found url: ' + url)
-            music_wrapper = get_item_wrapper(bot, type='radio', url=url, name=stationname)
+            music_wrapper = get_cached_wrapper_from_scrap(bot, type='radio', url=url, name=stationname, user=user)
             var.playlist.append(music_wrapper)
             log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
             bot.async_download_next()
@@ -440,11 +485,13 @@ def cmd_rb_play(bot, user, text, command, parameter):
             msg += "No playable url found for this station, please try another station."
             bot.send_msg(msg, text)
 
+
 yt_last_result = []
-yt_last_page = 0 # TODO: if we keep adding global variables, we need to consider sealing all commands up into classes.
+yt_last_page = 0  # TODO: if we keep adding global variables, we need to consider sealing all commands up into classes.
+
 
 def cmd_yt_search(bot, user, text, command, parameter):
-    global log, yt_last_result, yt_last_page
+    global log, yt_last_result, yt_last_page, song_shortlist
     item_per_page = 5
 
     if parameter:
@@ -452,6 +499,10 @@ def cmd_yt_search(bot, user, text, command, parameter):
         if parameter.startswith("-n"):
             yt_last_page += 1
             if len(yt_last_result) > yt_last_page * item_per_page:
+                song_shortlist = [{'type': 'url',
+                                   'url': "https://www.youtube.com/watch?v=" + result[0],
+                                   'title': result[1]
+                                   } for result in yt_last_result[yt_last_page * item_per_page: item_per_page]]
                 msg = _yt_format_result(yt_last_result, yt_last_page * item_per_page, item_per_page)
                 bot.send_msg(constants.strings('yt_result', result_table=msg), text)
             else:
@@ -463,6 +514,8 @@ def cmd_yt_search(bot, user, text, command, parameter):
             if results:
                 yt_last_result = results
                 yt_last_page = 0
+                song_shortlist = [{'type': 'url', 'url': "https://www.youtube.com/watch?v=" + result[0]}
+                                  for result in results[0: item_per_page]]
                 msg = _yt_format_result(results, 0, item_per_page)
                 bot.send_msg(constants.strings('yt_result', result_table=msg), text)
             else:
@@ -470,11 +523,12 @@ def cmd_yt_search(bot, user, text, command, parameter):
     else:
         bot.send_msg(constants.strings('bad_parameter', command=command), text)
 
+
 def _yt_format_result(results, start, count):
     msg = '<table><tr><th width="10%">Index</th><th>Title</th><th width="20%">Uploader</th></tr>'
     for index, item in enumerate(results[start:start+count]):
         msg += '<tr><td>{index:d}</td><td>{title}</td><td>{uploader}</td></tr>'.format(
-            index=index + 1 + start, title=item[1], uploader=item[2])
+            index=index + 1, title=item[1], uploader=item[2])
     msg += '</table>'
 
     return msg
@@ -484,25 +538,20 @@ def cmd_yt_play(bot, user, text, command, parameter):
     global log, yt_last_result, yt_last_page
 
     if parameter:
-        if parameter.isdigit() and 0 <= int(parameter) - 1 < len(yt_last_result):
-            url = "https://www.youtube.com/watch?v=" + yt_last_result[int(parameter) - 1][0]
+        results = util.youtube_search(parameter)
+        if results:
+            yt_last_result = results
+            yt_last_page = 0
+            url = "https://www.youtube.com/watch?v=" + yt_last_result[0][0]
             cmd_play_url(bot, user, text, command, url)
         else:
-            results = util.youtube_search(parameter)
-            if results:
-                yt_last_result = results
-                yt_last_page = 0
-                url = "https://www.youtube.com/watch?v=" + yt_last_result[0][0]
-                cmd_play_url(bot, user, text, command, url)
-            else:
-                bot.send_msg(constants.strings('yt_query_error'))
+            bot.send_msg(constants.strings('yt_query_error'))
     else:
         bot.send_msg(constants.strings('bad_parameter', command=command), text)
 
 
 def cmd_help(bot, user, text, command, parameter):
     global log
-
     bot.send_msg(constants.strings('help'), text)
     if bot.is_admin(user):
         bot.send_msg(constants.strings('admin_help'), text)
@@ -550,6 +599,9 @@ def cmd_stop_and_getout(bot, user, text, command, parameter):
     global log
 
     bot.stop()
+    if var.playlist.mode == "one-shot":
+        var.playlist.clear()
+
     if bot.channel:
         bot.mumble.channels.find_by_name(bot.channel).move_in()
 
@@ -561,7 +613,7 @@ def cmd_volume(bot, user, text, command, parameter):
     if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100:
         bot.volume_set = float(float(parameter) / 100)
         bot.send_msg(constants.strings('change_volume',
-            volume=int(bot.volume_set * 100), user=bot.mumble.users[text.actor]['name']), text)
+                     volume=int(bot.volume_set * 100), user=bot.mumble.users[text.actor]['name']))
         var.db.set('bot', 'volume', str(bot.volume_set))
         log.info('cmd: volume set to %d' % (bot.volume_set * 100))
     else:
@@ -611,7 +663,7 @@ def cmd_ducking_volume(bot, user, text, command, parameter):
     if parameter and parameter.isdigit() and 0 <= int(parameter) <= 100:
         bot.ducking_volume = float(float(parameter) / 100)
         bot.send_msg(constants.strings('change_ducking_volume',
-            volume=int(bot.ducking_volume * 100), user=bot.mumble.users[text.actor]['name']), text)
+                     volume=int(bot.ducking_volume * 100), user=bot.mumble.users[text.actor]['name']), text)
         # var.db.set('bot', 'volume', str(bot.volume_set))
         var.db.set('bot', 'ducking_volume', str(bot.ducking_volume))
         log.info('cmd: volume on ducking set to %d' % (bot.ducking_volume * 100))
@@ -622,12 +674,10 @@ def cmd_ducking_volume(bot, user, text, command, parameter):
 def cmd_current_music(bot, user, text, command, parameter):
     global log
 
-    reply = ""
     if len(var.playlist) > 0:
-        bot.send_msg(var.playlist.current_item().format_current_playing())
+        bot.send_msg(var.playlist.current_item().format_current_playing(), text)
     else:
-        reply = constants.strings('not_playing')
-    bot.send_msg(reply, text)
+        bot.send_msg(constants.strings('not_playing'), text)
 
 
 def cmd_skip(bot, user, text, command, parameter):
@@ -644,7 +694,7 @@ def cmd_last(bot, user, text, command, parameter):
 
     if len(var.playlist) > 0:
         bot.interrupt()
-        var.playlist.point_to(len(var.playlist) - 1)
+        var.playlist.point_to(len(var.playlist) - 1 - 1)
     else:
         bot.send_msg(constants.strings('queue_empty'), text)
 
@@ -653,14 +703,17 @@ def cmd_remove(bot, user, text, command, parameter):
     global log
 
     # Allow to remove specific music into the queue with a number
-    if parameter and parameter.isdigit() and int(parameter) > 0 \
-            and int(parameter) <= len(var.playlist):
+    if parameter and parameter.isdigit() and 0 < int(parameter) <= len(var.playlist):
 
         index = int(parameter) - 1
 
-        removed = None
         if index == var.playlist.current_index:
-            removed = var.playlist.remove(index)
+            removed = var.playlist[index]
+            bot.send_msg(constants.strings('removing_item',
+                                           item=removed.format_short_string()), text)
+            log.info("cmd: delete from playlist: " + removed.format_debug_string())
+
+            var.playlist.remove(index)
 
             if index < len(var.playlist):
                 if not bot.is_pause:
@@ -668,26 +721,22 @@ def cmd_remove(bot, user, text, command, parameter):
                     var.playlist.current_index -= 1
                     # then the bot will move to next item
 
-            else: # if item deleted is the last item of the queue
+            else:  # if item deleted is the last item of the queue
                 var.playlist.current_index -= 1
                 if not bot.is_pause:
                     bot.interrupt()
         else:
-            removed = var.playlist.remove(index)
+            var.playlist.remove(index)
 
-        bot.send_msg(constants.strings('removing_item',
-            item=removed.format_short_string()), text)
-
-        log.info("cmd: delete from playlist: " + removed.format_debug_string())
     else:
-        bot.send_msg(constants.strings('bad_parameter', command=command))
+        bot.send_msg(constants.strings('bad_parameter', command=command), text)
 
 
 def cmd_list_file(bot, user, text, command, parameter):
     global log
 
-    files = var.library.files
-    msgs = [ "<br> <b>Files available:</b>" if not parameter else "<br> <b>Matched files:</b>" ]
+    files = var.cache.files
+    msgs = [constants.strings("multiple_file_found")]
     try:
         count = 0
         for index, file in enumerate(files):
@@ -716,26 +765,30 @@ def cmd_queue(bot, user, text, command, parameter):
         msg = constants.strings('queue_empty')
         bot.send_msg(msg, text)
     else:
-        msgs = [ constants.strings('queue_contents')]
+        msgs = [constants.strings('queue_contents')]
         for i, music in enumerate(var.playlist):
-            newline = ''
+            tags = ''
+            if len(music.item().tags) > 0:
+                tags = "<sup>{}</sup>".format(", ".join(music.item().tags))
             if i == var.playlist.current_index:
-                newline = "<b style='color:orange'>{} ({}) {} </b>".format(i + 1, music.display_type(),
-                                                           music.format_short_string())
+                newline = "<b style='color:orange'>{} ({}) {} </b> {}".format(i + 1, music.display_type(),
+                                                                              music.format_short_string(), tags)
             else:
-                newline = '<b>{}</b> ({}) {}'.format(i + 1, music.display_type(),
-                                                           music.format_short_string())
+                newline = '<b>{}</b> ({}) {} {}'.format(i + 1, music.display_type(),
+                                                        music.format_short_string(), tags)
 
             msgs.append(newline)
 
         send_multi_lines(bot, msgs, text)
 
+
 def cmd_random(bot, user, text, command, parameter):
     global log
 
     bot.interrupt()
     var.playlist.randomize()
 
+
 def cmd_repeat(bot, user, text, command, parameter):
     global log
 
@@ -749,17 +802,18 @@ def cmd_repeat(bot, user, text, command, parameter):
             var.playlist.current_index + 1,
             music
         )
-        log.info("bot: add to playlist: " + music.format_debug_string)
+        log.info("bot: add to playlist: " + music.format_debug_string())
 
     bot.send_msg(constants.strings("repeat", song=music.format_song_string(), n=str(repeat)), text)
 
+
 def cmd_mode(bot, user, text, command, parameter):
     global log
 
     if not parameter:
         bot.send_msg(constants.strings("current_mode", mode=var.playlist.mode), text)
         return
-    if not parameter in ["one-shot", "repeat", "random", "autoplay"]:
+    if parameter not in ["one-shot", "repeat", "random", "autoplay"]:
         bot.send_msg(constants.strings('unknown_mode', mode=parameter), text)
     else:
         var.db.set('playlist', 'playback_mode', parameter)
@@ -771,29 +825,301 @@ def cmd_mode(bot, user, text, command, parameter):
             bot.interrupt()
             bot.launch_music()
 
+
+def cmd_play_tags(bot, user, text, command, parameter):
+    if not parameter:
+        bot.send_msg(constants.strings('bad_parameter', command=command), text)
+        return
+
+    msgs = [constants.strings('multiple_file_added') + "<ul>"]
+    count = 0
+
+    tags = parameter.split(",")
+    tags = list(map(lambda t: t.strip(), tags))
+    music_wrappers = get_cached_wrappers_by_tags(bot, tags, user)
+    for music_wrapper in music_wrappers:
+        count += 1
+        log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
+        msgs.append("<li><b>{}</b> (<i>{}</i>)</li>".format(music_wrapper.item().title, ", ".join(music_wrapper.item().tags)))
+
+    if count != 0:
+        msgs.append("</ul>")
+        var.playlist.extend(music_wrappers)
+        send_multi_lines(bot, msgs, None, "")
+    else:
+        bot.send_msg(constants.strings("no_file"), text)
+
+
+def cmd_add_tag(bot, user, text, command, parameter):
+    global log
+
+    params = parameter.split()
+    if len(params) == 2:
+        index = params[0]
+        tags = list(map(lambda t: t.strip(), params[1].split(",")))
+    elif len(params) == 1:
+        index = str(var.playlist.current_index + 1)
+        tags = list(map(lambda t: t.strip(), params[0].split(",")))
+    else:
+        bot.send_msg(constants.strings('bad_parameter', command=command), text)
+        return
+
+    if tags[0]:
+        if index.isdigit() and 1 <= int(index) <= len(var.playlist):
+            var.playlist[int(index) - 1].add_tags(tags)
+            log.info("cmd: add tags %s to song %s" % (", ".join(tags),
+                                                      var.playlist[int(index) - 1].format_debug_string()))
+            bot.send_msg(constants.strings("added_tags",
+                                           tags=", ".join(tags),
+                                           song=var.playlist[int(index) - 1].format_short_string()), text)
+            return
+
+        elif index == "*":
+            for item in var.playlist:
+                item.add_tags(tags)
+                log.info("cmd: add tags %s to song %s" % (", ".join(tags),
+                                                          item.format_debug_string()))
+            bot.send_msg(constants.strings("added_tags_to_all", tags=", ".join(tags)), text)
+            return
+
+    bot.send_msg(constants.strings('bad_parameter', command=command), text)
+
+
+def cmd_remove_tag(bot, user, text, command, parameter):
+    global log
+
+    params = parameter.split()
+
+    if len(params) == 2:
+        index = params[0]
+        tags = list(map(lambda t: t.strip(), params[1].split(",")))
+    elif len(params) == 1:
+        index = str(var.playlist.current_index + 1)
+        tags = list(map(lambda t: t.strip(), params[0].split(",")))
+    else:
+        bot.send_msg(constants.strings('bad_parameter', command=command), text)
+        return
+
+    if tags[0]:
+        if index.isdigit() and 1 <= int(index) <= len(var.playlist):
+            if tags[0] != "*":
+                var.playlist[int(index) - 1].remove_tags(tags)
+                log.info("cmd: remove tags %s from song %s" % (", ".join(tags),
+                                                               var.playlist[int(index) - 1].format_debug_string()))
+                bot.send_msg(constants.strings("removed_tags",
+                                               tags=", ".join(tags),
+                                               song=var.playlist[int(index) - 1].format_short_string()), text)
+                return
+            else:
+                var.playlist[int(index) - 1].clear_tags()
+                log.info("cmd: clear tags from song %s" % (var.playlist[int(index) - 1].format_debug_string()))
+                bot.send_msg(constants.strings("cleared_tags",
+                                               song=var.playlist[int(index) - 1].format_short_string()), text)
+                return
+
+        elif index == "*":
+            if tags[0] != "*":
+                for item in var.playlist:
+                    item.remove_tags(tags)
+                    log.info("cmd: remove tags %s from song %s" % (", ".join(tags),
+                                                                   item.format_debug_string()))
+                bot.send_msg(constants.strings("removed_tags_from_all", tags=", ".join(tags)), text)
+                return
+            else:
+                for item in var.playlist:
+                    item.clear_tags()
+                    log.info("cmd: clear tags from song %s" % (item.format_debug_string()))
+                bot.send_msg(constants.strings("cleared_tags_from_all"), text)
+                return
+
+    bot.send_msg(constants.strings('bad_parameter', command=command), text)
+
+
+def cmd_find_tagged(bot, user, text, command, parameter):
+    global song_shortlist
+
+    if not parameter:
+        bot.send_msg(constants.strings('bad_parameter', command=command), text)
+        return
+
+    msgs = [constants.strings('multiple_file_found') + "<ul>"]
+    count = 0
+
+    tags = parameter.split(",")
+    tags = list(map(lambda t: t.strip(), tags))
+
+    music_dicts = var.music_db.query_music_by_tags(tags)
+    song_shortlist = music_dicts
+    items = dicts_to_items(bot, music_dicts)
+
+    for i, item in enumerate(items):
+        count += 1
+        msgs.append("<li><b>{:d}</b> - <b>{}</b> (<i>{}</i>)</li>".format(i+1, item.title, ", ".join(item.tags)))
+
+    if count != 0:
+        msgs.append("</ul>")
+        msgs.append(constants.strings("shortlist_instruction"))
+        send_multi_lines(bot, msgs, text, "")
+    else:
+        bot.send_msg(constants.strings("no_file"), text)
+
+
+def cmd_search_library(bot, user, text, command, parameter):
+    global song_shortlist
+    if not parameter:
+        bot.send_msg(constants.strings('bad_parameter', command=command), text)
+        return
+
+    msgs = [constants.strings('multiple_file_found') + "<ul>"]
+    count = 0
+
+    _keywords = parameter.split(" ")
+    keywords = []
+    for kw in _keywords:
+        if kw:
+            keywords.append(kw)
+
+    music_dicts = var.music_db.query_music_by_keywords(keywords)
+    if music_dicts:
+        items = dicts_to_items(bot, music_dicts)
+        song_shortlist = music_dicts
+
+        for item in items:
+            count += 1
+            if len(item.tags) > 0:
+                msgs.append("<li><b>{:d}</b> - [{}] <b>{}</b> (<i>{}</i>)</li>".format(count, item.display_type(), item.title, ", ".join(item.tags)))
+            else:
+                msgs.append("<li><b>{:d}</b> - [{}] <b>{}</b> </li>".format(count, item.display_type(), item.title, ", ".join(item.tags)))
+
+        if count != 0:
+            msgs.append("</ul>")
+            msgs.append(constants.strings("shortlist_instruction"))
+            send_multi_lines(bot, msgs, text, "")
+        else:
+            bot.send_msg(constants.strings("no_file"), text)
+    else:
+        bot.send_msg(constants.strings("no_file"), text)
+
+
+def cmd_shortlist(bot, user, text, command, parameter):
+    global song_shortlist, log
+    try:
+        indexes = [int(i) for i in parameter.split(" ")]
+    except ValueError:
+        bot.send_msg(constants.strings('bad_parameter', command=command), text)
+        return
+
+    if len(indexes) > 1:
+        msgs = [constants.strings('multiple_file_added') + "<ul>"]
+        for index in indexes:
+            if 1 <= index <= len(song_shortlist):
+                kwargs = song_shortlist[index - 1]
+                kwargs['user'] = user
+                music_wrapper = get_cached_wrapper_from_scrap(bot, **kwargs)
+                var.playlist.append(music_wrapper)
+                log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
+                msgs.append("<li>[{}] <b>{}</b></li>".format(music_wrapper.item().type, music_wrapper.item().title))
+            else:
+                bot.send_msg(constants.strings('bad_parameter', command=command), text)
+                return
+
+        msgs.append("</ul>")
+        send_multi_lines(bot, msgs, None, "")
+        return
+    elif len(indexes) == 1:
+        index = indexes[0]
+        if 1 <= index <= len(song_shortlist):
+            kwargs = song_shortlist[index - 1]
+            kwargs['user'] = user
+            music_wrapper = get_cached_wrapper_from_scrap(bot, **kwargs)
+            var.playlist.append(music_wrapper)
+            log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
+            bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
+            return
+
+    bot.send_msg(constants.strings('bad_parameter', command=command), text)
+
+
+def cmd_delete_from_library(bot, user, text, command, parameter):
+    global song_shortlist, log
+    try:
+        indexes = [int(i) for i in parameter.split(" ")]
+    except ValueError:
+        bot.send_msg(constants.strings('bad_parameter', command=command), text)
+        return
+
+    if len(indexes) > 1:
+        msgs = [constants.strings('multiple_file_added') + "<ul>"]
+        count = 0
+        for index in indexes:
+            if 1 <= index <= len(song_shortlist):
+                music_dict = song_shortlist[index - 1]
+                if 'id' in music_dict:
+                    music_wrapper = get_cached_wrapper_by_id(bot, music_dict['id'], user)
+                    log.info("cmd: remove from library: " + music_wrapper.format_debug_string())
+                    msgs.append("<li>[{}] <b>{}</b></li>".format(music_wrapper.item().type, music_wrapper.item().title))
+                    var.playlist.remove_by_id(music_dict['id'])
+                    var.cache.free_and_delete(music_dict['id'])
+                    count += 1
+            else:
+                bot.send_msg(constants.strings('bad_parameter', command=command), text)
+                return
+
+        if count == 0:
+            bot.send_msg(constants.strings('bad_parameter', command=command), text)
+            return
+
+        msgs.append("</ul>")
+        send_multi_lines(bot, msgs, None, "")
+        return
+    elif len(indexes) == 1:
+        index = indexes[0]
+        if 1 <= index <= len(song_shortlist):
+            music_dict = song_shortlist[index - 1]
+            if 'id' in music_dict:
+                music_wrapper = get_cached_wrapper_by_id(bot, music_dict['id'], user)
+                bot.send_msg(constants.strings('file_deleted', item=music_wrapper.format_song_string()), text)
+                log.info("cmd: remove from library: " + music_wrapper.format_debug_string())
+                var.playlist.remove_by_id(music_dict['id'])
+                var.cache.free_and_delete(music_dict['id'])
+                return
+
+    bot.send_msg(constants.strings('bad_parameter', command=command), text)
+
+
 def cmd_drop_database(bot, user, text, command, parameter):
     global log
 
-    var.db.drop_table()
-    var.db = SettingsDatabase(var.dbfile)
-    var.music_db.drop_table()
-    var.music_db = MusicDatabase(var.dbfile)
-    log.info("command: database dropped.")
-    bot.send_msg(constants.strings('database_dropped'), text)
+    if bot.is_admin(user):
+        var.db.drop_table()
+        var.db = SettingsDatabase(var.dbfile)
+        var.music_db.drop_table()
+        var.music_db = MusicDatabase(var.dbfile)
+        log.info("command: database dropped.")
+        bot.send_msg(constants.strings('database_dropped'), text)
+    else:
+        bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
+
 
 def cmd_refresh_cache(bot, user, text, command, parameter):
     global log
-    var.library.build_dir_cache(bot)
-    log.info("command: cache refreshed.")
-    bot.send_msg(constants.strings('cache_refreshed'), text)
+    if bot.is_admin(user):
+        var.cache.build_dir_cache(bot)
+        log.info("command: Local file cache refreshed.")
+        bot.send_msg(constants.strings('cache_refreshed'), text)
+    else:
+        bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
+
 
 # Just for debug use
 def cmd_real_time_rms(bot, user, text, command, parameter):
     bot._display_rms = not bot._display_rms
 
+
 def cmd_loop_state(bot, user, text, command, parameter):
     print(bot._loop_status)
 
+
 def cmd_item(bot, user, text, command, parameter):
     print(bot.wait_for_downloading)
     print(var.playlist.current_item().to_dict())