<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>
{% 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 %}
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) {
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}`;
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() {
- 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 %}
-button.sorter { background-color: transparent; }
-button.sorting { background-color: white; color: black; }
-button.reverse { background-color: black; color: white; }
+{{ macros.css_sortable_table() }}
{% endblock %}
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>
-<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>
--- /dev/null
+{% 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 %}
+
_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')
'player': 'player',
'playlist': 'playlist',
'purge': 'purge',
+ 'tags': 'tags',
'thumbnails': 'thumbnails',
'yt_queries': 'yt_queries',
'yt_query': 'yt_query',
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']:
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:
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])
@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."""
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."""