feat: tag editing feature in web interface

This commit is contained in:
Terry Geng 2020-03-20 10:35:09 +08:00
parent 7f29deba01
commit 4e287d6b1b
4 changed files with 255 additions and 99 deletions

View File

@ -26,6 +26,7 @@ class Condition:
if self._order_by: if self._order_by:
sql += f" ORDEY BY {self._order_by}" sql += f" ORDEY BY {self._order_by}"
print(sql)
return sql return sql
def or_equal(self, column, equals_to, case_sensitive=True): def or_equal(self, column, equals_to, case_sensitive=True):
@ -330,7 +331,7 @@ class MusicDatabase:
return self._result_to_dict(results) return self._result_to_dict(results)
def query_music_by_id(self, _id): def query_music_by_id(self, _id):
return self.query_music(Condition().and_equal("id", _id)) return self.query_music(Condition().and_equal("id", _id))[0]
def query_music_by_keywords(self, keywords): def query_music_by_keywords(self, keywords):
condition = Condition() condition = Condition()

View File

@ -58,6 +58,7 @@ class ReverseProxied(object):
web = Flask(__name__) web = Flask(__name__)
web.config['TEMPLATES_AUTO_RELOAD'] = True
log = logging.getLogger("bot") log = logging.getLogger("bot")
user = 'Remote Control' user = 'Remote Control'
@ -361,20 +362,25 @@ def build_library_query_condition(form):
try: try:
condition = Condition() condition = Condition()
types = form['type'].split(",")
sub_cond = Condition()
for type in types:
sub_cond.or_equal("type", type)
condition.and_sub_condition(sub_cond)
if form['type'] == 'file': if form['type'] == 'file':
folder = form['dir'] folder = form['dir']
if not folder.endswith('/') and folder: if not folder.endswith('/') and folder:
folder += '/' folder += '/'
sub_cond = Condition() sub_cond = Condition()
count = 0
for file in var.cache.files: for file in var.cache.files:
if file.startswith(folder): if file.startswith(folder):
count += 1
sub_cond.or_equal("id", var.cache.file_id_lookup[file]) sub_cond.or_equal("id", var.cache.file_id_lookup[file])
if count > 900:
break
condition.and_sub_condition(sub_cond) condition.and_sub_condition(sub_cond)
condition.and_equal("type", "file")
elif form['type'] == 'url':
condition.and_equal("type", "url")
elif form['type'] == 'radio':
condition.and_equal("type", "radio")
tags = form['tags'].split(",") tags = form['tags'].split(",")
for tag in tags: for tag in tags:
@ -402,6 +408,7 @@ def library():
if request.form: if request.form:
log.debug("web: Post request from %s: %s" % (request.remote_addr, str(request.form))) log.debug("web: Post request from %s: %s" % (request.remote_addr, str(request.form)))
if request.form['action'] in ['add', 'query', 'delete']:
condition = build_library_query_condition(request.form) condition = build_library_query_condition(request.form)
total_count = var.music_db.query_music_count(condition) total_count = var.music_db.query_music_count(condition)
@ -419,7 +426,7 @@ def library():
condition.limit(ITEM_PER_PAGE) condition.limit(ITEM_PER_PAGE)
items = dicts_to_items(var.bot, var.music_db.query_music(condition)) items = dicts_to_items(var.bot, var.music_db.query_music(condition))
if 'action' in request.form and request.form['action'] == 'add': if request.form['action'] == 'add':
for item in items: for item in items:
music_wrapper = get_cached_wrapper(item, user) music_wrapper = get_cached_wrapper(item, user)
var.playlist.append(music_wrapper) var.playlist.append(music_wrapper)
@ -427,7 +434,7 @@ def library():
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string()) log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
return redirect("./", code=302) return redirect("./", code=302)
elif 'action' in request.form and request.form['action'] == 'delete': elif request.form['action'] == 'delete':
for item in items: for item in items:
var.playlist.remove_by_id(item.id) var.playlist.remove_by_id(item.id)
item = var.cache.get_item_by_id(var.bot, item.id) item = var.cache.get_item_by_id(var.bot, item.id)
@ -469,6 +476,12 @@ def library():
'total_pages': page_count, 'total_pages': page_count,
'active_page': current_page 'active_page': current_page
}) })
elif request.form['action'] == 'edit_tags':
item = var.music_db.query_music_by_id(request.form['id'])
item['tags'] = list(dict.fromkeys(request.form['tags'].split(","))) # remove duplicated items
var.music_db.insert_music(item)
return redirect("./", code=302)
else: else:
abort(400) abort(400)

View File

@ -73,9 +73,8 @@ class MusicCache(dict):
return items return items
def fetch(self, bot, id): def fetch(self, bot, id):
music_dicts = self.db.query_music_by_id(id) music_dict = self.db.query_music_by_id(id)
if music_dicts: if music_dict:
music_dict = music_dicts[0]
self[id] = dict_to_item(bot, music_dict) self[id] = dict_to_item(bot, music_dict)
return self[id] return self[id]
else: else:

View File

@ -109,22 +109,8 @@
<div class="page-header"> <div class="page-header">
<h1 id="forms">Music Library</h1> <h1 id="forms">Music Library</h1>
</div> </div>
<div class="card">
<div class="card-header">
<h4 class="card-title">Tags</h4>
</div>
<div class="card-body">
{% for tag in tags_color_lookup.keys() %}
<span class="tag-click badge badge-{{ tags_color_lookup[tag] }}"
onclick="request('post', {add_tag : '{{ tag }}'})">
{{ tag }}</span>
{% endfor %}
</div> </div>
</div> </div>
</div>
</div>
</div>
<div class="bs-docs-section">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div id="browser" class="card"> <div id="browser" class="card">
@ -215,6 +201,7 @@
<div class="library-info-col col-4" style="padding: 3px;"> <div class="library-info-col col-4" style="padding: 3px;">
<span class="library-item-path text-muted path">Path/to/the/file</span> <span class="library-item-path text-muted path">Path/to/the/file</span>
<div class="library-item-tags"> <div class="library-item-tags">
<a class="tag-space tag-click library-item-edit"><i class="fas fa-edit" style="color: #AAAAAA"></i></a>
<span class="library-item-notag badge badge-light text-muted font-italic">No tag</span> <span class="library-item-notag badge badge-light text-muted font-italic">No tag</span>
<span class="library-item-tag tag-space badge">Tag</span> <span class="library-item-tag tag-space badge">Tag</span>
</div> </div>
@ -253,7 +240,30 @@
<i class="fas fa-sync-alt" aria-hidden="true"></i> Rescan Files <i class="fas fa-sync-alt" aria-hidden="true"></i> Rescan Files
</button> </button>
<button type="submit" class="btn btn-secondary btn-space" onclick="downloadAllResults()"><i class="fa fa-download" aria-hidden="true"></i> Download All</button> <button type="submit" class="btn btn-secondary btn-space" onclick="downloadAllResults()"><i class="fa fa-download" aria-hidden="true"></i> Download All</button>
<button type="submit" class="btn btn-secondary btn-space" onclick="deleteAllResults()"><i class="fas fa-trash-alt" aria-hidden="true"></i> Delete All</button> <button type="button" class="btn btn-danger btn-space"
data-toggle="modal" data-target="#deleteWarningModal"><i class="fas fa-trash-alt" aria-hidden="true"></i> Delete All</button>
</div>
<div class="modal fade" id="deleteWarningModal" tabindex="-1" role="dialog" aria-labelledby="Warning-Delete-File" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteWarningModalLabel">Are you really sure?</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
All files listed here, include files on other pages, will be deleted from your hard-drive.
Is that what you want?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" data-dismiss="modal" onclick="deleteAllResults()">Delete All Listed Files</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -348,6 +358,41 @@
<input hidden type="text" name="keywords" value=""> <input hidden type="text" name="keywords" value="">
</form> </form>
<!-- Add tags input -->
<div class="modal fade" id="addTagModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addTagModalTitle">Edit tags for ?</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div id="addTagModalBody" class="modal-body">
<input hidden type="text" id="addTagModalItemId" name="id" value="">
<div class="modal-tag" style="display: none; width: 100%;">
<span class="modal-tag-text tag-space badge badge-pill badge-dark">Tag</span>
<a class="modal-tag-remove tag-click small"><i class="fas fa-times-circle btn-outline-danger"></i></a>
</div>
<div id="addTagModalTags" style="margin-left: 5px; margin-bottom: 10px;">
</div>
<div class="input-group">
<input class="form-control form-control-sm btn-space" type="text" id="addTagModalInput" placeholder="tag1,tag2,..." >
<button id="addTagModalAddBtn" type="button" class="btn btn-primary btn-sm" onclick="addTagModalAdd()">
<i class="fa fa-plus" aria-hidden="true" ></i>
Add
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button id="addTagModalSubmit" type="button" class="btn btn-success" data-dismiss="modal" onclick="addTagModalSubmit()">Edit!</button>
</div>
</div>
</div>
</div>
<script src="static/js/jquery-3.4.1.min.js" crossorigin="anonymous"></script> <script src="static/js/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
<script src="static/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script> <script src="static/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="static/js/fontawesome.all.js" crossorigin="anonymous"></script> <script src="static/js/fontawesome.all.js" crossorigin="anonymous"></script>
@ -501,7 +546,9 @@
} , 3000); } , 3000);
// ------ Browser ------ // ------ Browser ------
var filter_type = 'file'; var filter_file = true;
var filter_url = false;
var filter_radio = false;
var filter_dir = $("#filter-dir"); var filter_dir = $("#filter-dir");
var filter_keywords = $("#filter-keywords"); var filter_keywords = $("#filter-keywords");
var filter_btn_file = $("#filter-type-file"); var filter_btn_file = $("#filter-type-file");
@ -509,19 +556,35 @@
var filter_btn_radio = $("#filter-type-radio"); var filter_btn_radio = $("#filter-type-radio");
function setFilterType(type){ function setFilterType(type){
filter_type = type; filter_types = [];
filter_btn_file.removeClass("btn-primary").addClass("btn-secondary");
filter_btn_url.removeClass("btn-primary").addClass("btn-secondary");
filter_btn_radio.removeClass("btn-primary").addClass("btn-secondary");
filter_dir.prop("disabled", true);
if(type === "file"){ if(type === "file"){
if(filter_btn_file.hasClass("btn-primary")){
filter_btn_file.removeClass("btn-primary").addClass("btn-secondary");
filter_dir.prop("disabled", true);
filter_file = false;
}else{
filter_btn_file.removeClass("btn-secondary").addClass("btn-primary"); filter_btn_file.removeClass("btn-secondary").addClass("btn-primary");
filter_dir.prop("disabled", false); filter_dir.prop("disabled", false);
filter_file = true;
}
}else if(type === "url"){ }else if(type === "url"){
if(filter_btn_url.hasClass("btn-primary")) {
filter_btn_url.removeClass("btn-primary").addClass("btn-secondary");
filter_url = false;
}else{
filter_btn_url.removeClass("btn-secondary").addClass("btn-primary"); filter_btn_url.removeClass("btn-secondary").addClass("btn-primary");
filter_url = true;
}
}else if(type === "radio"){ }else if(type === "radio"){
if(filter_btn_radio.hasClass("btn-primary")) {
filter_btn_radio.removeClass("btn-primary").addClass("btn-secondary");
filter_radio = false;
}else{
filter_btn_radio.removeClass("btn-secondary").addClass("btn-primary"); filter_btn_radio.removeClass("btn-secondary").addClass("btn-primary");
filter_types.push('radio')
filter_radio = true;
}
} }
updateResults(); updateResults();
} }
@ -558,7 +621,7 @@
request('post', { request('post', {
'delete_item_from_library': $(e.currentTarget).parent().parent().find(".library-item-id").val() 'delete_item_from_library': $(e.currentTarget).parent().parent().find(".library-item-id").val()
}); });
updateResults(); updateResults(active_page);
} }
); );
@ -596,9 +659,13 @@
var thumb_element = $(".library-item-thumb"); var thumb_element = $(".library-item-thumb");
var type_element = $(".library-item-type"); var type_element = $(".library-item-type");
var path_element = $(".library-item-path"); var path_element = $(".library-item-path");
var tag_edit_element = $(".library-item-edit");
var notag_element = $(".library-item-notag"); var notag_element = $(".library-item-notag");
var tag_element = $(".library-item-tag"); var tag_element = $(".library-item-tag");
var add_tag_modal = $("#addTagModal");
function addResultItem(item){ function addResultItem(item){
id_element.val(item.id); id_element.val(item.id);
title_element.html(item.title); title_element.html(item.title);
@ -612,6 +679,14 @@
var tags = item_copy.find(".library-item-tags"); var tags = item_copy.find(".library-item-tags");
tags.empty(); tags.empty();
var tag_edit_copy = tag_edit_element.clone();
tag_edit_copy.click(function(){
addTagModalPrepare(item.id, item.title, item.tags);
add_tag_modal.modal('show');
});
tag_edit_copy.appendTo(tags);
if(item.tags.length > 0){ if(item.tags.length > 0){
item.tags.forEach(function (tag_tuple){ item.tags.forEach(function (tag_tuple){
tag_copy = tag_element.clone(); tag_copy = tag_element.clone();
@ -625,7 +700,7 @@
} }
item_copy.appendTo(lib_group); item_copy.appendTo(lib_group);
item_copy.slideDown(); item_copy.show();
} }
function getFilters(dest_page=1){ function getFilters(dest_page=1){
@ -635,8 +710,13 @@
tags_list.push(tag.innerHTML); tags_list.push(tag.innerHTML);
}); });
filter_types = [];
if(filter_file){ filter_types.push("file"); }
if(filter_url){ filter_types.push("url"); }
if(filter_radio){ filter_types.push("radio"); }
return { return {
type: filter_type, type: filter_types.join(','),
dir: filter_dir.val(), dir: filter_dir.val(),
tags: tags_list.join(","), tags: tags_list.join(","),
keywords: filter_keywords.val(), keywords: filter_keywords.val(),
@ -646,8 +726,10 @@
var lib_loading = $("#library-item-loading"); var lib_loading = $("#library-item-loading");
var lib_empty = $("#library-item-empty"); var lib_empty = $("#library-item-empty");
var active_page = 1;
function updateResults(dest_page=1){ function updateResults(dest_page=1){
active_page = dest_page;
data = getFilters(dest_page); data = getFilters(dest_page);
data.action = "query"; data.action = "query";
@ -755,7 +837,7 @@
if(total_pages > 25){ if(total_pages > 25){
i = (active_page - 12 >= 1) ? active_page - 12 : 1; i = (active_page - 12 >= 1) ? active_page - 12 : 1;
_i = total_pages - 24; _i = total_pages - 23;
i = (i < _i) ? i : _i; i = (i < _i) ? i : _i;
page_li_copy = page_li.clone(); page_li_copy = page_li.clone();
page_no_copy = page_no.clone(); page_no_copy = page_no.clone();
@ -803,6 +885,67 @@
}); });
} }
var add_tag_modal_title = $("#addTagModalTitle");
var add_tag_modal_item_id = $("#addTagModalItemId");
var add_tag_modal_tags = $("#addTagModalTags");
var add_tag_modal_input = $("#addTagModalInput");
var modal_tag = $(".modal-tag");
var modal_tag_text = $(".modal-tag-text");
function addTagModalPrepare(_id, _title, _tag_tuples){
add_tag_modal_title.html("Edit tags for " + _title);
add_tag_modal_item_id.val(_id);
add_tag_modal_tags.empty();
_tag_tuples.forEach(function(tag_tuple){
modal_tag_text.html(tag_tuple[0]);
var tag_copy = modal_tag.clone();
var modal_tag_remove = tag_copy.find(".modal-tag-remove");
modal_tag_remove.click(function(e){
$(e.currentTarget).parent().remove();
});
tag_copy.show();
tag_copy.appendTo(add_tag_modal_tags);
modal_tag_text.html("");
});
}
function addTagModalAdd(){
new_tags = add_tag_modal_input.val().split(",").map(function(str){return str.trim()});
new_tags.forEach(function(tag){
modal_tag_text.html(tag);
var tag_copy = modal_tag.clone();
var modal_tag_remove = tag_copy.find(".modal-tag-remove");
modal_tag_remove.click(function(e){
$(e.currentTarget).parent().remove();
});
tag_copy.show();
tag_copy.appendTo(add_tag_modal_tags);
modal_tag_text.html("");
});
add_tag_modal_input.val("");
}
function addTagModalSubmit(){
var all_tags = $(".modal-tag-text");
tags = [];
all_tags.each(function(i, element){
if(element.innerHTML){
tags.push(element.innerHTML);
}
});
$.ajax({
type: 'POST',
url : 'library',
data: {
action: 'edit_tags',
id: add_tag_modal_item_id.val(),
tags: tags.join(",")
}
});
updateResults(active_page);
}
themeInit(); themeInit();
updateResults(); updateResults();