home · contact · privacy
For /files, move towards live-updating view by filter via fetch GET /files.json. master
authorChristian Heller <c.heller@plomlompom.de>
Sun, 16 Feb 2025 17:11:13 +0000 (18:11 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 16 Feb 2025 17:11:13 +0000 (18:11 +0100)
src/templates/_base.tmpl
src/templates/files.tmpl
src/templates/playlist.tmpl
src/ytplom/http.py
src/ytplom/misc.py

index 2ac0b997d9ebbd2ce60a8861cbd62a0f6881cb08..cad70fa5816b9e02e466816b349ea2e15c5e103b 100644 (file)
@@ -37,21 +37,34 @@ function connect_events() {
         else {
             console.log("Error does not seem connection-related, therefore aborting."); }}}
 
         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 {
     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) {
         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) {
 
 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 [
 
 event_handlers.push(function(data) {  // update player state
     for (const [id, text] of [
index e0fc5b6103f2a245d11598e921ea23b253c348ce..70c087786b0f3b434b8a948f7c586530effc3a73 100644 (file)
@@ -3,9 +3,10 @@
 
 {% block script %}
 {{ macros.js_new_child_to() }}
 
 {% block script %}
 {{ macros.js_new_child_to() }}
+const PATH_FILES_JSON = "/{{page_names.files_json}}";
 
 const all_tags = {{showable_tags|tojson|safe}};
 
 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) {
 
 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);
   }
   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);
   }
   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;
   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);
       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 %}
 {% endblock %}
 
 
 {% block body %}
-<form method="GET">
-<input type="checkbox" name="show_absent" {% if show_absent %}checked{% endif %}/> show absent<br />
-filename: <input name="filter_path" value="{{filter_path}}" /><br />
-tags: <span id="tags"></span><br />
-<select id="tags_select" onchange="select_tag()"></select><br />
-<input type="submit" value="filter" />
-</form>
-<p>known files (shown: {{files|length}}):</p>
+filename pattern: <input id="input_filter_path" oninput="update_files_list()"  /><br />
+show absent: <input id="input_show_absent" type="checkbox" onclick="update_files_list()" /><br />
+needed tags: <select id="tags_select" onchange="select_tag()"></select><br />
+<span id="tags"></span><br />
+<hr />
+<p>known files (shown: <span id="files_count">?</span>):</p>
 <form action="/{{page_names.files}}" method="POST">
 <input type="hidden" name="redir_target" value="{{redir_target}}" />
 <form action="/{{page_names.files}}" method="POST">
 <input type="hidden" name="redir_target" value="{{redir_target}}" />
-<table>
+<table id="files_table">
 <tr><th>size</th><th>actions</th><th>tags</th><th>path</th></tr>
 <tr><th>size</th><th>actions</th><th>tags</th><th>path</th></tr>
-{% for file in files %}
-<tr>
-<td>{{ file.size | round(1) }}</td>
-<td><input type="submit" name="play_{{file.digest.b64}}" value="play" {% if not file.present %}disabled {% endif %}/></td>
-<td>{{file.tags_showable.joined}}</td>
-<td><a href="/{{page_names.file}}/{{file.digest.b64}}">{{file.rel_path}}</a></td>
-</tr>
-{% endfor %}
 </table>
 </form>
 {% endblock %}
 </table>
 </form>
 {% endblock %}
index a27d4a295f8a77b82d761bb4545bb8a5e91616dc..669e723a8dcb5c5f72b90a82abaa239cca09f29d 100644 (file)
@@ -41,10 +41,11 @@ event_handlers.push(function(data) {  // update playlist
         a_file.href = `${PATH_PREFIX_FILE}${file.digest}`; }})
 
 function redo_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 %}
 
 
 {% endblock %}
 
index a17b00ed215353dec0d1e6285d824793988dff59..15e3a465918f8b88b4db4cbc0109929c60d06467 100644 (file)
@@ -41,6 +41,7 @@ PAGE_NAMES: dict[str, str] = {
     'events': 'events',
     'file': 'file',
     'files': 'files',
     'events': 'events',
     'file': 'file',
     'files': 'files',
+    'files_json': 'files.json',
     'missing': 'missing',
     'player': 'player',
     'playlist': 'playlist',
     '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()
                 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']:
             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:
                  '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,
         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)
         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:
 
     def _send_missing_json(self) -> None:
         with DbConn() as conn:
index 010a2fb193dee7e70e8836059c20a595e527cdf0..ac017ede9e1c6c8054f626c32bac74c1c69af602 100644 (file)
@@ -358,6 +358,17 @@ class VideoFile(DbData):
     def _renew_last_update(self):
         self.last_update = DatetimeStr(datetime.now().strftime(TIMESTAMP_FMT))
 
     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:
     def save(self, conn: DbConn) -> None:
         """Extend super().save by new .last_update if sufficient changes."""
         if hash(self) != self._hash_on_last_update: