From fb7101a581889897f7c601f09739876033af577a Mon Sep 17 00:00:00 2001 From: Terry Geng Date: Thu, 19 Mar 2020 22:51:32 +0800 Subject: [PATCH] FEAT: MUSIC LIBRARY BROSWER --- command.py | 39 ++- database.py | 182 ++++++++---- interface.py | 219 ++++++++++----- media/cache.py | 16 +- static/css/custom.css | 78 ++++- static/image/empty_box.svg | 1 + static/image/loading.svg | 1 + templates/index.html | 563 +++++++++++++++++++++++++++---------- templates/playlist.html | 26 +- util.py | 16 +- 10 files changed, 835 insertions(+), 306 deletions(-) create mode 100644 static/image/empty_box.svg create mode 100644 static/image/loading.svg diff --git a/command.py b/command.py index 28c38d8..b5f1e19 100644 --- a/command.py +++ b/command.py @@ -10,7 +10,8 @@ import variables as var from librb import radiobrowser from database import SettingsDatabase, MusicDatabase 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 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} files = var.cache.dir.get_files(parameter) if files: + folder = parameter + if not folder.endswith('/'): + folder += '/' + msgs = [constants.strings('multiple_file_added')] count = 0 for file in files: 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) log.info("cmd: add to playlist: " + music_wrapper.format_debug_string()) 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) song_shortlist = music_dicts - for item in items: - count += 1 - if len(item.tags) > 0: - msgs.append("
  • {:d} - [{}] {} ({})
  • ".format(count, item.display_type(), item.title, ", ".join(item.tags))) - else: - msgs.append("
  • {:d} - [{}] {}
  • ".format(count, item.display_type(), item.title, ", ".join(item.tags))) - - if count != 0: - msgs.append("") - msgs.append(constants.strings("shortlist_instruction")) - send_multi_lines(bot, msgs, text, "") + if len(items) == 1: + music_wrapper = get_cached_wrapper(items[0], user) + var.playlist.append(music_wrapper) + log.info("cmd: add to playlist: " + music_wrapper.format_debug_string()) + bot.send_msg(constants.strings('file_added', item=music_wrapper.format_song_string())) else: - bot.send_msg(constants.strings("no_file"), text) + for item in items: + count += 1 + if len(item.tags) > 0: + msgs.append("
  • {:d} - [{}] {} ({})
  • ".format(count, item.display_type(), item.title, ", ".join(item.tags))) + else: + msgs.append("
  • {:d} - [{}] {}
  • ".format(count, item.display_type(), item.title, ", ".join(item.tags))) + + if count != 0: + msgs.append("") + msgs.append(constants.strings("shortlist_instruction")) + send_multi_lines(bot, msgs, text, "") + else: + bot.send_msg(constants.strings("no_file"), text) else: bot.send_msg(constants.strings("no_file"), text) diff --git a/database.py b/database.py index 7a53c88..2293f84 100644 --- a/database.py +++ b/database.py @@ -6,6 +6,112 @@ import datetime class DatabaseError(Exception): 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: version = 1 @@ -199,19 +305,21 @@ class MusicDatabase: conn.close() return tags - def query_music(self, **kwargs): - condition = [] - filler = [] + def query_music_count(self, condition: Condition): + filler = condition.filler + condition_str = condition.sql() - for key, value in kwargs.items(): - if isinstance(value, str): - condition.append(key + "=?") - filler.append(value) - elif isinstance(value, dict): - condition.append(key + " " + value[0] + " ?") - filler.append(value[1]) + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + results = cursor.execute("SELECT COUNT(*) FROM music " + "WHERE %s" % condition_str, filler).fetchall() + conn.close() - 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) cursor = conn.cursor() @@ -222,54 +330,39 @@ class MusicDatabase: return self._result_to_dict(results) def query_music_by_keywords(self, keywords): - condition = [] - filler = [] + condition = Condition() for keyword in keywords: - condition.append('LOWER(title) LIKE ?') - filler.append("%{:s}%".format(keyword.lower())) - - condition_str = " AND ".join(condition) + condition.and_like("title", f"%{keyword}%", case_sensitive=False) conn = sqlite3.connect(self.db_path) cursor = conn.cursor() 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() return self._result_to_dict(results) def query_music_by_tags(self, tags): - condition = [] - filler = [] + condition = Condition() for tag in tags: - condition.append('LOWER(tags) LIKE ?') - filler.append("%{:s},%".format(tag.lower())) - - condition_str = " AND ".join(condition) + condition.and_like("tags", f"%{tag},%", case_sensitive=False) conn = sqlite3.connect(self.db_path) cursor = conn.cursor() 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() 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) cursor = conn.cursor() - results = [] - - 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) + results = cursor.execute("SELECT id, tags FROM music " + "WHERE %s" % condition.sql(), condition.filler).fetchall() conn.close() @@ -309,24 +402,11 @@ class MusicDatabase: else: return [] - def delete_music(self, **kwargs): - 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) - + def delete_music(self, condition: Condition): conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute("DELETE FROM music " - "WHERE %s" % condition_str, filler) + "WHERE %s" % condition.sql(), condition.filler) conn.commit() conn.close() diff --git a/interface.py b/interface.py index 70b95ef..43f09a0 100644 --- a/interface.py +++ b/interface.py @@ -4,13 +4,17 @@ from functools import wraps from flask import Flask, render_template, request, redirect, send_file, Response, jsonify, abort import variables as var import util +import math import os import os.path import shutil from werkzeug.utils import secure_filename import errno 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 time @@ -126,7 +130,8 @@ def build_path_tags_lookup(): path_tags_lookup = {} ids = list(var.cache.file_id_lookup.values()) 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(): path_tags_lookup[path] = id_tags_lookup[id] @@ -207,45 +212,32 @@ def post(): if request.method == 'POST': if 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) - log.info('web: add to playlist(bottom): ' + music_wrapper.format_debug_string()) - - 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) + if 'add_item_at_once' in request.form: + music_wrapper = get_cached_wrapper_by_id(var.bot, request.form['add_item_at_once'], user) + if music_wrapper: var.playlist.insert(var.playlist.current_index + 1, music_wrapper) 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']): - try: - folder = request.form['add_folder'] - except: - folder = request.form['add_folder_recursively'] + if 'add_item_bottom' in request.form: + music_wrapper = get_cached_wrapper_by_id(var.bot, request.form['add_item_bottom'], user) - if not folder.endswith('/'): - folder += '/' + if music_wrapper: + 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): - dir = var.cache.dir - if 'add_folder_recursively' in request.form: - files = dir.get_files_recursively(folder) - else: - files = dir.get_files(folder) - - 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_item_next' in request.form: + music_wrapper = get_cached_wrapper_by_id(var.bot, request.form['add_item_next'], user) + if music_wrapper: + var.playlist.insert(var.playlist.current_index + 1, music_wrapper) + log.info('web: add to playlist(next): ' + music_wrapper.format_debug_string()) + else: + abort(404) elif 'add_url' in request.form: 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() +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"]) def upload(): @@ -374,19 +462,19 @@ def upload(): files = request.files.getlist("file[]") if not files: - return redirect("./", code=406) + return redirect("./", code=400) # filename = secure_filename(file.filename).strip() for file in files: filename = file.filename if filename == '': - return redirect("./", code=406) + return redirect("./", code=400) targetdir = request.form['targetdir'].strip() if targetdir == '': targetdir = 'uploads/' elif '../' in targetdir: - return redirect("./", code=406) + return redirect("./", code=400) log.info('web: Uploading file from %s:' % request.remote_addr) log.info('web: - filename: ' + filename) @@ -397,7 +485,7 @@ def upload(): storagepath = os.path.abspath(os.path.join(var.music_folder, targetdir)) print('storagepath:', storagepath) if not storagepath.startswith(os.path.abspath(var.music_folder)): - return redirect("./", code=406) + return redirect("./", code=400) try: os.makedirs(storagepath) @@ -424,37 +512,34 @@ def upload(): def download(): global log - if 'file' in request.args: - requested_file = request.args['file'] + print('id' in request.args) + 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)) - if '../' not in requested_file: - folder_path = var.music_folder - files = var.cache.files - if requested_file in files: - filepath = os.path.join(folder_path, requested_file) - try: - return send_file(filepath, as_attachment=True) - except Exception as e: - 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) + try: + return send_file(requested_file, 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__': diff --git a/media/cache.py b/media/cache.py index 1124651..3773355 100644 --- a/media/cache.py +++ b/media/cache.py @@ -9,7 +9,7 @@ import media.file import media.url import media.url_from_playlist import media.radio -from database import MusicDatabase +from database import MusicDatabase, Condition import variables as var import util @@ -21,7 +21,7 @@ class MusicCache(dict): self.log = logging.getLogger("bot") self.dir = None 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() 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 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: music_dict = music_dicts[0] self[id] = dict_to_item(bot, music_dict) @@ -101,7 +101,7 @@ class MusicCache(dict): if item.id in self: 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): if id in self: @@ -222,6 +222,11 @@ class CachedItemWrapper: # 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): item = var.cache.get_item(bot, **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): item = dict_to_item(bot, dict_from_db) - var.cache[dict_from_db['id']] = item - return CachedItemWrapper(var.cache, item.id, item.type, user) + return get_cached_wrapper(item, user) def get_cached_wrapper_by_id(bot, id, user): diff --git a/static/css/custom.css b/static/css/custom.css index b099b71..f7a95eb 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,10 +1,17 @@ .bs-docs-section{margin-top:4em} .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{float:left; } .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 { width: 50px; height: 50px; @@ -20,9 +27,74 @@ right: 50px; bottom: 40px; } - .floating-button:hover { background-color: hsl(0, 0%, 43%); 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; +} diff --git a/static/image/empty_box.svg b/static/image/empty_box.svg new file mode 100644 index 0000000..f896567 --- /dev/null +++ b/static/image/empty_box.svg @@ -0,0 +1 @@ + diff --git a/static/image/loading.svg b/static/image/loading.svg new file mode 100644 index 0000000..6bbd482 --- /dev/null +++ b/static/image/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 3a5ebf4..ab132f3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,107 +1,3 @@ -{% macro dirlisting(dir, path='') -%} - -{%- endmacro %} - @@ -186,9 +82,11 @@ Action - - - Fetching playlist .... + + + + + @@ -229,28 +127,133 @@
    -
    -
    -

    Files

    -
    +
    +
    +

    Browser

    +
    + +
    +
    +

    Filters

    +
    +
    +
    + +
    + + + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    + +
    + {% for tag in tags_color_lookup.keys() %} + {{ tag }} + {% endfor %} +
    +
    +
    +
    + +
    + +
    +
    + +
    + + + +
    + +
    +
    +
      +
    • + 1 +
    • +
    +
    +
    -
    -
    - - -
    -
    - - -
    + +
    -
    - {{ dirlisting(music_library) }}
    @@ -283,10 +286,9 @@ -
    + onclick="var $i = $('#add_url_input')[0]; request('post', {add_url : $i.value }); $i.value = ''; ">Add URL
    @@ -326,7 +328,7 @@
    + onclick="var $i = $('#add_radio_input')[0]; request('post', {add_radio : $i.value }); $i.value = '';">Add Radio
    @@ -337,6 +339,14 @@
    +
    + + + + + +
    + @@ -354,16 +364,17 @@ var playlist_ver = 0; - function request(url, _data, refresh=false){ + function request(_url, _data, refresh=false){ + console.log(_data); $.ajax({ type: 'POST', - url: 'post', + url: _url, data : _data, statusCode : { 200 : function(data) { if (data.ver !== playlist_ver) { - updatePlaylist(); playlist_ver = data.ver; + updatePlaylist(); } updateControls(data.empty, data.play, data.mode); } @@ -374,24 +385,35 @@ } } + var loading = $("#playlist-loading"); + var table = $("#playlist-table"); function displayPlaylist(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; - $.each(items, function(index, item){ - $("#playlist-table").append(item); + var items = data.items; + $.each(items, function(index, item){ + table.append(item); + }); + + table.animate({opacity: 1}, 200); }); - } function updatePlaylist(){ - $.ajax({ - type: 'GET', - url: 'playlist', - statusCode : { - 200 : displayPlaylist - } + table.animate({opacity: 0}, 200, function(){ + loading.show(); + $("#playlist-table .playlist-item").css("opacity", 0); + $.ajax({ + type: 'GET', + url: 'playlist', + statusCode : { + 200: displayPlaylist + } + }); + table.animate({opacity: 1}, 200); }); } @@ -468,8 +490,8 @@ statusCode : { 200 : function(data){ if(data.ver !== playlist_ver){ - updatePlaylist(); playlist_ver = data.ver; + updatePlaylist(); } updateControls(data.empty, data.play, data.mode); } @@ -477,7 +499,260 @@ }); } , 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(); + updateResults(); $(document).ready(updatePlaylist); diff --git a/templates/playlist.html b/templates/playlist.html index c7967b8..32428ac 100644 --- a/templates/playlist.html +++ b/templates/playlist.html @@ -1,18 +1,20 @@ {% if index == -1 %} - - Play list is empty. + + + + {% else %} -{% if index == playlist.current_index %} - -{% else %} - -{% endif %} - {{ index + 1 }} - -
    - {% if m.type != 'radio' and m.thumbnail %} - + {% if index == playlist.current_index %} + + {% else %} + + {% endif %} +{{ index + 1 }} + +
    + {% if m.type != 'radio' and m.thumbnail %} + {% else %} {% endif %} diff --git a/util.py b/util.py index 5698b4e..20456b7 100644 --- a/util.py +++ b/util.py @@ -59,35 +59,33 @@ def get_recursive_file_list_sorted(path): return filelist -# - zips all files of the given zippath (must be a directory) +# - zips files # - returns the absolute path of the created zip file # - zip file will be in the applications tmp folder (according to configuration) # - format of the filename itself = prefix_hash.zip # - prefix can be controlled by the caller # - hash is a sha1 of the string representation of the directories' contents (which are # zipped) -def zipdir(zippath, zipname_prefix=None): +def zipdir(files, zipname_prefix=None): zipname = var.tmp_folder if zipname_prefix and '../' not in zipname_prefix: zipname += zipname_prefix.strip().replace('/', '_') + '_' - files = get_recursive_file_list_sorted(zippath) - hash = hashlib.sha1((str(files).encode())).hexdigest() - zipname += hash + '.zip' + _hash = hashlib.sha1(str(files).encode()).hexdigest() + zipname += _hash + '.zip' if os.path.exists(zipname): return zipname zipf = zipfile.ZipFile(zipname, 'w', zipfile.ZIP_DEFLATED) - for file in files: - file_to_add = os.path.join(zippath, file) + for file_to_add in files: if not os.access(file_to_add, os.R_OK): continue - if file in var.config.get('bot', 'ignored_files'): + if file_to_add in var.config.get('bot', 'ignored_files'): 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.close()