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:
sql += f" ORDEY BY {self._order_by}"
print(sql)
return sql
def or_equal(self, column, equals_to, case_sensitive=True):
@ -330,7 +331,7 @@ class MusicDatabase:
return self._result_to_dict(results)
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):
condition = Condition()

View File

@ -58,6 +58,7 @@ class ReverseProxied(object):
web = Flask(__name__)
web.config['TEMPLATES_AUTO_RELOAD'] = True
log = logging.getLogger("bot")
user = 'Remote Control'
@ -361,20 +362,25 @@ def build_library_query_condition(form):
try:
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':
folder = form['dir']
if not folder.endswith('/') and folder:
folder += '/'
sub_cond = Condition()
count = 0
for file in var.cache.files:
if file.startswith(folder):
count += 1
sub_cond.or_equal("id", var.cache.file_id_lookup[file])
if count > 900:
break
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(",")
for tag in tags:
@ -402,73 +408,80 @@ def library():
if request.form:
log.debug("web: Post request from %s: %s" % (request.remote_addr, str(request.form)))
condition = build_library_query_condition(request.form)
if request.form['action'] in ['add', 'query', 'delete']:
condition = build_library_query_condition(request.form)
total_count = var.music_db.query_music_count(condition)
if not total_count:
abort(404)
total_count = var.music_db.query_music_count(condition)
if not total_count:
abort(404)
page_count = math.ceil(total_count / ITEM_PER_PAGE)
page_count = math.ceil(total_count / ITEM_PER_PAGE)
current_page = int(request.form['page']) if 'page' in request.form else 1
if current_page <= page_count:
condition.offset((current_page - 1) * ITEM_PER_PAGE)
else:
abort(404)
current_page = int(request.form['page']) if 'page' in request.form else 1
if current_page <= page_count:
condition.offset((current_page - 1) * ITEM_PER_PAGE)
else:
abort(404)
condition.limit(ITEM_PER_PAGE)
items = dicts_to_items(var.bot, var.music_db.query_music(condition))
condition.limit(ITEM_PER_PAGE)
items = dicts_to_items(var.bot, var.music_db.query_music(condition))
if 'action' in request.form and request.form['action'] == 'add':
for item in items:
music_wrapper = get_cached_wrapper(item, user)
var.playlist.append(music_wrapper)
if request.form['action'] == 'add':
for item in items:
music_wrapper = get_cached_wrapper(item, user)
var.playlist.append(music_wrapper)
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)
elif request.form['action'] == 'delete':
for item in items:
var.playlist.remove_by_id(item.id)
item = var.cache.get_item_by_id(var.bot, item.id)
if os.path.isfile(item.uri()):
log.info("web: delete file " + item.uri())
os.remove(item.uri())
var.cache.free_and_delete(item.id)
if len(os.listdir(var.music_folder + request.form['dir'])) == 0:
os.rmdir(var.music_folder + request.form['dir'])
return redirect("./", code=302)
else:
results = []
for item in items:
result = {}
result['id'] = item.id
result['title'] = item.title
result['type'] = item.display_type()
result['tags'] = [(tag, tag_color(tag)) for tag in item.tags]
if item.thumbnail:
result['thumb'] = f"data:image/PNG;base64,{item.thumbnail}"
else:
result['thumb'] = "static/image/unknown-album.png"
if item.type == 'file':
result['path'] = item.path
result['artist'] = item.artist
else:
result['path'] = item.url
result['artist'] = "??"
results.append(result)
return jsonify({
'items': results,
'total_pages': page_count,
'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)
elif 'action' in request.form and request.form['action'] == 'delete':
for item in items:
var.playlist.remove_by_id(item.id)
item = var.cache.get_item_by_id(var.bot, item.id)
if os.path.isfile(item.uri()):
log.info("web: delete file " + item.uri())
os.remove(item.uri())
var.cache.free_and_delete(item.id)
if len(os.listdir(var.music_folder + request.form['dir'])) == 0:
os.rmdir(var.music_folder + request.form['dir'])
return redirect("./", code=302)
else:
results = []
for item in items:
result = {}
result['id'] = item.id
result['title'] = item.title
result['type'] = item.display_type()
result['tags'] = [(tag, tag_color(tag)) for tag in item.tags]
if item.thumbnail:
result['thumb'] = f"data:image/PNG;base64,{item.thumbnail}"
else:
result['thumb'] = "static/image/unknown-album.png"
if item.type == 'file':
result['path'] = item.path
result['artist'] = item.artist
else:
result['path'] = item.url
result['artist'] = "??"
results.append(result)
return jsonify({
'items': results,
'total_pages': page_count,
'active_page': current_page
})
else:
abort(400)

View File

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

View File

@ -105,26 +105,12 @@
<div class="bs-docs-section">
<div class="row">
<div class="col">
<div class="page-header">
<h1 id="forms">Music Library</h1>
</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 class="col">
<div class="page-header">
<h1 id="forms">Music Library</h1>
</div>
</div>
</div>
</div>
</div>
<div class="bs-docs-section">
<div class="row">
<div class="col">
<div id="browser" class="card">
@ -215,6 +201,7 @@
<div class="library-info-col col-4" style="padding: 3px;">
<span class="library-item-path text-muted path">Path/to/the/file</span>
<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-tag tag-space badge">Tag</span>
</div>
@ -253,7 +240,30 @@
<i class="fas fa-sync-alt" aria-hidden="true"></i> Rescan Files
</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>
@ -348,6 +358,41 @@
<input hidden type="text" name="keywords" value="">
</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/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="static/js/fontawesome.all.js" crossorigin="anonymous"></script>
@ -501,7 +546,9 @@
} , 3000);
// ------ Browser ------
var filter_type = 'file';
var filter_file = true;
var filter_url = false;
var filter_radio = false;
var filter_dir = $("#filter-dir");
var filter_keywords = $("#filter-keywords");
var filter_btn_file = $("#filter-type-file");
@ -509,19 +556,35 @@
var filter_btn_radio = $("#filter-type-radio");
function setFilterType(type){
filter_type = type;
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);
filter_types = [];
if(type === "file"){
filter_btn_file.removeClass("btn-secondary").addClass("btn-primary");
filter_dir.prop("disabled", false);
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_dir.prop("disabled", false);
filter_file = true;
}
}else if(type === "url"){
filter_btn_url.removeClass("btn-secondary").addClass("btn-primary");
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_url = true;
}
}else if(type === "radio"){
filter_btn_radio.removeClass("btn-secondary").addClass("btn-primary");
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_types.push('radio')
filter_radio = true;
}
}
updateResults();
}
@ -558,7 +621,7 @@
request('post', {
'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 type_element = $(".library-item-type");
var path_element = $(".library-item-path");
var tag_edit_element = $(".library-item-edit");
var notag_element = $(".library-item-notag");
var tag_element = $(".library-item-tag");
var add_tag_modal = $("#addTagModal");
function addResultItem(item){
id_element.val(item.id);
title_element.html(item.title);
@ -612,6 +679,14 @@
var tags = item_copy.find(".library-item-tags");
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){
item.tags.forEach(function (tag_tuple){
tag_copy = tag_element.clone();
@ -625,7 +700,7 @@
}
item_copy.appendTo(lib_group);
item_copy.slideDown();
item_copy.show();
}
function getFilters(dest_page=1){
@ -635,8 +710,13 @@
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 {
type: filter_type,
type: filter_types.join(','),
dir: filter_dir.val(),
tags: tags_list.join(","),
keywords: filter_keywords.val(),
@ -646,8 +726,10 @@
var lib_loading = $("#library-item-loading");
var lib_empty = $("#library-item-empty");
var active_page = 1;
function updateResults(dest_page=1){
active_page = dest_page;
data = getFilters(dest_page);
data.action = "query";
@ -755,7 +837,7 @@
if(total_pages > 25){
i = (active_page - 12 >= 1) ? active_page - 12 : 1;
_i = total_pages - 24;
_i = total_pages - 23;
i = (i < _i) ? i : _i;
page_li_copy = page_li.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();
updateResults();