diff --git a/command.py b/command.py
index 305edba..da821db 100644
--- a/command.py
+++ b/command.py
@@ -9,8 +9,8 @@ import media.system
import util
import variables as var
from librb import radiobrowser
-from database import SettingsDatabase
-from media.playlist import get_item_wrapper
+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
@@ -54,21 +54,22 @@ 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)
+ bot.register_command(constants.commands('drop_database'), cmd_drop_database, True)
+ bot.register_command(constants.commands('recache'), cmd_refresh_cache, True)
# Just for debug use
- bot.register_command('rtrms', cmd_real_time_rms)
- bot.register_command('loop', cmd_loop_state)
- bot.register_command('item', cmd_item)
+ 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):
+def send_multi_lines(bot, lines, text, linebreak="
"):
global log
msg = ""
br = ""
for newline in lines:
msg += br
- br = "
"
+ br = linebreak
if (len(msg) + len(newline)) > (bot.mumble.get_max_message_length() - 4) != 0: # 4 == len("
")
bot.send_msg(msg, text)
msg = ""
@@ -163,106 +164,113 @@ def cmd_pause(bot, user, text, command, parameter):
bot.send_msg(constants.strings('paused'))
-def cmd_play_file(bot, user, text, command, parameter):
+def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=False):
global log
# if parameter is {index}
if parameter.isdigit():
- files = util.get_recursive_file_list_sorted(var.music_folder)
+ files = var.library.files
if int(parameter) < len(files):
- filename = files[int(parameter)].replace(var.music_folder, '')
- music_wrapper = get_item_wrapper(bot, type='file', path=filename, user=user)
+ music_wrapper = get_item_wrapper_by_id(bot, var.library.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(user)), text)
+ bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
+ return
# if parameter is {path}
else:
# sanitize "../" and so on
- path = os.path.abspath(os.path.join(var.music_folder, parameter))
- if not path.startswith(os.path.abspath(var.music_folder)):
- bot.send_msg(constants.strings('no_file'), text)
- return
+ # path = os.path.abspath(os.path.join(var.music_folder, parameter))
+ # if not path.startswith(os.path.abspath(var.music_folder)):
+ # bot.send_msg(constants.strings('no_file'), text)
+ # return
- if os.path.isfile(path):
+ if parameter in var.library.files:
music_wrapper = get_item_wrapper(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(user)), text)
+ bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
return
# if parameter is {folder}
- elif os.path.isdir(path):
- if parameter != '.' and parameter != './':
- if not parameter.endswith("/"):
- parameter += "/"
- else:
- parameter = ""
-
- files = util.get_recursive_file_list_sorted(var.music_folder)
- music_library = util.Dir(var.music_folder)
- for file in files:
- music_library.add_file(file)
-
- files = music_library.get_files(parameter)
+ files = var.library.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(bot, type='file', path=file, user=user)
+ music_wrapper = get_item_wrapper_by_id(bot, var.library.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)
- else:
- bot.send_msg(constants.strings('no_file'), text)
+ return
else:
# try to do a partial match
- files = util.get_recursive_file_list_sorted(var.music_folder)
+ files = var.library.files
matches = [(index, file) for index, file in enumerate(files) if parameter.lower() in file.lower()]
- if len(matches) == 0:
- bot.send_msg(constants.strings('no_file'), text)
- elif len(matches) == 1:
+ if len(matches) == 1:
file = matches[0][1]
- music_wrapper = get_item_wrapper(bot, type='file', path=file, user=user)
+ music_wrapper = get_item_wrapper_by_id(bot, var.library.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(user)), text)
- else:
- msgs = [ constants.strings('multiple_matches')]
+ bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()), text)
+ return
+ elif len(matches) > 1:
+ msgs = [ constants.strings('multiple_matches') ]
for match in matches:
- msgs.append("{:0>3d} - {:s}".format(match[0], match[1]))
+ music_wrapper = get_item_wrapper_by_id(bot, var.library.file_id_lookup[match[1]], user)
+ msgs.append("{:0>3d} - {:s} ({:s})".format(
+ match[0], music_wrapper.item().title, match[1]))
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)
+ cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=True)
-def cmd_play_file_match(bot, user, text, command, parameter):
+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 = util.get_recursive_file_list_sorted(music_folder)
- msgs = [ constants.strings('multiple_file_added')]
+ files = var.library.files
+ msgs = [ constants.strings('multiple_file_added') + "
"]
count = 0
try:
music_wrappers = []
for file in files:
match = re.search(parameter, file)
- if match:
+ if match and match[0]:
count += 1
- music_wrapper = get_item_wrapper(bot, type='file', path=file, user=user)
+ music_wrapper = get_item_wrapper_by_id(bot, var.library.file_id_lookup[file], user)
music_wrappers.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))
+ msgs.append("- {} ({})
".format(music_wrapper.item().title,
+ file[:match.span()[0]]
+ + ""
+ + file[match.span()[0]: match.span()[1]]
+ + ""
+ + file[match.span()[1]:]
+ ))
if count != 0:
+ msgs.append("
")
var.playlist.extend(music_wrappers)
- send_multi_lines(bot, msgs, text)
+ send_multi_lines(bot, msgs, text, "")
else:
- bot.send_msg(constants.strings('no_file'), text)
+ if do_not_refresh_cache:
+ bot.send_msg(constants.strings("no_file"), text)
+ else:
+ var.library.build_dir_cache(bot)
+ cmd_play_file_match(bot, user, text, command, parameter, do_not_refresh_cache=True)
except re.error as e:
msg = constants.strings('wrong_pattern', error=str(e))
@@ -678,9 +686,7 @@ def cmd_remove(bot, user, text, command, parameter):
def cmd_list_file(bot, user, text, command, parameter):
global log
- folder_path = var.music_folder
-
- files = util.get_recursive_file_list_sorted(folder_path)
+ files = var.library.files
msgs = [ "
Files available:" if not parameter else "
Matched files:" ]
try:
count = 0
@@ -714,7 +720,7 @@ def cmd_queue(bot, user, text, command, parameter):
for i, music in enumerate(var.playlist):
newline = ''
if i == var.playlist.current_index:
- newline = '{} ▶ ({}) {} ◀'.format(i + 1, music.display_type(),
+ newline = "{} ({}) {} ".format(i + 1, music.display_type(),
music.format_short_string())
else:
newline = '{} ({}) {}'.format(i + 1, music.display_type(),
@@ -745,7 +751,7 @@ def cmd_repeat(bot, user, text, command, parameter):
)
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)
+ 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
@@ -770,8 +776,17 @@ def cmd_drop_database(bot, user, text, command, parameter):
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)
+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)
+
# Just for debug use
def cmd_real_time_rms(bot, user, text, command, parameter):
bot._display_rms = not bot._display_rms
diff --git a/configuration.default.ini b/configuration.default.ini
index 1d102ec..2feaef3 100644
--- a/configuration.default.ini
+++ b/configuration.default.ini
@@ -61,6 +61,13 @@ announce_current_music = True
allow_other_channel_message = False
allow_private_message = True
+# 'save_music_library': If this is set True, the bot will save the metadata of music into the database.
+save_music_library = True
+
+# 'refresh_cache_on_startup': If this is set true, the bot will refresh its music directory cache when starting up.
+# But it won't reload metadata from each files. If set to False, it will used the cache last time.
+refresh_cache_on_startup = True
+
# If save_playlist is set True, the bot will save current
# playlist before quitting and reload it the next time it start.
save_playlist = True
@@ -159,6 +166,7 @@ ducking_threshold = duckthres
ducking_volume = duckv
drop_database = dropdatabase
+recache = recache
[strings]
current_volume = Current volume: {volume}.
@@ -178,19 +186,19 @@ bad_url = Bad URL requested.
preconfigurated_radio = Preconfigurated Radio available:
unable_download = Error while downloading music...
which_command = Do you mean
{commands}
-multiple_matches = Track not found! Possible candidates:
+multiple_matches = File not found! Possible candidates:
queue_contents = Items on the playlist:
queue_empty = Playlist is empty!
invalid_index = Invalid index {index}. Use '!queue' to see your playlist.
-now_playing = Playing
{item}
+now_playing = Playing {item}
radio = Radio
file = File
url_from_playlist = URL
url = URL
-radio_item = {title} from {name} added by {user}
-file_item = {artist} - {title} added by {user}
-url_from_playlist_item = {title} from playlist {playlist} added by {user}
-url_item = {title} added by {user}
+radio_item = {title} from {name} added by {user}
+file_item = {artist} - {title} added by {user}
+url_from_playlist_item = {title} from playlist {playlist} added by {user}
+url_item = {title} added by {user}
not_in_my_channel = You're not in my channel, command refused!
pm_not_allowed = Private message aren't allowed.
too_long = {song} is too long, removed from playlist!
@@ -216,6 +224,7 @@ yt_result = Youtube query result: {result_table} Use !ytplay {{index}} to
yt_no_more = No more results!
yt_query_error = Unable to query youtube!
playlist_fetching_failed = Unable to fetch the playlist!
+cache_refreshed = Cache refreshed!
help = Commands
Control
@@ -266,7 +275,9 @@ admin_help = Admin command
!userunban {user} - unban a user
!urlban {url} - ban an url
!urlunban {url} - unban an url
- !dropdatabase - clear the entire database, YOU SHOULD KNOW WHAT YOU ARE DOING.
+ !urlunban {url} - unban an url
+ !recache {url} - rebuild local music file cache
+ !dropdatabase - clear the entire database, you will lose all settings and music library.
diff --git a/configuration.example.ini b/configuration.example.ini
index edccee7..9b45300 100644
--- a/configuration.example.ini
+++ b/configuration.example.ini
@@ -74,6 +74,10 @@ port = 64738
# 'save_music_library': If this is set True, the bot will save the metadata of music into the database.
#save_music_library = True
+# 'refresh_cache_on_startup': If this is set true, the bot will refresh its music directory cache when starting up.
+# But it won't reload metadata from each files. If set to False, it will used the cache last time.
+#refresh_cache_on_startup = True
+
# 'save_playlist': If save_playlist is set True, the bot will save current playlist before quitting
# and reload it the next time it start. It requires save_music_library to be True to function.
#save_playlist = True
diff --git a/database.py b/database.py
index 13f5ef9..2352f90 100644
--- a/database.py
+++ b/database.py
@@ -238,3 +238,10 @@ class MusicDatabase:
"WHERE %s" % condition_str, filler)
conn.commit()
conn.close()
+
+
+ def drop_table(self):
+ conn = sqlite3.connect(self.db_path)
+ cursor = conn.cursor()
+ cursor.execute("DROP TABLE music")
+ conn.close()
diff --git a/interface.py b/interface.py
index eed0f6e..36b811c 100644
--- a/interface.py
+++ b/interface.py
@@ -4,22 +4,15 @@ from functools import wraps
from flask import Flask, render_template, request, redirect, send_file, Response, jsonify, abort
import variables as var
import util
-from datetime import datetime
import os
import os.path
import shutil
-import random
from werkzeug.utils import secure_filename
import errno
import media
-from media.playlist import get_item_wrapper
-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.playlist import get_item_wrapper, get_item_wrapper_by_id
import logging
import time
-import constants
class ReverseProxied(object):
@@ -101,16 +94,9 @@ def requires_auth(f):
@web.route("/", methods=['GET'])
@requires_auth
def index():
- folder_path = var.music_folder
- files = util.get_recursive_file_list_sorted(var.music_folder)
- music_library = util.Dir(folder_path)
- for file in files:
- music_library.add_file(file)
-
-
return render_template('index.html',
- all_files=files,
- music_library=music_library,
+ all_files=var.library.files,
+ music_library=var.library.dir,
os=os,
playlist=var.playlist,
user=var.user,
@@ -157,14 +143,13 @@ def status():
def post():
global log
- folder_path = var.music_folder
if request.method == 'POST':
if request.form:
log.debug("web: Post request from %s: %s" % ( request.remote_addr, str(request.form)))
if 'add_file_bottom' in request.form and ".." not in request.form['add_file_bottom']:
path = var.music_folder + request.form['add_file_bottom']
if os.path.isfile(path):
- music_wrapper = get_item_wrapper(var.bot, type='file', path=request.form['add_file_bottom'], user=user)
+ music_wrapper = get_item_wrapper_by_id(var.bot, var.library.file_id_lookup[request.form['add_file_bottom']], user)
var.playlist.append(music_wrapper)
log.info('web: add to playlist(bottom): ' + music_wrapper.format_debug_string())
@@ -172,7 +157,7 @@ def post():
elif 'add_file_next' in request.form and ".." not in request.form['add_file_next']:
path = var.music_folder + request.form['add_file_next']
if os.path.isfile(path):
- music_wrapper = get_item_wrapper(var.bot, type='file', path=request.form['add_file_next'], user=user)
+ music_wrapper = get_item_wrapper_by_id(var.bot, var.library.file_id_lookup[request.form['add_file_next']], user)
var.playlist.insert(var.playlist.current_index + 1, music_wrapper)
log.info('web: add to playlist(next): ' + music_wrapper.format_debug_string())
@@ -186,19 +171,15 @@ def post():
folder += '/'
if os.path.isdir(var.music_folder + folder):
-
- files = util.get_recursive_file_list_sorted(var.music_folder)
- music_library = util.Dir(folder_path)
- for file in files:
- music_library.add_file(file)
-
+ dir = var.library.dir
if 'add_folder_recursively' in request.form:
- files = music_library.get_files_recursively(folder)
+ files = dir.get_files_recursively(folder)
else:
- files = music_library.get_files(folder)
+ files = dir.get_files(folder)
music_wrappers = list(map(
- lambda file: get_item_wrapper(var.bot, type='file', path=file, user=user),
+ lambda file:
+ get_item_wrapper_by_id(var.bot, var.library.file_id_lookup[folder + file], user),
files))
var.playlist.extend(music_wrappers)
@@ -370,7 +351,7 @@ def download():
log.info('web: Download of file %s requested from %s:' % (requested_file, request.remote_addr))
if '../' not in requested_file:
folder_path = var.music_folder
- files = util.get_recursive_file_list_sorted(var.music_folder)
+ files = var.library.files
if requested_file in files:
filepath = os.path.join(folder_path, requested_file)
diff --git a/media/library.py b/media/library.py
index 48049ce..33c1c05 100644
--- a/media/library.py
+++ b/media/library.py
@@ -1,13 +1,11 @@
import logging
-
from database import MusicDatabase
+import json
+
from media.item import item_builders, item_loaders, item_id_generators
-from media.file import FileItem
-from media.url import URLItem
-from media.url_from_playlist import PlaylistURLItem
-from media.radio import RadioItem
from database import MusicDatabase
import variables as var
+import util
class MusicLibrary(dict):
@@ -15,8 +13,10 @@ class MusicLibrary(dict):
super().__init__()
self.db = db
self.log = logging.getLogger("bot")
+ self.dir = None
+ self.files = []
- def get_item_by_id(self, bot, id):
+ def get_item_by_id(self, bot, id): # Why all these functions need a bot? Because it need the bot to send message!
if id in self:
return self[id]
@@ -26,6 +26,9 @@ class MusicLibrary(dict):
self[id] = item
self.log.debug("library: music found in database: %s" % item.format_debug_string())
return item
+ else:
+ raise KeyError("Unable to fetch item from the database! Please try to refresh the cache by !recache.")
+
def get_item(self, bot, **kwargs):
# kwargs should provide type and id, and parameters to build the item if not existed in the library.
@@ -59,8 +62,13 @@ class MusicLibrary(dict):
self.log.debug("library: music save into database: %s" % self[id].format_debug_string())
self.db.insert_music(self[id].to_dict())
- def delete(self, id):
- self.db.delete_music(id=id)
+ def delete(self, item):
+ if item.type == 'file' and item.path in self.file_id_lookup:
+ del self.file_id_lookup[item.path]
+ self.files.remove(item.path)
+ self.save_dir_cache()
+
+ self.db.delete_music(id=item.id)
def free(self, id):
if id in self:
@@ -68,3 +76,31 @@ class MusicLibrary(dict):
def free_all(self):
self.clear()
+
+ def build_dir_cache(self, bot):
+ self.log.info("library: rebuild directory cache")
+ self.files = []
+ self.file_id_lookup = {}
+ files = util.get_recursive_file_list_sorted(var.music_folder)
+ self.dir = util.Dir(var.music_folder)
+ for file in files:
+ item = self.get_item(bot, type='file', path=file)
+ if item.validate():
+ self.dir.add_file(file)
+ self.files.append(file)
+ self.file_id_lookup[file] = item.id
+
+ self.save_dir_cache()
+
+ def save_dir_cache(self):
+ var.db.set("dir_cache", "files", json.dumps(self.file_id_lookup))
+
+ def load_dir_cache(self, bot):
+ self.log.info("library: load directory cache from database")
+ loaded = json.loads(var.db.get("dir_cache", "files"))
+ self.files = loaded.keys()
+ self.file_id_lookup = loaded
+ self.dir = util.Dir(var.music_folder)
+ for file, id in loaded.items():
+ self.dir.add_file(file)
+
diff --git a/media/playlist.py b/media/playlist.py
index 6b8d430..3f34af5 100644
--- a/media/playlist.py
+++ b/media/playlist.py
@@ -298,6 +298,7 @@ class BasePlaylist(list):
if not item.validate() or item.is_failed():
self.log.debug("playlist: validating failed.")
self.remove_by_id(item.id)
+ var.library.delete(item.item())
self.log.debug("playlist: validating finished.")
self.validating_thread_lock.release()
@@ -422,6 +423,10 @@ class AutoPlaylist(BasePlaylist):
# self.refresh()
# return self
+ def clear(self):
+ super().clear()
+ self.refresh()
+
def next(self):
if len(self) == 0:
return False
diff --git a/mumbleBot.py b/mumbleBot.py
index 058c6cc..7433a48 100644
--- a/mumbleBot.py
+++ b/mumbleBot.py
@@ -184,12 +184,12 @@ class MumbleBot:
else:
self.log.debug("update: no new version found.")
- def register_command(self, cmd, handle):
+ def register_command(self, cmd, handle, no_partial_match=False):
cmds = cmd.split(",")
for command in cmds:
command = command.strip()
if command:
- self.cmd_handle[command] = handle
+ self.cmd_handle[command] = { 'handle': handle, 'partial_match': not no_partial_match}
self.log.debug("bot: command added: " + command)
def set_comment(self):
@@ -254,19 +254,19 @@ class MumbleBot:
try:
if command in self.cmd_handle:
command_exc = command
- self.cmd_handle[command](self, user, text, command, parameter)
+ self.cmd_handle[command]['handle'](self, user, text, command, parameter)
else:
# try partial match
cmds = self.cmd_handle.keys()
matches = []
for cmd in cmds:
- if cmd.startswith(command):
+ if cmd.startswith(command) and self.cmd_handle[cmd]['partial_match']:
matches.append(cmd)
if len(matches) == 1:
self.log.info("bot: {:s} matches {:s}".format(command, matches[0]))
command_exc = matches[0]
- self.cmd_handle[command_exc](self, user, text, command_exc, parameter)
+ self.cmd_handle[command_exc]['handle'](self, user, text, command_exc, parameter)
elif len(matches) > 1:
self.mumble.users[text.actor].send_text_message(
constants.strings('which_command', commands="
".join(matches)))
@@ -347,6 +347,7 @@ class MumbleBot:
break
else:
var.playlist.remove_by_id(next.id)
+ var.library.delete(next.item())
# =======================
@@ -406,6 +407,7 @@ class MumbleBot:
self.send_msg(constants.strings('download_in_progress', item=current.format_short_string()))
else:
var.playlist.remove_by_id(current.id)
+ var.library.delete(current.item())
else:
self._loop_status = 'Empty queue'
else:
@@ -654,6 +656,12 @@ if __name__ == '__main__':
var.bot = MumbleBot(args)
command.register_all_commands(var.bot)
+ if var.config.get("bot", "refresh_cache_on_startup", fallback=True)\
+ or not var.db.has_option("dir_cache", "files"):
+ var.library.build_dir_cache(var.bot)
+ else:
+ var.library.load_dir_cache(var.bot)
+
# load playlist
if var.config.getboolean('bot', 'save_playlist', fallback=True):
var.bot_logger.info("bot: load playlist from previous session")