FEAT: MUSIC LIBRARY BROSWER
This commit is contained in:
39
command.py
39
command.py
@ -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)
|
||||||
|
|
||||||
|
182
database.py
182
database.py
@ -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()
|
||||||
|
|
||||||
|
219
interface.py
219
interface.py
@ -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__':
|
||||||
|
@ -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):
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
1
static/image/empty_box.svg
Normal file
1
static/image/empty_box.svg
Normal 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
1
static/image/loading.svg
Normal 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 |
@ -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>
|
||||||
|
@ -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
16
util.py
@ -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()
|
||||||
|
Reference in New Issue
Block a user