home · contact · privacy
Add new /tags view, sortable by name and usage number. master
authorChristian Heller <c.heller@plomlompom.de>
Thu, 20 Feb 2025 22:33:38 +0000 (23:33 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Thu, 20 Feb 2025 22:33:38 +0000 (23:33 +0100)
src/templates/_base.tmpl
src/templates/_macros.tmpl
src/templates/files.tmpl
src/templates/tags.tmpl [new file with mode: 0644]
src/ytplom/http.py
src/ytplom/misc.py

index 007bce981f68c46607a86bd9b30b2c2abc879ccb..74a2cd01bc43a60dfa1b458b7887f667e199fa05 100644 (file)
@@ -117,6 +117,7 @@ td, th { vertical-align: top; text-align: left; margin: 0; padding: 0; }
 <div id="header">
 {{ macros.link_if("playlist" != selected, page_names.playlist) }}
 · {{ macros.link_if("files" != selected, page_names.files) }}
 <div id="header">
 {{ macros.link_if("playlist" != selected, page_names.playlist) }}
 · {{ macros.link_if("files" != selected, page_names.files) }}
+· {{ macros.link_if("tags" != selected, page_names.tags) }}
 · {{ macros.link_if("yt_queries" != selected, page_names.yt_queries, "queries") }}
 <hr />
 <button class="btn_if_can_play" onclick="player_command('prev')">prev</button>
 · {{ macros.link_if("yt_queries" != selected, page_names.yt_queries, "queries") }}
 <hr />
 <button class="btn_if_can_play" onclick="player_command('prev')">prev</button>
index 9c70226f3ab0be0f3b7d579f143716e064fdb5b6..9cf18c9f92b97c4aa338a51f9475b6f9fe2c3521 100644 (file)
@@ -3,3 +3,46 @@
 {% if display_name %}{{display_name}}{% else %}{{target}}{% endif %}
 {% if cond %}</a>{% endif %}
 {% endmacro %}
 {% if display_name %}{{display_name}}{% else %}{{target}}{% endif %}
 {% if cond %}</a>{% endif %}
 {% endmacro %}
+
+{% macro css_sortable_table() %}
+button.sorter { background-color: transparent; }
+button.sorting { background-color: white; color: black; }
+button.reverse { background-color: black; color: white; }
+{% endmacro %}
+
+{% macro js_manage_sortable_table(default_sort_key) %}
+var sort_key = "{{default_sort_key}}";
+
+function draw_sortable_table() {
+    items_for_sortable_table.sort((a, b) => {
+        let inverter = 1;
+        let _sort_key = sort_key;
+        if (sort_key[0] == '-') {
+            inverter = -1;
+            _sort_key = sort_key.substring(1);
+        }
+        return inverter * ((a[_sort_key] > b[_sort_key]) ? 1 : -1);
+    });
+    const table = document.getElementById("sortable_table");
+    Array.from(document.getElementsByClassName("sortable_row")).forEach((row) => row.remove());
+    items_for_sortable_table.forEach((item) => {
+        const tr = new_child_to('tr', table);
+        tr.classList.add("sortable_row");
+        populate_list_item_row(tr, item);
+    });
+}
+
+function sort_by(button, key) {
+    Array.from(document.getElementsByClassName("sorting")).forEach((btn_i) => {
+       if (btn_i != button) {
+           btn_i.classList.remove("sorting");
+           btn_i.classList.remove("reverse");
+       } else if (btn_i.classList.contains("sorting")) {
+           btn_i.classList.toggle("reverse");
+       }
+    });
+    button.classList.add("sorting");
+    sort_key = button.classList.contains("reverse") ? `-${key}` : key;
+    draw_sortable_table();
+}
+{% endmacro %}
index 81ae35dfbb6089a3246c8551435e738d9335306e..4c4fa0c531900cd7197b0775a7e101d275e3ffb5 100644 (file)
@@ -6,8 +6,6 @@ const PATH_FILES_JSON = "/{{page_names.files_json}}";
 
 const all_tags = {{showable_tags|tojson|safe}};
 var needed_tags = {{needed_tags|tojson|safe}};
 
 const all_tags = {{showable_tags|tojson|safe}};
 var needed_tags = {{needed_tags|tojson|safe}};
-var filtered_files = [];
-var sort_key = "rel_path";
 
 function select_tag() {
   if (tags_select.selectedIndex < 1) {
 
 function select_tag() {
   if (tags_select.selectedIndex < 1) {
@@ -46,38 +44,6 @@ function update_filter_inputs() {
   update_files_list();
 }
 
   update_files_list();
 }
 
-function draw_files_table() {
-    filtered_files.sort((a, b) => {
-        let inverter = 1;
-        let _sort_key = sort_key;
-        if (sort_key[0] == '-') {
-            inverter = -1;
-            _sort_key = sort_key.substring(1);
-        }
-        const cmp = "tags_count" == _sort_key
-                    ? (a.tags_showable.length > b.tags_showable.length)
-                    : (a[_sort_key] > b[_sort_key]);
-        return inverter * (cmp ? 1 : -1);
-    });
-    const table = document.getElementById("files_table");
-    Array.from(document.getElementsByClassName("file_row")).forEach((row) => row.remove());
-    filtered_files.forEach((file) => {
-        const tr = new_child_to('tr', table);
-        tr.classList.add("file_row");
-        new_child_to('td', tr, file.size);
-        new_child_to('td', tr, file.duration);
-        const td_inject = new_child_to('td', tr);
-        const btn_inject = new_child_to('button', td_inject);
-        btn_inject.onclick = function() { player_command(`inject_${file.digest}`) };
-        btn_inject.disabled = !file.present;
-        btn_inject.textContent = 'inject';
-        father_tag_links(new_child_to('td', tr), file.tags_showable);
-        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}`;
-    });
-}
-
 async function update_files_list() {
     const filter_path = encodeURIComponent(document.getElementById("input_filter_path").value);
     let target = `${PATH_FILES_JSON}?filter_path=${filter_path}`;
 async function update_files_list() {
     const filter_path = encodeURIComponent(document.getElementById("input_filter_path").value);
     let target = `${PATH_FILES_JSON}?filter_path=${filter_path}`;
@@ -85,37 +51,36 @@ async function update_files_list() {
     if (document.getElementById("input_show_absent").checked) { target = `${target}&show_absent=1`; }
     {% endif %}
     needed_tags.forEach((tag) => target = `${target}&needed_tag=${encodeURIComponent(tag)}`);
     if (document.getElementById("input_show_absent").checked) { target = `${target}&show_absent=1`; }
     {% endif %}
     needed_tags.forEach((tag) => target = `${target}&needed_tag=${encodeURIComponent(tag)}`);
-    filtered_files = await wrapped_fetch(target).then((response) => response.json());
-    document.getElementById("files_count").textContent = `${filtered_files.length}`;
-    draw_files_table();
+    items_for_sortable_table = await wrapped_fetch(target).then((response) => response.json());
+    document.getElementById("files_count").textContent = `${items_for_sortable_table.length}`;
+    draw_sortable_table();
 }
 
 function inject_all() {
 }
 
 function inject_all() {
-    filtered_files.forEach((file) => { player_command(`inject_${file.digest}`) });
+    items_for_sortable_table.forEach((file) => { player_command(`inject_${file.digest}`) });
 }
 
 }
 
-function sort_by(button, key) {
-    Array.from(document.getElementsByClassName("sorting")).forEach((btn_i) => {
-       if (btn_i != button) {
-           btn_i.classList.remove("sorting");
-           btn_i.classList.remove("reverse");
-       } else if (btn_i.classList.contains("sorting")) {
-           btn_i.classList.toggle("reverse");
-       }
-    });
-    button.classList.add("sorting");
-    sort_key = button.classList.contains("reverse") ? `-${key}` : key;
-    draw_files_table();
+{{ macros.js_manage_sortable_table("rel_path") }}
+var items_for_sortable_table = [];
+function populate_list_item_row(tr, file) {
+    new_child_to('td', tr, file.size);
+    new_child_to('td', tr, file.duration);
+    const td_inject = new_child_to('td', tr);
+    const btn_inject = new_child_to('button', td_inject);
+    btn_inject.onclick = function() { player_command(`inject_${file.digest}`) };
+    btn_inject.disabled = !file.present;
+    btn_inject.textContent = 'inject';
+    father_tag_links(new_child_to('td', tr), file.tags_showable);
+    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 css %}
 window.addEventListener('load', update_filter_inputs);
 {% endblock %}
 
 
 {% block css %}
-button.sorter { background-color: transparent; }
-button.sorting { background-color: white; color: black; }
-button.reverse { background-color: black; color: white; }
+{{ macros.css_sortable_table() }}
 {% endblock %}
 
 
 {% endblock %}
 
 
@@ -131,12 +96,12 @@ needed tags: <select id="tags_select" onchange="select_tag()"></select><br />
 known files (shown: <span id="files_count">?</span>):
 <button onclick="inject_all();">inject all</button>
 </p>
 known files (shown: <span id="files_count">?</span>):
 <button onclick="inject_all();">inject all</button>
 </p>
-<table id="files_table">
+<table id="sortable_table">
 <tr>
 <th><button class="sorter" onclick="sort_by(this, 'size'); ">size</button></th>
 <th><button class="sorter" onclick="sort_by(this, 'duration'); ">duration</button></th>
 <th>actions</th>
 <tr>
 <th><button class="sorter" onclick="sort_by(this, 'size'); ">size</button></th>
 <th><button class="sorter" onclick="sort_by(this, 'duration'); ">duration</button></th>
 <th>actions</th>
-<th>tags <button class="sorter" onclick="sort_by(this, 'tags_count'); ">count</button></th>
+<th>tags <button class="sorter" onclick="sort_by(this, 'n_tags'); ">count</button></th>
 <th><button class="sorter" onclick="sort_by(this, 'rel_path'); ">path</button></th>
 </tr>
 </table>
 <th><button class="sorter" onclick="sort_by(this, 'rel_path'); ">path</button></th>
 </tr>
 </table>
diff --git a/src/templates/tags.tmpl b/src/templates/tags.tmpl
new file mode 100644 (file)
index 0000000..a6f9f08
--- /dev/null
@@ -0,0 +1,30 @@
+{% extends '_base.tmpl' %}
+
+
+{% block script %}
+{{ macros.js_manage_sortable_table("name") }}
+var items_for_sortable_table = {{tags|tojson|safe}};
+function populate_list_item_row(tr, tag) {
+    a_name = new_child_to('a', new_child_to('td', tr), tag.name);
+    a_name.href = `${PATH_FILES}?needed_tag=${encodeURIComponent(tag.name)}`;
+    new_child_to('td', tr, tag.number);
+}
+window.addEventListener('load', draw_sortable_table);
+{% endblock %}
+
+
+{% block css %}
+{{ macros.css_sortable_table() }}
+#sortable_table { width: auto; }
+{% endblock %}
+
+
+{% block body %}
+<table id="sortable_table">
+<tr>
+<th><button class="sorter" onclick="sort_by(this, 'name'); ">name</button></th>
+<th><button class="sorter" onclick="sort_by(this, 'number'); ">usage number</button></th>
+</tr>
+</table>
+{% endblock %}
+
index 817220a764bf4439176a8318db806c06185f1f5e..280b88c7fdbf7da243e4824406e620601dec84c1 100644 (file)
@@ -31,6 +31,7 @@ _PATH_TEMPLATES = PATH_APP_DATA.joinpath('templates')
 _NAME_TEMPLATE_FILE_DATA = Path('file_data.tmpl')
 _NAME_TEMPLATE_FILES = Path('files.tmpl')
 _NAME_TEMPLATE_PLAYLIST = Path('playlist.tmpl')
 _NAME_TEMPLATE_FILE_DATA = Path('file_data.tmpl')
 _NAME_TEMPLATE_FILES = Path('files.tmpl')
 _NAME_TEMPLATE_PLAYLIST = Path('playlist.tmpl')
+_NAME_TEMPLATE_TAGS = Path('tags.tmpl')
 _NAME_TEMPLATE_YT_QUERIES = Path('yt_queries.tmpl')
 _NAME_TEMPLATE_YT_RESULT = Path('yt_result.tmpl')
 _NAME_TEMPLATE_YT_RESULTS = Path('yt_results.tmpl')
 _NAME_TEMPLATE_YT_QUERIES = Path('yt_queries.tmpl')
 _NAME_TEMPLATE_YT_RESULT = Path('yt_result.tmpl')
 _NAME_TEMPLATE_YT_RESULTS = Path('yt_results.tmpl')
@@ -48,6 +49,7 @@ PAGE_NAMES: dict[str, str] = {
     'player': 'player',
     'playlist': 'playlist',
     'purge': 'purge',
     'player': 'player',
     'playlist': 'playlist',
     'purge': 'purge',
+    'tags': 'tags',
     'thumbnails': 'thumbnails',
     'yt_queries': 'yt_queries',
     'yt_query': 'yt_query',
     'thumbnails': 'thumbnails',
     'yt_queries': 'yt_queries',
     'yt_query': 'yt_query',
@@ -205,6 +207,8 @@ class _TaskHandler(PlomHttpHandler):
                 self._send_files_json()
             elif self.pagename == PAGE_NAMES['missing']:
                 self._send_missing_json()
                 self._send_files_json()
             elif self.pagename == PAGE_NAMES['missing']:
                 self._send_missing_json()
+            elif self.pagename == PAGE_NAMES['tags']:
+                self._send_tags_index()
             elif self.pagename == PAGE_NAMES['thumbnails']:
                 self._send_thumbnail()
             elif self.pagename == PAGE_NAMES['yt_result']:
             elif self.pagename == PAGE_NAMES['thumbnails']:
                 self._send_thumbnail()
             elif self.pagename == PAGE_NAMES['yt_result']:
@@ -326,7 +330,9 @@ class _TaskHandler(PlomHttpHandler):
                     TagSet(set(self.params.all_for('needed_tag'))),
                     bool(self.params.first_for('show_absent')))
         files.sort(key=lambda t: t.rel_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_json([f.as_dict for f in files])
+        self._send_json([
+            f.as_dict | {'n_tags': len(f.tags_showable.as_str_list)}
+            for f in files])
 
     def _send_missing_json(self) -> None:
         with DbConn() as conn:
 
     def _send_missing_json(self) -> None:
         with DbConn() as conn:
@@ -334,6 +340,14 @@ class _TaskHandler(PlomHttpHandler):
                        if f.missing]
         self._send_json(missing)
 
                        if f.missing]
         self._send_json(missing)
 
+    def _send_tags_index(self) -> None:
+        with DbConn() as conn:
+            tags_to_numbers = [
+                    {'name': k, 'number': v} for k, v
+                    in VideoFile.showable_tags_to_numbers(conn).items()]
+        self._send_rendered_template(_NAME_TEMPLATE_TAGS,
+                                     {'tags': tags_to_numbers})
+
     def _send_thumbnail(self) -> None:
         filename = Path(self.path_toks[2])
         ensure_expected_dirs([PATH_THUMBNAILS])
     def _send_thumbnail(self) -> None:
         filename = Path(self.path_toks[2])
         ensure_expected_dirs([PATH_THUMBNAILS])
index 404884506e506c89deb4058dd91849dc17c947da..2fd993e5b8f6251cc143a1a87e1b94c1a78939e2 100644 (file)
@@ -426,8 +426,8 @@ class VideoFile(DbData):
     @classmethod
     def get_filtered(cls,
                      conn: DbConn,
     @classmethod
     def get_filtered(cls,
                      conn: DbConn,
-                     filter_path: FilterStr,
-                     needed_tags_seen: TagSet,
+                     filter_path: FilterStr = FilterStr(''),
+                     needed_tags_seen: TagSet = TagSet(set()),
                      show_absent: bool = False
                      ) -> list[Self]:
         """Return cls.get_all matching provided filter criteria."""
                      show_absent: bool = False
                      ) -> list[Self]:
         """Return cls.get_all matching provided filter criteria."""
@@ -440,6 +440,15 @@ class VideoFile(DbData):
             and needed_tags_seen.whitelisted(cls.tags_display_whitelist
                                              ).are_all_in(f.tags)]
 
             and needed_tags_seen.whitelisted(cls.tags_display_whitelist
                                              ).are_all_in(f.tags)]
 
+    @classmethod
+    def showable_tags_to_numbers(cls, conn) -> dict[str, int]:
+        """Return showable tags with how often used in showable files."""
+        tags_to_numbers: dict[str, int] = {}
+        for file in cls.get_filtered(conn):
+            for tag in file.tags.whitelisted(cls.tags_display_whitelist):
+                tags_to_numbers[tag] = tags_to_numbers.get(tag, 0) + 1
+        return tags_to_numbers
+
     @classmethod
     def all_tags_showable(cls, conn) -> TagSet:
         """Show all used tags passing .tags_display_whitelist."""
     @classmethod
     def all_tags_showable(cls, conn) -> TagSet:
         """Show all used tags passing .tags_display_whitelist."""