From 1f9aa0d61e1c07e6031ef708e39121b6f6e01483 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Sun, 16 Feb 2025 18:11:13 +0100 Subject: [PATCH] For /files, move towards live-updating view by filter via fetch GET /files.json. --- src/templates/_base.tmpl | 35 +++++++++++++------ src/templates/files.tmpl | 68 ++++++++++++++++++++++--------------- src/templates/playlist.tmpl | 9 ++--- src/ytplom/http.py | 29 ++++++++-------- src/ytplom/misc.py | 11 ++++++ 5 files changed, 96 insertions(+), 56 deletions(-) diff --git a/src/templates/_base.tmpl b/src/templates/_base.tmpl index 2ac0b99..cad70fa 100644 --- a/src/templates/_base.tmpl +++ b/src/templates/_base.tmpl @@ -37,21 +37,34 @@ function connect_events() { else { console.log("Error does not seem connection-related, therefore aborting."); }}} -async function send_to(data, target, verbose=false) { - if (verbose) { console.log(`Trying to send to ${target}:`, data); } +async function wrapped_fetch(target, fetch_kwargs=null, verbose=false) { + if (verbose) { + console.log(`Trying to fetch ${target}, kwargs:`, fetch_kwargs); + } try { - const response = await fetch(target, { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify(data) }); + const response = await (fetch_kwargs ? fetch(target, fetch_kwargs) : fetch(target)); if (200 != response.status) { - console.log(`Got unexpected response on sending to ${target}:`, response); } - else if (verbose) { console.log("Got response:", response); }} - catch(error) { - console.log(`Error on sending to ${target}:`, error); }} + console.log(`Got unexpected fetch response from ${target}:`, response); + } else { + return response; + } + } catch(error) { + console.log(`Error on sending to ${target}:`, error); + } +} + +async function post_to(data, target, verbose=false) { + const fetch_kwargs = { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(data) + }; + wrapped_fetch(target, fetch_kwargs, verbose); +} function player_command(command) { - send_to({command: [command]}, PATH_PLAYER); } + post_to({command: [command]}, PATH_PLAYER); +} event_handlers.push(function(data) { // update player state for (const [id, text] of [ diff --git a/src/templates/files.tmpl b/src/templates/files.tmpl index e0fc5b6..70c0877 100644 --- a/src/templates/files.tmpl +++ b/src/templates/files.tmpl @@ -3,9 +3,10 @@ {% block script %} {{ macros.js_new_child_to() }} +const PATH_FILES_JSON = "/{{page_names.files_json}}"; const all_tags = {{showable_tags|tojson|safe}}; -var needed_tags = {{needed_tags|tojson|safe}}; +var needed_tags = []; function select_tag() { if (tags_select.selectedIndex < 1) { @@ -13,15 +14,15 @@ function select_tag() { } const chosen_tag = document.getElementById('tags_select').value; needed_tags.push(chosen_tag); - reload_selector(); + update_filter_inputs(); } -function reload_selector() { +function update_filter_inputs() { const tags_select = document.getElementById('tags_select'); while (tags_select.options.length > 0) { tags_select.remove(0); } - new_child_to('option', tags_select, 'add tag'); + new_child_to('option', tags_select, 'add'); all_tags.forEach((tag) => { if (needed_tags.includes(tag)) { return; @@ -38,40 +39,53 @@ function reload_selector() { tag_text_node.remove(); btn_del.remove(); needed_tags = needed_tags.filter(tag => tag !== chosen_tag); - reload_selector(); + update_filter_inputs(); }; - const input = new_child_to('input', tags_div); - input.type = 'hidden'; - input.name = 'needed_tag'; - input.value = chosen_tag; }); + update_files_list(); } -window.addEventListener('load', reload_selector); +async function update_files_list() { + const filter_path = encodeURIComponent(document.getElementById("input_filter_path").value); + let target = `${PATH_FILES_JSON}?filter_path=${filter_path}`; + if (document.getElementById("input_show_absent").checked) { target = `${target}&show_absent=1`; } + needed_tags.forEach((tag) => target = `${target}&needed_tag=${encodeURIComponent(tag)}`); + const files_data = await wrapped_fetch(target).then((response) => response.json()); + document.getElementById("files_count").textContent = `${files_data.length}`; + const table = document.getElementById("files_table"); + Array.from(document.getElementsByClassName("file_row")).forEach((row) => row.remove()); + files_data.forEach((file) => { + const tr = new_child_to('tr', table); + tr.classList.add("file_row"); + new_child_to('td', tr, file.size); + const td_play = new_child_to('td', tr); + const btn_play = new_child_to('input', td_play); + btn_play.type = 'submit'; + btn_play.name = `play_${file.digest}`; + btn_play.value = 'play'; + btn_play.disabled = !file.present; + new_child_to('td', tr, file.tags_showable.join(", ")); + const td_link = new_child_to('td', tr); + const a = new_child_to('a', td_link, file.rel_path); + a.href = `${PATH_PREFIX_FILE}${file.digest}`; + }); +} + +window.addEventListener('load', update_filter_inputs); {% endblock %} {% block body %} -
- show absent
-filename:
-tags:
-
- -
-

known files (shown: {{files|length}}):

+filename pattern:
+show absent:
+needed tags:
+
+
+

known files (shown: ?):

- +
-{% for file in files %} - - - - - - -{% endfor %}
sizeactionstagspath
{{ file.size | round(1) }}{{file.tags_showable.joined}}{{file.rel_path}}
{% endblock %} diff --git a/src/templates/playlist.tmpl b/src/templates/playlist.tmpl index a27d4a2..669e723 100644 --- a/src/templates/playlist.tmpl +++ b/src/templates/playlist.tmpl @@ -41,10 +41,11 @@ event_handlers.push(function(data) { // update playlist a_file.href = `${PATH_PREFIX_FILE}${file.digest}`; }}) function redo_playlist() { - send_to({filter_path: [document.getElementsByName('filter_path')[0].value], - needed_tags: [document.getElementsByName('needed_tags')[0].value]}, - PATH_PLAYER); - player_command('reload'); } + post_to({filter_path: [document.getElementsByName('filter_path')[0].value], + needed_tags: [document.getElementsByName('needed_tags')[0].value]}, + PATH_PLAYER); + player_command('reload'); +} {% endblock %} diff --git a/src/ytplom/http.py b/src/ytplom/http.py index a17b00e..15e3a46 100644 --- a/src/ytplom/http.py +++ b/src/ytplom/http.py @@ -41,6 +41,7 @@ PAGE_NAMES: dict[str, str] = { 'events': 'events', 'file': 'file', 'files': 'files', + 'files_json': 'files.json', 'missing': 'missing', 'player': 'player', 'playlist': 'playlist', @@ -198,6 +199,8 @@ class _TaskHandler(PlomHttpHandler): self._send_file_data() elif self.pagename == PAGE_NAMES['files']: self._send_files_index() + elif self.pagename == PAGE_NAMES['files_json']: + self._send_files_json() elif self.pagename == PAGE_NAMES['missing']: self._send_missing_json() elif self.pagename == PAGE_NAMES['thumbnails']: @@ -300,24 +303,22 @@ class _TaskHandler(PlomHttpHandler): 'unused_tags': unused_tags}) def _send_files_index(self) -> None: - filter_path = FilterStr(self.params.first_for('filter_path')) - show_absent = bool(self.params.first_for('show_absent')) - needed_tags_list = self.params.all_for('needed_tag') + with DbConn() as conn: + showable_tags = sorted(list(VideoFile.all_tags_showable(conn))) + self._send_rendered_template(_NAME_TEMPLATE_FILES, + {'showable_tags': showable_tags}) + + def _send_files_json(self) -> None: with DbConn() as conn: files = VideoFile.get_filtered( conn, - filter_path, - TagSet(set(needed_tags_list)), - show_absent) - showable_tags = sorted(list(VideoFile.all_tags_showable(conn))) + FilterStr(self.params.first_for('filter_path')), + TagSet(set(self.params.all_for('needed_tag'))), + bool(self.params.first_for('show_absent'))) files.sort(key=lambda t: t.rel_path) - self._send_rendered_template(_NAME_TEMPLATE_FILES, - {'files': files, - 'selected': 'files', - 'filter_path': filter_path, - 'showable_tags': showable_tags, - 'needed_tags': needed_tags_list, - 'show_absent': show_absent}) + self.send_http(bytes(json_dumps([f.as_dict for f in files]), + encoding='utf8'), + headers=[(_HEADER_CONTENT_TYPE, MIME_APP_JSON)]) def _send_missing_json(self) -> None: with DbConn() as conn: diff --git a/src/ytplom/misc.py b/src/ytplom/misc.py index 010a2fb..ac017ed 100644 --- a/src/ytplom/misc.py +++ b/src/ytplom/misc.py @@ -358,6 +358,17 @@ class VideoFile(DbData): def _renew_last_update(self): self.last_update = DatetimeStr(datetime.now().strftime(TIMESTAMP_FMT)) + @property + def as_dict(self) -> dict[str, bool | str | list[str]]: + """Return dict of values relevant for /files.""" + return { + 'digest': self.digest.b64, + 'present': self.present, + 'rel_path': str(self.rel_path), + 'size': f'{self.size:.2f}', + 'tags_showable': self.tags_showable.as_str_list, + } + def save(self, conn: DbConn) -> None: """Extend super().save by new .last_update if sufficient changes.""" if hash(self) != self._hash_on_last_update: -- 2.30.2