feat: tag editing feature in web interface
This commit is contained in:
parent
7f29deba01
commit
4e287d6b1b
@ -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()
|
||||
|
139
interface.py
139
interface.py
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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">×</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">×</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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user