From b2e5efec937411ebdf6782e7daba74f6440b0ed1 Mon Sep 17 00:00:00 2001 From: Terry Geng Date: Wed, 25 Mar 2020 11:38:49 +0800 Subject: [PATCH] refactor: optimized playlist logic --- command.py | 12 +-- interface.py | 63 ++++++++---- media/cache.py | 2 +- media/file.py | 4 +- media/item.py | 2 +- media/playlist.py | 12 +-- media/radio.py | 2 +- media/url.py | 2 +- mumbleBot.py | 4 +- static/css/custom.css | 8 +- templates/index.html | 205 +++++++++++++++++++++++++++++++++------- templates/playlist.html | 70 -------------- 12 files changed, 242 insertions(+), 144 deletions(-) delete mode 100644 templates/playlist.html diff --git a/command.py b/command.py index 3705fe5..9f24fd9 100644 --- a/command.py +++ b/command.py @@ -708,7 +708,7 @@ def cmd_remove(bot, user, text, command, parameter): if index == var.playlist.current_index: removed = var.playlist[index] bot.send_msg(constants.strings('removing_item', - item=removed.format_short_string()), text) + item=removed.format_title()), text) log.info("cmd: delete from playlist: " + removed.format_debug_string()) var.playlist.remove(index) @@ -781,10 +781,10 @@ def cmd_queue(bot, user, text, command, parameter): tags = "{}".format(", ".join(music.item().tags)) if i == var.playlist.current_index: newline = "{} ({}) {} {}".format(i + 1, music.display_type(), - music.format_short_string(), tags) + music.format_title(), tags) else: newline = '{} ({}) {} {}'.format(i + 1, music.display_type(), - music.format_short_string(), tags) + music.format_title(), tags) msgs.append(newline) @@ -883,7 +883,7 @@ def cmd_add_tag(bot, user, text, command, parameter): var.playlist[int(index) - 1].format_debug_string())) bot.send_msg(constants.strings("added_tags", tags=", ".join(tags), - song=var.playlist[int(index) - 1].format_short_string()), text) + song=var.playlist[int(index) - 1].format_title()), text) return elif index == "*": @@ -922,13 +922,13 @@ def cmd_remove_tag(bot, user, text, command, parameter): var.playlist[int(index) - 1].format_debug_string())) bot.send_msg(constants.strings("removed_tags", tags=", ".join(tags), - song=var.playlist[int(index) - 1].format_short_string()), text) + song=var.playlist[int(index) - 1].format_title()), text) return else: var.playlist[int(index) - 1].clear_tags() log.info("cmd: clear tags from song %s" % (var.playlist[int(index) - 1].format_debug_string())) bot.send_msg(constants.strings("cleared_tags", - song=var.playlist[int(index) - 1].format_short_string()), text) + song=var.playlist[int(index) - 1].format_title()), text) return elif index == "*": diff --git a/interface.py b/interface.py index e4e8d28..6096676 100644 --- a/interface.py +++ b/interface.py @@ -7,11 +7,14 @@ import util import math import os import os.path -import shutil -from werkzeug.utils import secure_filename import errno +from typing import Type import media -from media.item import dicts_to_items, dict_to_item +from media.item import dicts_to_items, dict_to_item, BaseItem +from media.file import FileItem +from media.url import URLItem +from media.url_from_playlist import PlaylistURLItem +from media.radio import RadioItem 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 @@ -58,7 +61,7 @@ class ReverseProxied(object): web = Flask(__name__) -#web.config['TEMPLATES_AUTO_RELOAD'] = True +web.config['TEMPLATES_AUTO_RELOAD'] = True log = logging.getLogger("bot") user = 'Remote Control' @@ -160,11 +163,7 @@ def index(): @requires_auth def playlist(): if len(var.playlist) == 0: - return jsonify({'items': [render_template('playlist.html', - m=False, - index=None - )] - }) + return ('', 204) tags_color_lookup = build_tags_color_lookup() # TODO: cached this? items = [] @@ -174,27 +173,55 @@ def playlist(): for tag in item_wrapper.item().tags: tag_tuples.append([tag, tags_color_lookup[tag]]) - items.append(render_template('playlist.html', - index=index, - tag_tuples=tag_tuples, - m=item_wrapper.item(), - playlist=var.playlist - ) - ) + item: Type[BaseItem] = item_wrapper.item() - return jsonify({'items': items}) + title = item.format_title() + artist = "??" + path = "" + if isinstance(item, FileItem): + path = item.path + if item.artist: + artist = item.artist + elif isinstance(item, URLItem): + path = f" { item.url }" + elif isinstance(item, PlaylistURLItem): + path = f" { item.url }" + artist = f" { item.playlist_title }" + elif isinstance(item, RadioItem): + path = f" { item.url }" + + thumb = "" + if item.type != 'radio' and item.thumbnail: + thumb = f"data:image/PNG;base64,{item.thumbnail}" + else: + thumb = "static/image/unknown-album.png" + + items.append({ + 'index': index, + 'id': item.id, + 'type': item.display_type(), + 'path': path, + 'title': title, + 'artist': artist, + 'thumbnail': thumb, + 'tags': tag_tuples, + }) + + return jsonify({'items': items, 'current_index': var.playlist.current_index}) def status(): if len(var.playlist) > 0: return jsonify({'ver': var.playlist.version, + 'current_index': var.playlist.current_index, 'empty': False, 'play': not var.bot.is_pause, 'mode': var.playlist.mode}) else: return jsonify({'ver': var.playlist.version, + 'current_index': var.playlist.current_index, 'empty': True, - 'play': False, + 'play': not var.bot.is_pause, 'mode': var.playlist.mode}) diff --git a/media/cache.py b/media/cache.py index b16e4eb..ba709a8 100644 --- a/media/cache.py +++ b/media/cache.py @@ -183,7 +183,7 @@ class CachedItemWrapper: return self.item().format_song_string(self.user) def format_short_string(self): - return self.item().format_short_string() + return self.item().format_title() def format_debug_string(self): return self.item().format_debug_string() diff --git a/media/file.py b/media/file.py index 146faf7..91f52a3 100644 --- a/media/file.py +++ b/media/file.py @@ -151,7 +151,7 @@ class FileItem(BaseItem): def format_debug_string(self): return "[file] {descrip} ({path})".format( - descrip=self.format_short_string(), + descrip=self.format_title(), path=self.path ) @@ -171,7 +171,7 @@ class FileItem(BaseItem): return display - def format_short_string(self): + def format_title(self): title = self.title if self.title else self.path if self.artist: return self.artist + " - " + title diff --git a/media/item.py b/media/item.py index c8d7067..91a4e21 100644 --- a/media/item.py +++ b/media/item.py @@ -95,7 +95,7 @@ class BaseItem: def format_current_playing(self, user): return self.id - def format_short_string(self): + def format_title(self): return self.title def format_debug_string(self): diff --git a/media/playlist.py b/media/playlist.py index 8affbb3..41798f1 100644 --- a/media/playlist.py +++ b/media/playlist.py @@ -90,8 +90,6 @@ class BasePlaylist(list): if len(self) == 0: return False - self.version += 1 - if self.current_index < len(self) - 1: self.current_index += 1 return self[self.current_index] @@ -99,7 +97,6 @@ class BasePlaylist(list): return False def point_to(self, index): - self.version += 1 if -1 <= index < len(self): self.current_index = index @@ -134,12 +131,14 @@ class BasePlaylist(list): return removed def remove_by_id(self, id): - self.version += 1 to_be_removed = [] for index, wrapper in enumerate(self): if wrapper.id == id: to_be_removed.append(index) + if to_be_removed: + self.version += 1 + for index in to_be_removed: self.remove(index) @@ -312,8 +311,6 @@ class RepeatPlaylist(BasePlaylist): if len(self) == 0: return False - self.version += 1 - if self.current_index < len(self) - 1: self.current_index += 1 return self[self.current_index] @@ -345,12 +342,11 @@ class RandomPlaylist(BasePlaylist): if len(self) == 0: return False - self.version += 1 - if self.current_index < len(self) - 1: self.current_index += 1 return self[self.current_index] else: + self.version += 1 self.randomize() self.current_index = 0 return self[0] diff --git a/media/radio.py b/media/radio.py index 65b1c59..f7e10e5 100644 --- a/media/radio.py +++ b/media/radio.py @@ -157,7 +157,7 @@ class RadioItem(BaseItem): def format_current_playing(self, user): return constants.strings("now_playing", item=self.format_song_string(user)) - def format_short_string(self): + def format_title(self): return self.title if self.title else self.url def display_type(self): diff --git a/media/url.py b/media/url.py index 80ac818..2ae011f 100644 --- a/media/url.py +++ b/media/url.py @@ -242,7 +242,7 @@ class URLItem(BaseItem): return display - def format_short_string(self): + def format_title(self): return self.title if self.title.strip() else self.url def display_type(self): diff --git a/mumbleBot.py b/mumbleBot.py index 27268fd..01e7680 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -450,7 +450,7 @@ class MumbleBot: self.log.error("bot: with ffmpeg error: %s", self.last_ffmpeg_err) self.last_ffmpeg_err = "" - self.send_msg(constants.strings('unable_play', item=current.format_short_string())) + self.send_msg(constants.strings('unable_play', item=current.format_title())) var.playlist.remove_by_id(current.id) var.cache.free_and_delete(current.id) @@ -466,7 +466,7 @@ class MumbleBot: self.log.info("bot: current music isn't ready, start downloading.") self.wait_for_downloading = True var.playlist.async_prepare(var.playlist.current_index) - self.send_msg(constants.strings('download_in_progress', item=current.format_short_string())) + self.send_msg(constants.strings('download_in_progress', item=current.format_title())) else: var.playlist.remove_by_id(current.id) var.cache.free_and_delete(current.id) diff --git a/static/css/custom.css b/static/css/custom.css index 77422b6..fd152eb 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -4,7 +4,13 @@ .tag-space{margin-right:3px;} .playlist-title-td{width:60%} .playlist-title{float:left; } -.playlist-artwork{float:left; margin-left:10px;} +.playlist-item {transition: all 0.2s ease-in-out;} +.playlist-artwork{ + float:left; + margin-left:10px; + white-space: nowrap; + overflow: hidden; +} .tag-click{ cursor:pointer; transition: 400ms; diff --git a/templates/index.html b/templates/index.html index 3a4d4db..ec653f9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -89,6 +89,44 @@ + + + + + + + + + +
+ +
+
+ + +
+ +
+ +
+ +
+
+ + + + + +
+ + +
+ + @@ -410,7 +448,34 @@ $(event.target).closest('form').submit(); }); + + // ---------------------- + // ------ Playlist ------ + // ---------------------- + + + var pl_item_template = $(".playlist-item-template"); + var pl_id_element = $(".playlist-item-id"); + var pl_index_element = $(".playlist-item-index"); + var pl_title_element = $(".playlist-item-title"); + var pl_artist_element = $(".playlist-item-artist"); + var pl_thumb_element = $(".playlist-item-thumbnail"); + var pl_type_element = $(".playlist-item-type"); + var pl_path_element = $(".playlist-item-path"); + + var pl_tag_edit_element = $(".playlist-item-edit"); + + var notag_element = $(".library-item-notag"); // these elements are shared with library + var tag_element = $(".library-item-tag"); + + var add_tag_modal = $("#addTagModal"); + + var playlist_loading = $("#playlist-loading"); + var playlist_table = $("#playlist-table"); + var playlist_empty = $("#playlist-empty"); + var playlist_ver = 0; + var playlist_current_index = 0; function request(_url, _data, refresh=false){ console.log(_data); @@ -433,38 +498,124 @@ } } - var loading = $("#playlist-loading"); - var table = $("#playlist-table"); - function displayPlaylist(data){ - // console.info(data); - table.animate({opacity: 0}, 200, function(){ - loading.hide(); - $("#playlist-table .playlist-item").remove(); + function addPlaylistItem(item){ + pl_id_element.val(item.id); + pl_index_element.html(item.index + 1); + pl_title_element.html(item.title); + pl_artist_element.html(item.artist); + pl_thumb_element.attr("src", item.thumbnail); + pl_type_element.html(item.type); + pl_path_element.html(item.path); - var items = data.items; - $.each(items, function(index, item){ - table.append(item); + var item_copy = pl_item_template.clone(); + item_copy.attr("id", "playlist-item-" + item.index); + item_copy.addClass("playlist-item"); + + var tags = item_copy.find(".playlist-item-tags"); + tags.empty(); + + var tag_edit_copy = pl_tag_edit_element.clone(); + tag_edit_copy.click(function(){ + addTagModalShow(item.id, item.title, item.tags); + }); + tag_edit_copy.appendTo(tags); + + 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); + } - table.animate({opacity: 1}, 200); + item_copy.appendTo(playlist_table); + item_copy.show(); + } + + function displayPlaylist(data){ + playlist_table.animate({opacity: 0}, 200, function(){ + playlist_loading.hide(); + $(".playlist-item").remove(); + var items = data.items; + items.forEach( + function (item) { + addPlaylistItem(item); + bindPlaylistEvent(); + } + ); + + displayActiveItem(data.current_index); + playlist_table.animate({opacity: 1}, 200); }); } + function displayActiveItem(current_index){ + $(".playlist-item").removeClass("table-active"); + $("#playlist-item-"+current_index).addClass("table-active"); + } + function updatePlaylist(){ - table.animate({opacity: 0}, 200, function(){ - loading.show(); + playlist_table.animate({opacity: 0}, 200, function(){ + playlist_empty.hide(); + playlist_loading.show(); $("#playlist-table .playlist-item").css("opacity", 0); $.ajax({ type: 'GET', url: 'playlist', statusCode : { - 200: displayPlaylist + 200: displayPlaylist, + 204: function(){ + playlist_loading.hide(); + playlist_empty.show(); + $(".playlist-item").remove(); + } } }); - table.animate({opacity: 1}, 200); + playlist_table.animate({opacity: 1}, 200); }); } + function checkForPlaylistUpdate(){ + $.ajax({ + type: 'POST', + url : 'post', + statusCode : { + 200 : function(data){ + if(data.ver !== playlist_ver){ + playlist_ver = data.ver; + updatePlaylist(); + } + if(data.current_index !== playlist_current_index){ + playlist_current_index = data.current_index; + displayActiveItem(data.current_index); + } + updateControls(data.empty, data.play, data.mode); + } + } + }); + } + + function bindPlaylistEvent() { + $(".playlist-item-play").unbind().click( + function (e) { + request('post', { + 'play_music': ($(e.currentTarget).parent().parent().parent().find(".playlist-item-index").html() - 1) + }); + } + ); + $(".playlist-item-trash").unbind().click( + function (e) { + request('post', { + 'delete_music': ($(e.currentTarget).parent().parent().parent().find(".playlist-item-index").html() - 1) + }); + } + ); + } + function updateControls(empty, play, mode){ if(empty){ $("#play-btn").prop("disabled", true); @@ -531,23 +682,11 @@ } // Check the version of playlist to see if update is needed. - setInterval(function(){ - $.ajax({ - type: 'POST', - url : 'post', - statusCode : { - 200 : function(data){ - if(data.ver !== playlist_ver){ - playlist_ver = data.ver; - updatePlaylist(); - } - updateControls(data.empty, data.play, data.mode); - } - } - }); - } , 3000); + setInterval(checkForPlaylistUpdate , 3000); + // --------------------- // ------ Browser ------ + // --------------------- var filter_file = false; var filter_url = false; var filter_radio = false; @@ -665,10 +804,10 @@ var path_element = $(".library-item-path"); var tag_edit_element = $(".library-item-edit"); - var notag_element = $(".library-item-notag"); - var tag_element = $(".library-item-tag"); + //var notag_element = $(".library-item-notag"); + //var tag_element = $(".library-item-tag"); - var add_tag_modal = $("#addTagModal"); + //var add_tag_modal = $("#addTagModal"); function addResultItem(item){ id_element.val(item.id); diff --git a/templates/playlist.html b/templates/playlist.html deleted file mode 100644 index 07f6994..0000000 --- a/templates/playlist.html +++ /dev/null @@ -1,70 +0,0 @@ -{% if index == None %} - - - - - -{% else %} - {% if index == playlist.current_index %} - - {% else %} - - {% endif %} -{{ index + 1 }} - -
- {% if m.type != 'radio' and m.thumbnail %} - - {% else %} - - {% endif %} -
-
- {% if m.title.strip() %} - {{ m.title|truncate(45) }} - {% elif m.url %} - {{ m.url|truncate(45) }} - {% endif %} - {{ m.display_type() }} -
- {% if m.type == 'file' %} - {% if m.artist %} - {{ m.artist }} - {% else %} - ?? - {% endif %} - {% elif m.type == 'url_from_playlist' %} - {{ m.playlist_title|truncate(50) }} - {% else %} - Unknown Artist - {% endif %} -
- - - {% for tag_tuple in tag_tuples %} - {{ tag_tuple[0] }} - {% endfor %} -
- - - {% if m.type == 'url' or m.type == 'radio' or m.type == 'url_from_playlist' %} - {{ m.url|truncate(50) }} - {% elif m.type == 'file' %} - {{ m.path|truncate(50) }} - {% endif %} - - -
- - -
- - -{% endif %}