FEAT: MUSIC LIBRARY BROSWER

This commit is contained in:
Terry Geng
2020-03-19 22:51:32 +08:00
parent 3fe64c96c6
commit fb7101a581
10 changed files with 835 additions and 306 deletions

View File

@ -10,7 +10,8 @@ import variables as var
from librb import radiobrowser from librb import radiobrowser
from database import SettingsDatabase, MusicDatabase from database import SettingsDatabase, MusicDatabase
from media.item import item_id_generators, dict_to_item, dicts_to_items 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.cache import get_cached_wrapper_from_scrap, get_cached_wrapper_by_id, get_cached_wrappers_by_tags, \
get_cached_wrapper
from media.url_from_playlist import get_playlist_info from media.url_from_playlist import get_playlist_info
log = logging.getLogger("bot") log = logging.getLogger("bot")
@ -231,12 +232,16 @@ def cmd_play_file(bot, user, text, command, parameter, do_not_refresh_cache=Fals
# if parameter is {folder} # if parameter is {folder}
files = var.cache.dir.get_files(parameter) files = var.cache.dir.get_files(parameter)
if files: if files:
folder = parameter
if not folder.endswith('/'):
folder += '/'
msgs = [constants.strings('multiple_file_added')] msgs = [constants.strings('multiple_file_added')]
count = 0 count = 0
for file in files: for file in files:
count += 1 count += 1
music_wrapper = get_cached_wrapper_by_id(bot, var.cache.file_id_lookup[file], user) music_wrapper = get_cached_wrapper_by_id(bot, var.cache.file_id_lookup[folder + file], user)
var.playlist.append(music_wrapper) 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())
msgs.append("{} ({})".format(music_wrapper.item().title, music_wrapper.item().path)) msgs.append("{} ({})".format(music_wrapper.item().title, music_wrapper.item().path))
@ -984,19 +989,25 @@ def cmd_search_library(bot, user, text, command, parameter):
items = dicts_to_items(bot, music_dicts) items = dicts_to_items(bot, music_dicts)
song_shortlist = music_dicts song_shortlist = music_dicts
for item in items: if len(items) == 1:
count += 1 music_wrapper = get_cached_wrapper(items[0], user)
if len(item.tags) > 0: var.playlist.append(music_wrapper)
msgs.append("<li><b>{:d}</b> - [{}] <b>{}</b> (<i>{}</i>)</li>".format(count, item.display_type(), item.title, ", ".join(item.tags))) log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
else: bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string()))
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: else:
bot.send_msg(constants.strings("no_file"), text) 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: else:
bot.send_msg(constants.strings("no_file"), text) bot.send_msg(constants.strings("no_file"), text)

View File

@ -6,6 +6,112 @@ import datetime
class DatabaseError(Exception): class DatabaseError(Exception):
pass pass
class Condition:
def __init__(self):
self.filler = []
self._sql = ""
self._limit = 0
self._offset = 0
self._order_by = ""
pass
def sql(self):
sql = self._sql
if not self._sql:
sql = "TRUE"
if self._limit:
sql += f" LIMIT {self._limit}"
if self._offset:
sql += f" OFFSET {self._offset}"
if self._order_by:
sql += f" ORDEY BY {self._order_by}"
return sql
def or_equal(self, column, equals_to, case_sensitive=True):
if not case_sensitive:
column = f"LOWER({column})"
equals_to = equals_to.lower()
if self._sql:
self._sql += f" OR {column}=?"
else:
self._sql += f"{column}=?"
self.filler.append(equals_to)
return self
def and_equal(self, column, equals_to, case_sensitive=True):
if not case_sensitive:
column = f"LOWER({column})"
equals_to = equals_to.lower()
if self._sql:
self._sql += f" AND {column}=?"
else:
self._sql += f"{column}=?"
self.filler.append(equals_to)
return self
def or_like(self, column, equals_to, case_sensitive=True):
if not case_sensitive:
column = f"LOWER({column})"
equals_to = equals_to.lower()
if self._sql:
self._sql += f" OR {column} LIKE ?"
else:
self._sql += f"{column} LIKE ?"
self.filler.append(equals_to)
return self
def and_like(self, column, equals_to, case_sensitive=True):
if not case_sensitive:
column = f"LOWER({column})"
equals_to = equals_to.lower()
if self._sql:
self._sql += f" AND {column} LIKE ?"
else:
self._sql += f"{column} LIKE ?"
self.filler.append(equals_to)
return self
def or_sub_condition(self, sub_condition):
self.filler.extend(sub_condition.filler)
if self._sql:
self._sql += f"OR ({sub_condition.sql()})"
else:
self._sql += f"({sub_condition.sql()})"
return self
def and_sub_condition(self, sub_condition):
self.filler.extend(sub_condition.filler)
if self._sql:
self._sql += f"AND ({sub_condition.sql()})"
else:
self._sql += f"({sub_condition.sql()})"
return self
def limit(self, limit):
self._limit = limit
return self
def offset(self, offset):
self._offset = offset
return self
class SettingsDatabase: class SettingsDatabase:
version = 1 version = 1
@ -199,19 +305,21 @@ class MusicDatabase:
conn.close() conn.close()
return tags return tags
def query_music(self, **kwargs): def query_music_count(self, condition: Condition):
condition = [] filler = condition.filler
filler = [] condition_str = condition.sql()
for key, value in kwargs.items(): conn = sqlite3.connect(self.db_path)
if isinstance(value, str): cursor = conn.cursor()
condition.append(key + "=?") results = cursor.execute("SELECT COUNT(*) FROM music "
filler.append(value) "WHERE %s" % condition_str, filler).fetchall()
elif isinstance(value, dict): conn.close()
condition.append(key + " " + value[0] + " ?")
filler.append(value[1])
condition_str = " AND ".join(condition) return results[0][0]
def query_music(self, condition: Condition):
filler = condition.filler
condition_str = condition.sql()
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
@ -222,54 +330,39 @@ class MusicDatabase:
return self._result_to_dict(results) return self._result_to_dict(results)
def query_music_by_keywords(self, keywords): def query_music_by_keywords(self, keywords):
condition = [] condition = Condition()
filler = []
for keyword in keywords: for keyword in keywords:
condition.append('LOWER(title) LIKE ?') condition.and_like("title", f"%{keyword}%", case_sensitive=False)
filler.append("%{:s}%".format(keyword.lower()))
condition_str = " AND ".join(condition)
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
results = cursor.execute("SELECT id, type, title, metadata, tags FROM music " results = cursor.execute("SELECT id, type, title, metadata, tags FROM music "
"WHERE %s" % condition_str, filler).fetchall() "WHERE %s" % condition.sql(), condition.filler).fetchall()
conn.close() conn.close()
return self._result_to_dict(results) return self._result_to_dict(results)
def query_music_by_tags(self, tags): def query_music_by_tags(self, tags):
condition = [] condition = Condition()
filler = []
for tag in tags: for tag in tags:
condition.append('LOWER(tags) LIKE ?') condition.and_like("tags", f"%{tag},%", case_sensitive=False)
filler.append("%{:s},%".format(tag.lower()))
condition_str = " AND ".join(condition)
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
results = cursor.execute("SELECT id, type, title, metadata, tags FROM music " results = cursor.execute("SELECT id, type, title, metadata, tags FROM music "
"WHERE %s" % condition_str, filler).fetchall() "WHERE %s" % condition.sql(), condition.filler).fetchall()
conn.close() conn.close()
return self._result_to_dict(results) return self._result_to_dict(results)
def query_tags_by_ids(self, ids): def query_tags(self, condition):
# TODO: Can we keep a index of tags?
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
results = [] results = cursor.execute("SELECT id, tags FROM music "
"WHERE %s" % condition.sql(), condition.filler).fetchall()
for i in range(int(len(ids)/990) + 1):
condition_str = " OR ".join(['id=?'] * min(990, len(ids) - i*990))
_results = cursor.execute("SELECT id, tags FROM music "
"WHERE %s" % condition_str,
ids[i*990: i*990 + min(990, len(ids) - i*990)]).fetchall()
if _results:
results.extend(_results)
conn.close() conn.close()
@ -309,24 +402,11 @@ class MusicDatabase:
else: else:
return [] return []
def delete_music(self, **kwargs): def delete_music(self, condition: Condition):
condition = []
filler = []
for key, value in kwargs.items():
if isinstance(value, str):
condition.append(key + "=?")
filler.append(value)
else:
condition.append(key + " " + value[0] + " ?")
filler.append(value[1])
condition_str = " AND ".join(condition)
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM music " cursor.execute("DELETE FROM music "
"WHERE %s" % condition_str, filler) "WHERE %s" % condition.sql(), condition.filler)
conn.commit() conn.commit()
conn.close() conn.close()

View File

@ -4,13 +4,17 @@ from functools import wraps
from flask import Flask, render_template, request, redirect, send_file, Response, jsonify, abort from flask import Flask, render_template, request, redirect, send_file, Response, jsonify, abort
import variables as var import variables as var
import util import util
import math
import os import os
import os.path import os.path
import shutil import shutil
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import errno import errno
import media import media
from media.cache import get_cached_wrapper_from_scrap, get_cached_wrapper_by_id, get_cached_wrappers_by_tags from media.item import dicts_to_items
from media.cache import get_cached_wrapper_from_scrap, get_cached_wrapper_by_id, get_cached_wrappers_by_tags, \
get_cached_wrapper
from database import MusicDatabase, Condition
import logging import logging
import time import time
@ -126,7 +130,8 @@ def build_path_tags_lookup():
path_tags_lookup = {} path_tags_lookup = {}
ids = list(var.cache.file_id_lookup.values()) ids = list(var.cache.file_id_lookup.values())
if len(ids) > 0: if len(ids) > 0:
id_tags_lookup = var.music_db.query_tags_by_ids(ids) condition = Condition().and_equal("type", "file")
id_tags_lookup = var.music_db.query_tags(condition)
for path, id in var.cache.file_id_lookup.items(): for path, id in var.cache.file_id_lookup.items():
path_tags_lookup[path] = id_tags_lookup[id] path_tags_lookup[path] = id_tags_lookup[id]
@ -207,45 +212,32 @@ def post():
if request.method == 'POST': if request.method == 'POST':
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 '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_cached_wrapper_by_id(var.bot, var.cache.file_id_lookup[request.form['add_file_bottom']], user)
var.playlist.append(music_wrapper) if 'add_item_at_once' in request.form:
log.info('web: add to playlist(bottom): ' + music_wrapper.format_debug_string()) music_wrapper = get_cached_wrapper_by_id(var.bot, request.form['add_item_at_once'], user)
if music_wrapper:
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_cached_wrapper_by_id(var.bot, var.cache.file_id_lookup[request.form['add_file_next']], user)
var.playlist.insert(var.playlist.current_index + 1, music_wrapper) var.playlist.insert(var.playlist.current_index + 1, music_wrapper)
log.info('web: add to playlist(next): ' + music_wrapper.format_debug_string()) log.info('web: add to playlist(next): ' + music_wrapper.format_debug_string())
var.bot.interrupt()
else:
abort(404)
elif ('add_folder' in request.form and ".." not in request.form['add_folder']) or ('add_folder_recursively' in request.form and ".." not in request.form['add_folder_recursively']): if 'add_item_bottom' in request.form:
try: music_wrapper = get_cached_wrapper_by_id(var.bot, request.form['add_item_bottom'], user)
folder = request.form['add_folder']
except:
folder = request.form['add_folder_recursively']
if not folder.endswith('/'): if music_wrapper:
folder += '/' var.playlist.append(music_wrapper)
log.info('web: add to playlist(bottom): ' + music_wrapper.format_debug_string())
else:
abort(404)
if os.path.isdir(var.music_folder + folder): elif 'add_item_next' in request.form:
dir = var.cache.dir music_wrapper = get_cached_wrapper_by_id(var.bot, request.form['add_item_next'], user)
if 'add_folder_recursively' in request.form: if music_wrapper:
files = dir.get_files_recursively(folder) var.playlist.insert(var.playlist.current_index + 1, music_wrapper)
else: log.info('web: add to playlist(next): ' + music_wrapper.format_debug_string())
files = dir.get_files(folder) else:
abort(404)
music_wrappers = list(map(
lambda file:
get_cached_wrapper_by_id(var.bot, var.cache.file_id_lookup[folder + file], user), files))
var.playlist.extend(music_wrappers)
for music_wrapper in music_wrappers:
log.info('web: add to playlist: ' + music_wrapper.format_debug_string())
elif 'add_url' in request.form: elif 'add_url' in request.form:
music_wrapper = get_cached_wrapper_from_scrap(var.bot, type='url', url=request.form['add_url'], user=user) music_wrapper = get_cached_wrapper_from_scrap(var.bot, type='url', url=request.form['add_url'], user=user)
@ -367,6 +359,102 @@ def post():
return status() return status()
def build_library_query_condition(form):
try:
condition = Condition()
if form['type'] == 'file':
folder = form['dir']
if not folder.endswith('/') and folder:
folder += '/'
sub_cond = Condition()
for file in var.cache.files:
if file.startswith(folder):
sub_cond.or_equal("id", var.cache.file_id_lookup[file])
condition.and_sub_condition(sub_cond)
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:
condition.and_like("tags", f"%{tag},%", case_sensitive=False)
_keywords = form['keywords'].split(" ")
keywords = []
for kw in _keywords:
if kw:
keywords.append(kw)
for keyword in keywords:
condition.and_like("title", f"%{keyword}%", case_sensitive=False)
return condition
except KeyError:
abort(400)
@web.route("/library", methods=['POST'])
@requires_auth
def library():
global log
ITEM_PER_PAGE = 10
if request.form:
log.debug("web: Post request from %s: %s" % (request.remote_addr, str(request.form)))
condition = build_library_query_condition(request.form)
total_count = var.music_db.query_music_count(condition)
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)
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)
log.info("cmd: add to playlist: " + music_wrapper.format_debug_string())
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)
@web.route('/upload', methods=["POST"]) @web.route('/upload', methods=["POST"])
def upload(): def upload():
@ -374,19 +462,19 @@ def upload():
files = request.files.getlist("file[]") files = request.files.getlist("file[]")
if not files: if not files:
return redirect("./", code=406) return redirect("./", code=400)
# filename = secure_filename(file.filename).strip() # filename = secure_filename(file.filename).strip()
for file in files: for file in files:
filename = file.filename filename = file.filename
if filename == '': if filename == '':
return redirect("./", code=406) return redirect("./", code=400)
targetdir = request.form['targetdir'].strip() targetdir = request.form['targetdir'].strip()
if targetdir == '': if targetdir == '':
targetdir = 'uploads/' targetdir = 'uploads/'
elif '../' in targetdir: elif '../' in targetdir:
return redirect("./", code=406) return redirect("./", code=400)
log.info('web: Uploading file from %s:' % request.remote_addr) log.info('web: Uploading file from %s:' % request.remote_addr)
log.info('web: - filename: ' + filename) log.info('web: - filename: ' + filename)
@ -397,7 +485,7 @@ def upload():
storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir)) storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir))
print('storagepath:', storagepath) print('storagepath:', storagepath)
if not storagepath.startswith(os.path.abspath(var.music_folder)): if not storagepath.startswith(os.path.abspath(var.music_folder)):
return redirect("./", code=406) return redirect("./", code=400)
try: try:
os.makedirs(storagepath) os.makedirs(storagepath)
@ -424,37 +512,34 @@ def upload():
def download(): def download():
global log global log
if 'file' in request.args: print('id' in request.args)
requested_file = request.args['file'] if 'id' in request.args and request.args['id']:
item = dicts_to_items(var.bot,
var.music_db.query_music(
Condition().and_equal('id', request.args['id'])))[0]
requested_file = item.uri()
log.info('web: Download of file %s requested from %s:' % (requested_file, request.remote_addr)) 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 = var.cache.files
if requested_file in files: try:
filepath = os.path.join(folder_path, requested_file) return send_file(requested_file, as_attachment=True)
try: except Exception as e:
return send_file(filepath, as_attachment=True) log.exception(e)
except Exception as e: abort(404)
log.exception(e)
abort(404)
elif 'directory' in request.args:
requested_dir = request.args['directory']
folder_path = var.music_folder
requested_dir_fullpath = os.path.abspath(os.path.join(folder_path, requested_dir)) + '/'
if requested_dir_fullpath.startswith(folder_path):
if os.path.samefile(requested_dir_fullpath, folder_path):
prefix = 'all'
else:
prefix = secure_filename(os.path.relpath(requested_dir_fullpath, folder_path))
zipfile = util.zipdir(requested_dir_fullpath, prefix)
try:
return send_file(zipfile, as_attachment=True)
except Exception as e:
log.exception(e)
abort(404)
return redirect("./", code=400) else:
condition = build_library_query_condition(request.args)
items = dicts_to_items(var.bot, var.music_db.query_music(condition))
zipfile = util.zipdir([item.uri() for item in items])
try:
return send_file(zipfile, as_attachment=True)
except Exception as e:
log.exception(e)
abort(404)
return abort(400)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -9,7 +9,7 @@ import media.file
import media.url import media.url
import media.url_from_playlist import media.url_from_playlist
import media.radio import media.radio
from database import MusicDatabase from database import MusicDatabase, Condition
import variables as var import variables as var
import util import util
@ -21,7 +21,7 @@ class MusicCache(dict):
self.log = logging.getLogger("bot") self.log = logging.getLogger("bot")
self.dir = None self.dir = None
self.files = [] self.files = []
self.file_id_lookup = {} self.file_id_lookup = {} # TODO: Now I see this is silly. Gonna add a column "path" in the database.
self.dir_lock = threading.Lock() self.dir_lock = threading.Lock()
def get_item_by_id(self, bot, id): # Why all these functions need a bot? Because it need the bot to send message! def get_item_by_id(self, bot, id): # Why all these functions need a bot? Because it need the bot to send message!
@ -73,7 +73,7 @@ class MusicCache(dict):
return items return items
def fetch(self, bot, id): def fetch(self, bot, id):
music_dicts = self.db.query_music(id=id) music_dicts = self.db.query_music(Condition().and_equal("id", id))
if music_dicts: if music_dicts:
music_dict = music_dicts[0] music_dict = music_dicts[0]
self[id] = dict_to_item(bot, music_dict) self[id] = dict_to_item(bot, music_dict)
@ -101,7 +101,7 @@ class MusicCache(dict):
if item.id in self: if item.id in self:
del self[item.id] del self[item.id]
self.db.delete_music(id=item.id) self.db.delete_music(Condition().and_equal("id", item.id))
def free(self, id): def free(self, id):
if id in self: if id in self:
@ -222,6 +222,11 @@ class CachedItemWrapper:
# Remember!!! Get wrapper functions will automatically add items into the cache! # Remember!!! Get wrapper functions will automatically add items into the cache!
def get_cached_wrapper(item, user):
var.cache[item.id] = item
return CachedItemWrapper(var.cache, item.id, item.type, user)
def get_cached_wrapper_from_scrap(bot, **kwargs): def get_cached_wrapper_from_scrap(bot, **kwargs):
item = var.cache.get_item(bot, **kwargs) item = var.cache.get_item(bot, **kwargs)
if 'user' not in kwargs: if 'user' not in kwargs:
@ -231,8 +236,7 @@ def get_cached_wrapper_from_scrap(bot, **kwargs):
def get_cached_wrapper_from_dict(bot, dict_from_db, user): def get_cached_wrapper_from_dict(bot, dict_from_db, user):
item = dict_to_item(bot, dict_from_db) item = dict_to_item(bot, dict_from_db)
var.cache[dict_from_db['id']] = item return get_cached_wrapper(item, user)
return CachedItemWrapper(var.cache, item.id, item.type, user)
def get_cached_wrapper_by_id(bot, id, user): def get_cached_wrapper_by_id(bot, id, user):

View File

@ -1,10 +1,17 @@
.bs-docs-section{margin-top:4em} .bs-docs-section{margin-top:4em}
.bs-docs-section .page-header h1 { padding: 2rem 0; font-size: 3rem; margin-bottom: 10px } .bs-docs-section .page-header h1 { padding: 2rem 0; font-size: 3rem; margin-bottom: 10px }
.btn-space{margin-right:5px} .btn-space{margin-right:5px;}
.tag-space{margin-right:3px;}
.playlist-title-td{width:60%} .playlist-title-td{width:60%}
.playlist-title{float:left; } .playlist-title{float:left; }
.playlist-artwork{float:left; margin-left:10px;} .playlist-artwork{float:left; margin-left:10px;}
.tag-click{cursor:pointer;} .tag-click{
cursor:pointer;
transition: 400ms;
}
.tag-clicked{
transform: scale(1.2);
}
.floating-button { .floating-button {
width: 50px; width: 50px;
height: 50px; height: 50px;
@ -20,9 +27,74 @@
right: 50px; right: 50px;
bottom: 40px; bottom: 40px;
} }
.floating-button:hover { .floating-button:hover {
background-color: hsl(0, 0%, 43%); background-color: hsl(0, 0%, 43%);
color: white; color: white;
} }
.library-item{
display: flex;
margin-left: 5px;
padding: .5rem .5rem .5rem 0;
height: 72px;
transition: ease-in-out 200ms;
}
.library-thumb-img {
width: 70px;
height: 70px;
border-radius: 5px;
}
.library-thumb-col {
position: relative;
padding-left: 0;
overflow: hidden;
margin: -0.5rem 1rem -0.5rem 0;
}
.library-thumb-grp {
position: absolute;
top: 0;
left: -95px;
width: 70px;
margin-left: 15px;
transition: left 300ms;
border-radius: 5px;
opacity: 0.6;
font-weight: 300;
}
.library-thumb-img:hover + .library-thumb-grp {
left: -15px;
}
.library-thumb-grp:hover {
left: -15px;
}
.library-thumb-btn-up {
position: absolute !important;
top: 0;
height: 35px;
padding-top: 5px;
}
.library-thumb-btn-down {
position: absolute !important;
top: 35px;
height: 35px;
padding-top: 5px;
}
.library-info-col {
margin-right: 2rem;
padding: 3px 0;
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
overflow: hidden;
}
.library-info-col .small {
font-weight: 300;
}
.library-action {
margin-left: auto;
}
.library-info-col .path{
font-style: italic !important;
font-weight: 300;
}

View File

@ -0,0 +1 @@
<svg height="512pt" viewBox="0 -12 512.00032 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m455.074219 172.613281 53.996093-53.996093c2.226563-2.222657 3.273438-5.367188 2.828126-8.480469-.441407-3.113281-2.328126-5.839844-5.085938-7.355469l-64.914062-35.644531c-4.839844-2.65625-10.917969-.886719-13.578126 3.953125-2.65625 4.84375-.890624 10.921875 3.953126 13.578125l53.234374 29.230469-46.339843 46.335937-166.667969-91.519531 46.335938-46.335938 46.839843 25.722656c4.839844 2.65625 10.921875.890626 13.578125-3.953124 2.660156-4.839844.890625-10.921876-3.953125-13.578126l-53.417969-29.335937c-3.898437-2.140625-8.742187-1.449219-11.882812 1.695313l-54 54-54-54c-3.144531-3.144532-7.988281-3.832032-11.882812-1.695313l-184.929688 101.546875c-2.757812 1.515625-4.644531 4.238281-5.085938 7.355469-.445312 3.113281.601563 6.257812 2.828126 8.480469l53.996093 53.996093-53.996093 53.992188c-2.226563 2.226562-3.273438 5.367187-2.828126 8.484375.441407 3.113281 2.328126 5.839844 5.085938 7.351562l55.882812 30.6875v102.570313c0 3.652343 1.988282 7.011719 5.1875 8.769531l184.929688 101.542969c1.5.824219 3.15625 1.234375 4.8125 1.234375s3.3125-.410156 4.8125-1.234375l184.929688-101.542969c3.199218-1.757812 5.1875-5.117188 5.1875-8.769531v-102.570313l55.882812-30.683594c2.757812-1.515624 4.644531-4.242187 5.085938-7.355468.445312-3.113282-.601563-6.257813-2.828126-8.480469zm-199.074219 90.132813-164.152344-90.136719 164.152344-90.140625 164.152344 90.140625zm-62.832031-240.367188 46.332031 46.335938-166.667969 91.519531-46.335937-46.335937zm-120.328125 162.609375 166.667968 91.519531-46.339843 46.339844-166.671875-91.519531zm358.089844 184.796875-164.929688 90.5625v-102.222656c0-5.523438-4.476562-10-10-10s-10 4.476562-10 10v102.222656l-164.929688-90.5625v-85.671875l109.046876 59.878907c1.511718.828124 3.167968 1.234374 4.808593 1.234374 2.589844 0 5.152344-1.007812 7.074219-2.929687l54-54 54 54c1.921875 1.925781 4.484375 2.929687 7.074219 2.929687 1.640625 0 3.296875-.40625 4.808593-1.234374l109.046876-59.878907zm-112.09375-46.9375-46.339844-46.34375 166.667968-91.515625 46.34375 46.335938zm0 0" fill="#aaaaaa" fill-opacity="1"/><path d="m404.800781 68.175781c2.628907 0 5.199219-1.070312 7.070313-2.933593 1.859375-1.859376 2.929687-4.4375 2.929687-7.066407 0-2.632812-1.070312-5.210937-2.929687-7.070312-1.859375-1.863281-4.441406-2.929688-7.070313-2.929688-2.640625 0-5.210937 1.066407-7.070312 2.929688-1.871094 1.859375-2.929688 4.4375-2.929688 7.070312 0 2.628907 1.058594 5.207031 2.929688 7.066407 1.859375 1.863281 4.441406 2.933593 7.070312 2.933593zm0 0" fill="#aaaaaa" fill-opacity="1"/><path d="m256 314.925781c-2.628906 0-5.210938 1.066407-7.070312 2.929688-1.859376 1.867187-2.929688 4.4375-2.929688 7.070312 0 2.636719 1.070312 5.207031 2.929688 7.078125 1.859374 1.859375 4.441406 2.921875 7.070312 2.921875s5.210938-1.0625 7.070312-2.921875c1.859376-1.871094 2.929688-4.441406 2.929688-7.078125 0-2.632812-1.070312-5.203125-2.929688-7.070312-1.859374-1.863281-4.441406-2.929688-7.070312-2.929688zm0 0" fill="#aaaaaa" fill-opacity="1"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

1
static/image/loading.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="64px" height="64px" viewBox="0 0 128 128" xml:space="preserve"><g><path d="M75.4 126.63a11.43 11.43 0 0 1-2.1-22.65 40.9 40.9 0 0 0 30.5-30.6 11.4 11.4 0 1 1 22.27 4.87h.02a63.77 63.77 0 0 1-47.8 48.05v-.02a11.38 11.38 0 0 1-2.93.37z" fill="#aaaaaa" fill-opacity="1"/><animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1800ms" repeatCount="indefinite"></animateTransform></g></svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@ -1,107 +1,3 @@
{% macro dirlisting(dir, path='') -%}
<ul class="list-group">
{% if dir and dir.get_subdirs().items() %}
{% for subdirname, subdirobj in dir.get_subdirs().items() %}
{% set subdirpath = os.path.relpath(subdirobj.fullpath, music_library.fullpath) %}
{% set subdirid = subdirpath.replace("/","-") %}
<li class="directory list-group-item list-group-item-primary">
<div class="btn-group" role="group">
<div class="btn-group" role="group">
<button type="button" class="btn btn-success btn-sm"
onclick="request('/post', {add_folder : '{{ subdirpath }}'})">
<i class="fa fa-plus" aria-hidden="true"></i>
</button>
<div class="btn-group" role="group">
<button id="btnGroupDrop2" type="button" class="btn btn-success btn-sm dropdown-toggle btn-space" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop2" style="">
<a class="dropdown-item"
onclick="request('/post', {add_folder : '{{ subdirpath }}'})">
<i class="fa fa-folder" aria-hidden="true"></i> Entire folder
</a>
<a class="dropdown-item"
onclick="request('/post', {add_folder_recursively : '{{ subdirpath }}'})">
<i class="fa fa-folder" aria-hidden="true"></i> Entire folder and sub-folders
</a>
</div>
</div>
</div>
</div>
<div class="btn-group lead"><div class="btn-space"><i class="fa fa-folder" aria-hidden="true"></i></div><a class="lead" data-toggle="collapse"
data-target="#multiCollapse-{{ subdirid }}" aria-expanded="true"
aria-controls="multiCollapse-{{ subdirid }}" href="#"> {{ subdirpath }}/</a>
</div>
<div class="btn-group" style="float: right;">
<form action="./download" method="get" class="directory">
<input type="text" value="{{ subdirpath }}" name="directory" hidden>
<button type="submit" class="btn btn-primary btn-sm btn-space"><i class="fa fa-download" aria-hidden="true"></i></button>
</form>
<button type="submit" class="btn btn-danger btn-sm btn-space"
onclick="request('/post', {delete_folder : '{{ subdirpath }}'}, true)">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</li>
<div class="collapse multi-collapse" id="multiCollapse-{{ subdirid }}">
{{ dirlisting(subdirobj, subdirpath) -}}
</div>
{% endfor %}
{% endif %}
{% set files = dir.get_files() %}
{% if files %}
{% for file in files %}
{% set filepath = os.path.relpath(os.path.join(dir.fullpath, file), music_library.fullpath) %}
<li class="file list-group-item">
<div class="btn-group" role="group">
<div class="btn-group" role="group">
<button type="button" class="btn btn-success btn-sm"
onclick="request('/post', {add_file_bottom : '{{ filepath }}'})">
<i class="fa fa-plus" aria-hidden="true"></i>
</button>
<div class="btn-group" role="group">
<button id="btnGroupDrop2" type="button" class="btn btn-success btn-sm dropdown-toggle btn-space" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop2" style="">
<a class="dropdown-item"
onclick="request('/post', {add_file_bottom : '{{ filepath }}'})">
<i class="fa fa-angle-down" aria-hidden="true"></i> To bottom of play list
</a>
<a class="dropdown-item"
onclick="request('/post', {add_file_next : '{{ filepath }}'})">
<i class="fa fa-angle-right" aria-hidden="true"></i> After current song
</a>
</div>
</div>
</div>
</div>
<div class="btn-group lead">
<div class="btn-space"><i class="fa fa-music" aria-hidden="true"></i></div>
{{ filepath }}
</div>
{% if tags_lookup[filepath] %}
{% for tag in tags_lookup[filepath] %}
<span class="badge badge-{{ tags_color_lookup[tag] }}">{{ tag }}</span>
{% endfor %}
{% endif %}
<div class="btn-group" style="float: right;">
<form action="./download" method="get" class="file file_download">
<input type="text" value="{{ filepath }}" name="file" hidden>
<button type="submit" class="btn btn-primary btn-sm btn-space"><i class="fa fa-download" aria-hidden="true"></i></button>
</form>
<button type="submit" class="btn btn-danger btn-sm btn-space"
onclick="request('/post', {delete_music_file : '{{ filepath }}'}, true)">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</li>
{% endfor %}
{% endif %}
</ul>
{%- endmacro %}
<!DOCTYPE html> <!DOCTYPE html>
<head> <head>
@ -186,9 +82,11 @@
<th scope="col">Action</th> <th scope="col">Action</th>
</tr> </tr>
</thead> </thead>
<tbody id="playlist-table"> <tbody id="playlist-table" class="playlist-table">
<tr class="table-dark"> <tr id="playlist-loading">
<td colspan="4" class="text-muted" style="text-align:center;"> Fetching playlist .... </td> <td colspan="4" style="text-align:center;">
<img style="margin: auto; width: 35px;" src="static/image/loading.svg" />
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -229,28 +127,133 @@
<div class="bs-docs-section"> <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">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Files</h4> <h4 class="card-title">Browser</h4>
</div> </div>
<div class="card-body">
<div class="alert alert-secondary">
<h4 class="alert-heading">Filters</h4>
<div class="row">
<div class="col-6">
<div id="filter-type" class="form-group row">
<label class="col-sm-2 col-form-label">Type</label>
<div class="btn-group btn-group-toggle" data-toggle="buttons" style="height: 35px; padding-top:5px;">
<button type="button" id="filter-type-file" class="btn btn-primary btn-sm" onclick="setFilterType('file')">File</button>
<button type="button" id="filter-type-url" class="btn btn-secondary btn-sm" onclick="setFilterType('url')">URL</button>
<button type="button" id="filter-type-radio" class="btn btn-secondary btn-sm" onclick="setFilterType('radio')">Radio</button>
</div>
</div>
<div id="filter-path" class="form-group row">
<label class="col-sm-2 col-form-label" for="filter-dir" style="max-width: none">Directory</label>
<div class="col-sm-8">
<select class="form-control form-control-sm" id="filter-dir" style="margin-top:5px;">
<option value="">.</option>
{% for dir in music_library.get_subdirs_recursively() %}
<option value="{{ dir }}">{{ dir }}</option>
{% endfor %}
</select>
</div>
</div>
<div id="filter-path" class="form-group row">
<label class="col-sm-2 col-form-label" for="filter-keywords" style="max-width: none">Keywords</label>
<div class="col-sm-8">
<input class="form-control form-control-sm" id="filter-keywords" name="keywords"
placeholder="Keywords..." style="margin-top:5px;"/>
</div>
</div>
</div>
<div class="col-6">
<div id="filter-type" class="form-group row">
<label class="col-sm-2 col-form-label">Tags</label>
<div class="col-sm-10" style="padding-top:5px;">
{% for tag in tags_color_lookup.keys() %}
<span id="filter-tag" class="filter-tag tag-click badge badge-{{ tags_color_lookup[tag] }}">{{ tag }}</span>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div id="library-group" class="list-group library-group">
<div id="library-item-loading" class="list-group-item library-item">
<img style="margin: auto; width: 35px;" src="static/image/loading.svg" />
</div>
<div id="library-item-empty" style="display: none" class="list-group-item library-item">
<img style="margin: auto; width: 35px;" src="static/image/empty_box.svg" />
</div>
<div id="library-item" style="display: none;" class="list-group-item library-item">
<input hidden type="text" class="library-item-id" value="" />
<div class="library-thumb-col">
<div class="library-thumb-img">
<img class="library-item-thumb library-thumb-img" src="static/image/unknown-album.png"/>
</div>
<div class="btn-group-vertical library-thumb-grp">
<div class="library-item-add-next btn btn-secondary library-thumb-btn-up" title="Play Next">
<i class="fa fa-plus" aria-hidden="true"></i>
<span class="btn-space"></span>
<i class="fa fa-arrow-right" aria-hidden="true"></i>
</div>
<div class="library-item-add-bottom btn btn-secondary library-thumb-btn-down" title="Add to Playlist">
<i class="fa fa-plus" aria-hidden="true"></i>
<span class="btn-space"></span>
<i class="fas fa-level-down-alt" aria-hidden="true"></i>
</div>
</div>
</div>
<div class="library-info-col col-5" style="padding: 12px 0;">
<div>
<span class="library-item-type lead text-muted btn-space">[File]</span>
<span class="library-item-title lead btn-space">This is my title</span>
<span class="library-item-artist text-muted"> - Artist</span>
</div>
</div>
<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">
<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>
</div>
<div class="btn-group library-action">
<button class="library-item-play btn btn-info btn-sm btn-space" type="button">
<i class="fa fa-play" aria-hidden="true"></i>
</button>
<button class="library-item-download btn btn-primary btn-sm btn-space" type="button">
<i class="fa fa-download" aria-hidden="true"></i>
</button>
<button class="library-item-trash btn btn-danger btn-sm btn-space" type="button">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</div>
<div class="list-group">
<div id="library-pagination" style="margin-left: auto; margin-top: 10px;">
<ul id="library-page-ul" class="pagination pagination">
<li class="library-page-li page-item active">
<a class="library-page-no page-link">1</a>
</li>
</ul>
</div>
</div>
<div class="card-body">
<div class="btn-group" style="margin-bottom: 5px;" role="group"> <div class="btn-group" style="margin-bottom: 5px;" role="group">
<button type="submit" class="btn btn-secondary btn-space" <button type="submit" class="btn btn-secondary btn-space"
onclick="request('/post', {action : 'rescan'}); location.reload()"> onclick="request('post', {action : 'rescan'}); updateResults()">
<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>
<form action="./download" method="get" class="directory form1"> <button type="submit" class="btn btn-secondary btn-space" onclick="downloadAllResults()"><i class="fa fa-download" aria-hidden="true"></i> Download All</button>
<input type="text" value="./" name="directory" hidden> <button type="submit" class="btn btn-secondary btn-space" onclick="addAllResults()"><i class="fa fa-plus" aria-hidden="true"></i> Add All</button>
<button type="submit" class="btn btn-secondary btn-space"><i class="fa fa-download" aria-hidden="true"></i> Download All</button>
</form>
<form method="post" class="directory form3">
<input type="text" value="./" name="add_folder_recursively" hidden>
<button type="submit" class="btn btn-secondary btn-space"><i class="fa fa-plus" aria-hidden="true"></i> Add All</button>
</form>
</div> </div>
<br />
{{ dirlisting(music_library) }}
</div> </div>
</div> </div>
</div> </div>
@ -283,10 +286,9 @@
<input class="form-control btn-space" list="targetdirs" id="targetdir" name="targetdir" <input class="form-control btn-space" list="targetdirs" id="targetdir" name="targetdir"
placeholder="uploads" /> placeholder="uploads" />
<datalist id="targetdirs"> <datalist id="targetdirs">
<option value="uploads"> {% for dir in music_library.get_subdirs_recursively() %}
{% for dir in music_library.get_subdirs_recursively() %} <option value="{{ dir }}" />
<option value="{{ dir }}"> {% endfor %}
{% endfor %}
</datalist> </datalist>
</div> </div>
<button class="btn btn-primary btn-space" type="submit" <button class="btn btn-primary btn-space" type="submit"
@ -311,7 +313,7 @@
<div class="input-group"> <div class="input-group">
<input class="form-control btn-space" type="text" id="add_url_input" placeholder="URL..."> <input class="form-control btn-space" type="text" id="add_url_input" placeholder="URL...">
<button type="submit" class="btn btn-primary" <button type="submit" class="btn btn-primary"
onclick="var $i = $('#add_url_input')[0]; request('/post', {add_url : $i.value }); $i.value = ''; ">Add URL</button> onclick="var $i = $('#add_url_input')[0]; request('post', {add_url : $i.value }); $i.value = ''; ">Add URL</button>
</div> </div>
</div> </div>
</div> </div>
@ -326,7 +328,7 @@
<div class="input-group"> <div class="input-group">
<input class="form-control btn-space" type="text" id="add_radio_input" placeholder="Radio Address..."> <input class="form-control btn-space" type="text" id="add_radio_input" placeholder="Radio Address...">
<button type="submit" class="btn btn-primary" <button type="submit" class="btn btn-primary"
onclick="var $i = $('#add_radio_input')[0]; request('/post', {add_radio : $i.value }); $i.value = '';">Add Radio</button> onclick="var $i = $('#add_radio_input')[0]; request('post', {add_radio : $i.value }); $i.value = '';">Add Radio</button>
</div> </div>
</div> </div>
</div> </div>
@ -337,6 +339,14 @@
<div class="floating-button" onclick="switchTheme()"> <i class="fas fa-lightbulb" aria-hidden="true"></i> </div> <div class="floating-button" onclick="switchTheme()"> <i class="fas fa-lightbulb" aria-hidden="true"></i> </div>
<form id="download-form" action="download" method="GET" target="_blank">
<input hidden type="text" name="id" value="">
<input hidden type="text" name="type" value="">
<input hidden type="text" name="dir" value="">
<input hidden type="text" name="tags" value="">
<input hidden type="text" name="keywords" value="">
</form>
<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>
@ -354,16 +364,17 @@
var playlist_ver = 0; var playlist_ver = 0;
function request(url, _data, refresh=false){ function request(_url, _data, refresh=false){
console.log(_data);
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: 'post', url: _url,
data : _data, data : _data,
statusCode : { statusCode : {
200 : function(data) { 200 : function(data) {
if (data.ver !== playlist_ver) { if (data.ver !== playlist_ver) {
updatePlaylist();
playlist_ver = data.ver; playlist_ver = data.ver;
updatePlaylist();
} }
updateControls(data.empty, data.play, data.mode); updateControls(data.empty, data.play, data.mode);
} }
@ -374,24 +385,35 @@
} }
} }
var loading = $("#playlist-loading");
var table = $("#playlist-table");
function displayPlaylist(data){ function displayPlaylist(data){
// console.info(data); // console.info(data);
$("#playlist-table tr").remove(); table.animate({opacity: 0}, 200, function(){
loading.hide();
$("#playlist-table .playlist-item").remove();
var items = data.items; var items = data.items;
$.each(items, function(index, item){ $.each(items, function(index, item){
$("#playlist-table").append(item); table.append(item);
});
table.animate({opacity: 1}, 200);
}); });
} }
function updatePlaylist(){ function updatePlaylist(){
$.ajax({ table.animate({opacity: 0}, 200, function(){
type: 'GET', loading.show();
url: 'playlist', $("#playlist-table .playlist-item").css("opacity", 0);
statusCode : { $.ajax({
200 : displayPlaylist type: 'GET',
} url: 'playlist',
statusCode : {
200: displayPlaylist
}
});
table.animate({opacity: 1}, 200);
}); });
} }
@ -468,8 +490,8 @@
statusCode : { statusCode : {
200 : function(data){ 200 : function(data){
if(data.ver !== playlist_ver){ if(data.ver !== playlist_ver){
updatePlaylist();
playlist_ver = data.ver; playlist_ver = data.ver;
updatePlaylist();
} }
updateControls(data.empty, data.play, data.mode); updateControls(data.empty, data.play, data.mode);
} }
@ -477,7 +499,260 @@
}); });
} , 3000); } , 3000);
// ------ Browser ------
var filter_type = 'file';
var filter_dir = $("#filter-dir");
var filter_keywords = $("#filter-keywords");
var filter_btn_file = $("#filter-type-file");
var filter_btn_url = $("#filter-type-url");
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);
if(type === "file"){
filter_btn_file.removeClass("btn-secondary").addClass("btn-primary");
filter_dir.prop("disabled", false);
}else if(type === "url"){
filter_btn_url.removeClass("btn-secondary").addClass("btn-primary");
}else if(type === "radio"){
filter_btn_radio.removeClass("btn-secondary").addClass("btn-primary");
}
updateResults();
}
// Bind Event
var _tag = null;
$(".filter-tag").click(function (e) {
var tag = $(e.currentTarget);
_tag = tag;
if (!tag.hasClass('tag-clicked')) {
tag.addClass('tag-clicked');
} else {
tag.removeClass('tag-clicked');
}
updateResults();
});
filter_dir.change(function(){updateResults()});
filter_keywords.change(function(){updateResults()});
var item_template = $("#library-item");
function bindLibraryResultEvent() {
$(".library-item-play").unbind().click(
function (e) {
request('post', {
'add_item_at_once': $(e.currentTarget).parent().parent().find(".library-item-id").val()
});
}
);
$(".library-item-trash").unbind().click(
function (e) {
request('post', {
'delete_item_from_library': $(e.currentTarget).parent().parent().find(".library-item-id").val()
});
}
);
$(".library-item-download").unbind().click(
function (e) {
var id = $(e.currentTarget).parent().parent().find(".library-item-id").val();
//window.open('/download?id=' + id);
downloadId(id);
}
);
$(".library-item-add-next").unbind().click(
function (e) {
var id = $(e.currentTarget).parent().parent().parent().find(".library-item-id").val();
request('post', {
'add_item_next': id
});
}
);
$(".library-item-add-bottom").unbind().click(
function (e) {
var id = $(e.currentTarget).parent().parent().parent().find(".library-item-id").val();
request('post', {
'add_item_bottom': id
});
}
);
}
var lib_group = $("#library-group");
var id_element = $(".library-item-id");
var title_element = $(".library-item-title");
var artist_element = $(".library-item-artist");
var thumb_element = $(".library-item-thumb");
var type_element = $(".library-item-type");
var path_element = $(".library-item-path");
var notag_element = $(".library-item-notag");
var tag_element = $(".library-item-tag");
function addResultItem(item){
id_element.val(item.id);
title_element.html(item.title);
artist_element.html(item.artist ? ("- " + item.artist) : "");
thumb_element.attr("src", item.thumb);
type_element.html("[" + item.type + "]");
path_element.html(item.path);
var item_copy = item_template.clone();
item_copy.addClass("library-item-active");
var tags = item_copy.find(".library-item-tags");
tags.empty();
if(item.tags.length > 0){
item.tags.forEach(function (tag_tuple){
tag_copy = tag_element.clone();
tag_copy.html(tag_tuple[0]);
tag_copy.addClass("badge-" + tag_tuple[1]);
tag_copy.appendTo(tags);
});
}else{
tag_copy = notag_element.clone();
tag_copy.appendTo(tags);
}
item_copy.appendTo(lib_group);
item_copy.slideDown();
}
function getFilters(dest_page=1){
var tags = $(".tag-clicked");
var tags_list = [];
tags.each(function (index, tag){
tags_list.push(tag.innerHTML);
});
return {
type: filter_type,
dir: filter_dir.val(),
tags: tags_list.join(","),
keywords: filter_keywords.val(),
page: dest_page
};
}
var lib_loading = $("#library-item-loading");
var lib_empty = $("#library-item-empty");
function updateResults(dest_page=1){
data = getFilters(dest_page);
data.action = "query";
lib_group.animate({opacity: 0}, 200, function() {
$.ajax({
type: 'POST',
url : 'library',
data: data,
statusCode: {
200: processResults,
404: function(){
lib_loading.hide();
lib_empty.show();
}
}
});
$(".library-item-active").remove();
lib_empty.hide();
lib_loading.show();
lib_group.animate({opacity: 1}, 200);
});
}
var download_form = $("#download-form");
var download_id = download_form.find("input[name='id']");
var download_type = download_form.find("input[name='type']");
var download_dir = download_form.find("input[name='dir']");
var download_tags = download_form.find("input[name='tags']");
var download_keywords = download_form.find("input[name='keywords']");
function addAllResults(){
data = getFilters();
data.action = "add";
console.log(data);
$.ajax({
type: 'POST',
url : 'library',
data: data,
statusCode: {200: processResults}
});
updatePlaylist();
}
function downloadAllResults(){
cond = getFilters();
download_id.val();
download_type.val(cond.type);
download_dir.val(cond.dir);
download_tags.val(cond.tags);
download_keywords.val(cond.keywords);
download_form.submit();
}
function downloadId(id){
download_id.attr("value" ,id);
download_type.attr("value", "");
download_dir.attr("value", "");
download_tags.attr("value", "");
download_keywords.attr("value", "");
download_form.submit();
}
var page_ul = $("#library-page-ul");
var page_li = $(".library-page-li");
var page_no = $(".library-page-no");
function processResults(data){
lib_group.animate({opacity: 0}, 200, function() {
lib_loading.hide();
total_pages = data.total_pages;
active_page = data.active_page;
items = data.items;
items.forEach(
function (item) {
addResultItem(item);
bindLibraryResultEvent();
}
);
page_ul.empty();
page_li.removeClass('active').empty();
for (i = 1; i <= total_pages; i++) {
var page_li_copy = page_li.clone();
var page_no_copy = page_no.clone();
page_no_copy.html(i.toString());
if (active_page === i) {
page_li_copy.addClass("active");
} else {
page_no_copy.click(function (e) {
_page_no = $(e.currentTarget).html();
updateResults(_page_no);
});
}
page_no_copy.appendTo(page_li_copy);
page_li_copy.appendTo(page_ul);
}
lib_group.animate({opacity: 1}, 200);
});
}
themeInit(); themeInit();
updateResults();
$(document).ready(updatePlaylist); $(document).ready(updatePlaylist);
</script> </script>

View File

@ -1,18 +1,20 @@
{% if index == -1 %} {% if index == -1 %}
<tr class="table-dark"> <tr id="playlist-loading">
<td colspan="4" class="text-muted" style="text-align:center;"> Play list is empty. </td> <td colspan="4" style="text-align:center;">
<img style="margin: auto; width: 35px;" src="static/image/loading.svg" />
</td>
</tr> </tr>
{% else %} {% else %}
{% if index == playlist.current_index %} {% if index == playlist.current_index %}
<tr class="table-active"> <tr class="playlist-item table-active">
{% else %} {% else %}
<tr> <tr class="playlist-item">
{% endif %} {% endif %}
<th scope="row">{{ index + 1 }}</th> <th scope="row">{{ index + 1 }}</th>
<td> <td>
<div class="playlist-title"> <div class="playlist-title">
{% if m.type != 'radio' and m.thumbnail %} {% if m.type != 'radio' and m.thumbnail %}
<img width="80" src="data:image/PNG;base64,{{ m.thumbnail }}"/> <img width="80" src="data:image/PNG;base64,{{ m.thumbnail }}"/>
{% else %} {% else %}
<img width="80" src="static/image/unknown-album.png"/> <img width="80" src="static/image/unknown-album.png"/>
{% endif %} {% endif %}

16
util.py
View File

@ -59,35 +59,33 @@ def get_recursive_file_list_sorted(path):
return filelist return filelist
# - zips all files of the given zippath (must be a directory) # - zips files
# - returns the absolute path of the created zip file # - returns the absolute path of the created zip file
# - zip file will be in the applications tmp folder (according to configuration) # - zip file will be in the applications tmp folder (according to configuration)
# - format of the filename itself = prefix_hash.zip # - format of the filename itself = prefix_hash.zip
# - prefix can be controlled by the caller # - prefix can be controlled by the caller
# - hash is a sha1 of the string representation of the directories' contents (which are # - hash is a sha1 of the string representation of the directories' contents (which are
# zipped) # zipped)
def zipdir(zippath, zipname_prefix=None): def zipdir(files, zipname_prefix=None):
zipname = var.tmp_folder zipname = var.tmp_folder
if zipname_prefix and '../' not in zipname_prefix: if zipname_prefix and '../' not in zipname_prefix:
zipname += zipname_prefix.strip().replace('/', '_') + '_' zipname += zipname_prefix.strip().replace('/', '_') + '_'
files = get_recursive_file_list_sorted(zippath) _hash = hashlib.sha1(str(files).encode()).hexdigest()
hash = hashlib.sha1((str(files).encode())).hexdigest() zipname += _hash + '.zip'
zipname += hash + '.zip'
if os.path.exists(zipname): if os.path.exists(zipname):
return zipname return zipname
zipf = zipfile.ZipFile(zipname, 'w', zipfile.ZIP_DEFLATED) zipf = zipfile.ZipFile(zipname, 'w', zipfile.ZIP_DEFLATED)
for file in files: for file_to_add in files:
file_to_add = os.path.join(zippath, file)
if not os.access(file_to_add, os.R_OK): if not os.access(file_to_add, os.R_OK):
continue continue
if file in var.config.get('bot', 'ignored_files'): if file_to_add in var.config.get('bot', 'ignored_files'):
continue continue
add_file_as = os.path.relpath(os.path.join(zippath, file), os.path.join(zippath, '..')) add_file_as = os.path.basename(file_to_add)
zipf.write(file_to_add, add_file_as) zipf.write(file_to_add, add_file_as)
zipf.close() zipf.close()