--- /dev/null
+/*
+global
+EventSource
+clearInterval
+console
+document
+fetch
+setInterval
+setTimeout
+window
+*/
+/*
+eslint
+"array-element-newline": [
+ "error",
+ "consistent",
+ "multiline"
+],
+"capitalized-comments": [
+ "error",
+ "never"
+],
+"function-paren-newline": "off",
+"max-lines": [
+ "error",
+ {"max": 450, "skipBlankLines": true, "skipComments": true}
+],
+"max-lines-per-function": [
+ "error",
+ 84
+],
+"max-params": [
+ "error",
+ 5
+],
+"max-statements": [
+ "error",
+ 22
+],
+"multiline-ternary": [
+ "error",
+ "always-multiline"
+],
+"newline-after-var": "off",
+"newline-before-return": "off",
+"no-console": [
+ "error",
+ { "allow": ["dir", "log", "warn", "error"] }
+],
+"no-extra-parens": "off",
+"no-nested-ternary": "off",
+"no-ternary": "off",
+"padded-blocks": [
+ "error",
+ "never"
+],
+"one-var": [
+ "error",
+ "consecutive"
+],
+*/
+
+const
+ CLS_REVERSE = "reverse",
+ CLS_SORTABLE_ROW = "sortable_row",
+ CLS_SORTING = "sorting",
+ DEC_BASE = 10,
+ DUR_MS_IN_S = 1000,
+ DUR_S_IN_M = 60,
+ DUR_S_MIN = 0,
+ DUR_S_TIMESTAMP_INC = 1,
+ DUR_S_TO_RETRY = 5,
+ FACTOR_IDENTICAL = 1,
+ FACTOR_INVERT = -1,
+ ID_PLAYER_CTLS = "player_controls",
+ ID_SORTABLE_TABLE = "sortable_table",
+ IS_GT = 1,
+ IS_LTE = -1,
+ PATH_EVENTS = "/events",
+ PATH_PLAYER = "/player",
+ STATUS_CODE_OK = 200;
+export const
+ SYMBOL_DOWN = "v",
+ SYMBOL_UP = "^";
+export const
+ BUTTONS_UP_DOWN = [
+ [SYMBOL_UP, "up"],
+ [SYMBOL_DOWN, "down"]
+ ],
+ CMD_ADD_NEXT = "inject",
+ CMD_ADD_PLAY = "injectplay",
+ CMD_RM = "rm",
+ IDX_PATH_ID = 2,
+ IDX_START = 0,
+ LABEL_ADD_NEXT = "add as next",
+ LABEL_ADD_PLAY = "add and play",
+ LEN_EMPTY = 0,
+ PARAM_TAG_NEEDED = "needed_tag",
+ PATH_DOWNLOADS_JSON = "/downloads.json",
+ PATH_FILES = "/files",
+ PATH_FILES_JSON = "/files.json",
+ PATH_PREFIX_DOWNLOAD = "/download/",
+ PATH_PREFIX_FILE = "/file/",
+ PATH_PREFIX_FILE_JSON = "/file.json/",
+ PATH_PREFIX_YT_RESULT = "/yt_result/",
+ PREFIX_CLICKABLE = "clickable",
+ PREFIX_MINUS = "-",
+ SYMBOL_RM = "x",
+ eventHandlers = {
+ "ping" (
+ ) {
+ // dummy to keep connection alive
+ }
+ };
+let
+ eventsParams = "",
+ eventsStream = null,
+ playerDiv = null,
+ timestampInterval = null;
+
+const connectEvents = (
+) => {
+ eventsStream = new EventSource(`${PATH_EVENTS}?${eventsParams}`);
+ eventsStream.onmessage = (
+ event
+ ) => {
+ const payload = JSON.parse(event.data);
+ Object.keys(payload).forEach(
+ (key) => eventHandlers[key](payload[key]));
+ };
+ eventsStream.onerror = (
+ error
+ ) => {
+ const whileConnecting = eventsStream.readyState ===
+ eventsStream.CONNECTING;
+ console.log(
+ `Error on ${PATH_EVENTS} connection:`,
+ error
+ );
+ eventsStream.close();
+ if (whileConnecting) {
+ console.log("Error seemed connection-related, trying reconnect.");
+ setTimeout(
+ connectEvents,
+ DUR_S_TO_RETRY * DUR_MS_IN_S
+ );
+ } else {
+ console.log(
+ "Error does not seem connection-related, therefore aborting."
+ );
+ }
+ };
+};
+
+export const subscribeEvents = (
+ params = ""
+) => {
+ eventsParams = params;
+ window.addEventListener(
+ "load",
+ connectEvents
+ );
+};
+
+export const addChildTo = (
+ tag,
+ parent,
+ attrs = {}
+) => {
+ const el = document.createElement(tag);
+ parent.appendChild(el);
+ for (const [key, value] of Object.entries(attrs)) {
+ el[key] = value;
+ }
+ return el;
+};
+
+export const addATo = (
+ parent,
+ textContent,
+ href
+) => addChildTo(
+ "a",
+ parent,
+ {
+ href,
+ textContent
+ }
+);
+
+export const wrappedFetch = async (
+ target,
+ verbose,
+ fetchKwargs
+) => {
+ if (verbose) {
+ console.log(
+ `Fetching ${target}, kwargs:`,
+ fetchKwargs
+ );
+ }
+ try {
+ const response = await fetch(
+ target,
+ fetchKwargs
+ );
+ if (response.status !== STATUS_CODE_OK) {
+ const err = Error(
+ `Unexpected response status: ${response.status}`);
+ err.response = response;
+ throw err;
+ }
+ return response;
+ } catch (error) {
+ console.log(`Error on sending to ${target}: ${error.message}`);
+ console.dir(error);
+ throw error;
+ }
+};
+
+export const wrappedPost = async (
+ target,
+ body,
+ verbose = false
+) => {
+ const response = await wrappedFetch(
+ target,
+ verbose,
+ {
+ "body": JSON.stringify(body),
+ "headers": {"Content-Type": "application/json"},
+ "method": "POST"
+ }
+ );
+ return response;
+};
+
+export const wrappedCommand = (
+ target,
+ command
+) => wrappedPost(
+ target,
+ {"command": [command]}
+);
+
+export const playerCommand = (command) => {
+ wrappedCommand(
+ PATH_PLAYER,
+ command
+ );
+};
+
+export const addButtonTo = (
+ parent,
+ label,
+ disabled,
+ onclick
+) => addChildTo(
+ "button",
+ parent,
+ {
+ disabled,
+ onclick,
+ "textContent": label
+ }
+);
+
+export const addPlayerBtnTo = (
+ parent,
+ label,
+ command,
+ disabled = false
+) => addButtonTo(
+ parent,
+ label,
+ disabled,
+ () => playerCommand(command)
+);
+
+export const addTextTo = (
+ parent,
+ text
+) => {
+ const node = document.createTextNode(text);
+ parent.appendChild(node);
+ return node;
+};
+
+export const addTdTo = (
+ parent,
+ attrs = {}
+) => addChildTo(
+ "td",
+ parent,
+ attrs
+);
+
+export const addATdTo = (
+ parent,
+ text,
+ href
+) => addATo(
+ addTdTo(parent),
+ text,
+ href
+);
+
+export const addTrTo = (
+ parent
+) => addChildTo(
+ "tr",
+ parent
+);
+
+export const addTagLinksTo = (
+ parent,
+ tags
+) => {
+ tags.forEach(
+ (
+ tag
+ ) => {
+ addATo(
+ parent,
+ tag,
+ `${PATH_FILES}?${PARAM_TAG_NEEDED}=${encodeURIComponent(tag)}`
+ );
+ addTextTo(
+ parent,
+ " "
+ );
+ }
+ );
+};
+
+eventHandlers.player = (
+ data
+) => {
+ const
+ addTextToDiv = (
+ text
+ ) => addTextTo(
+ playerDiv,
+ text
+ );
+ const // eslint-disable-line one-var -- to provide addTextToDiv above
+ addPlayerBtnToDiv = (
+ label,
+ command
+ ) => addPlayerBtnTo(
+ playerDiv,
+ label,
+ command,
+ !data.can_play
+ ),
+ addSeparator = (
+ ) => addTextToDiv(
+ " · "
+ );
+ playerDiv.innerHTML = "";
+ addPlayerBtnToDiv(
+ "prev",
+ "prev"
+ );
+ addPlayerBtnToDiv(
+ "next",
+ "next"
+ );
+ addPlayerBtnToDiv(
+ data.is_playing ? "pause" : "play",
+ "play"
+ );
+ addSeparator();
+ addTextToDiv(
+ data.is_running ? (data.is_playing ? "playing" : "paused") : "stopped"
+ );
+ if (data.title_digest) {
+ const formatSeconds = (totalSeconds) => {
+ if (totalSeconds < DUR_S_MIN) {
+ return "?";
+ }
+ const
+ minutes = Math.floor(totalSeconds / DUR_S_IN_M),
+ seconds = totalSeconds % DUR_S_IN_M;
+ return `${minutes}:${seconds < DEC_BASE ? "0" : ""}${seconds}`;
+ };
+ addTextToDiv(" (");
+ const timestampSpan = addChildTo(
+ "span",
+ playerDiv
+ );
+ clearInterval(timestampInterval);
+ let {timestamp} = data;
+ timestampSpan.textContent = formatSeconds(timestamp);
+ if (data.is_playing) {
+ timestampInterval = setInterval(
+ () => {
+ timestamp += DUR_S_TIMESTAMP_INC;
+ timestampSpan.textContent = formatSeconds(timestamp);
+ },
+ DUR_MS_IN_S / data.speed
+ );
+ }
+ addTextToDiv(`/${formatSeconds(data.duration)}): `);
+ addATo(
+ playerDiv,
+ data.title,
+ `${PATH_PREFIX_FILE}${data.title_digest}`
+ );
+ addSeparator();
+ addTagLinksTo(
+ playerDiv,
+ data.title_tags
+ );
+ }
+};
+
+export const sortableTableSetup = {
+ "itemsForSortableTable": null,
+ "populateListItemRow": null,
+ "sortCols": null,
+ "sortKey": null
+};
+
+export const drawTable = (
+ idTable,
+ clsTableRow,
+ items,
+ populateRow,
+ emptyFirst = true
+) => {
+ const table = document.getElementById(idTable);
+ if (emptyFirst) {
+ Array.from(
+ document.getElementsByClassName(clsTableRow)
+ ).forEach((row) => row.remove());
+ }
+ items.forEach((item, idx, arr) => {
+ const tr = addTrTo(table);
+ tr.classList.add(clsTableRow);
+ populateRow(
+ tr,
+ item,
+ idx,
+ arr
+ );
+ });
+};
+
+export const drawSortableTable = (
+) => {
+ sortableTableSetup.itemsForSortableTable.sort(
+ (
+ cmpA,
+ cmpB
+ ) => {
+ let
+ inverter = FACTOR_IDENTICAL,
+ {sortKey} = sortableTableSetup;
+ if (sortKey.startsWith(PREFIX_MINUS)) {
+ inverter = FACTOR_INVERT;
+ sortKey = sortKey.substring(PREFIX_MINUS.length);
+ }
+ return inverter * (cmpA[sortKey] > cmpB[sortKey] ? IS_GT : IS_LTE);
+ }
+ );
+ drawTable(
+ ID_SORTABLE_TABLE,
+ CLS_SORTABLE_ROW,
+ sortableTableSetup.itemsForSortableTable,
+ sortableTableSetup.populateListItemRow
+ );
+};
+
+export const assignOnclicks = (items, onclick) => {
+ items.forEach((item) => {
+ const el = document.getElementById(`${PREFIX_CLICKABLE}:${item}`);
+ el.onclick = (
+ ) => onclick(
+ el,
+ item
+ );
+ });
+};
+
+window.addEventListener(
+ "beforeunload",
+ () => {
+ if (eventsStream) {
+ eventsStream.close();
+ }
+ }
+);
+
+window.addEventListener(
+ "load",
+ () => {
+ playerDiv = document.getElementById(ID_PLAYER_CTLS);
+
+ if (sortableTableSetup.populateListItemRow) {
+ drawSortableTable();
+ assignOnclicks(
+ sortableTableSetup.sortCols,
+ (
+ button,
+ colName
+ ) => {
+ Array.from(
+ document.getElementsByClassName(CLS_SORTING)
+ ).forEach(
+ (
+ btnIdx
+ ) => {
+ if (btnIdx !== button) {
+ btnIdx.classList.remove(CLS_SORTING);
+ btnIdx.classList.remove(CLS_REVERSE);
+ } else if (btnIdx.classList.contains(CLS_SORTING)) {
+ btnIdx.classList.toggle(CLS_REVERSE);
+ }
+ }
+ );
+ button.classList.add(CLS_SORTING);
+ const prefix = button.classList.contains(CLS_REVERSE)
+ ? ""
+ : PREFIX_MINUS;
+ sortableTableSetup.sortKey = `${prefix}${colName}`;
+ drawSortableTable();
+ }
+ );
+ }
+ }
+);
+
-{% import '_macros.tmpl' as macros %}
+{% macro section_link(pagename, target, display_name = false) %}
+{% if target != pagename %}<a href="/{{target}}">{% endif %}
+{% if display_name %}{{display_name}}{% else %}{{target}}{% endif %}
+{% if target != pagename %}</a>{% endif %}
+{% endmacro %}
+
<!DOCTYPE html>
<html>
+
<head>
<meta charset="UTF-8">
-<script>
-const MS_IN_S = 1000;
-const RETRY_INTERVAL_S = 5;
-const PATH_EVENTS = "/{{page_names.events}}";
-const PATH_PLAYER = "/{{page_names.player}}";
-const PATH_PREFIX_FILE = "/{{page_names.file}}/";
-const PATH_FILES = "/{{page_names.files}}";
-
-function add_child_to(tag, parent, attrs={}) {
- const el = document.createElement(tag);
- parent.appendChild(el);
- for (const [key, value] of Object.entries(attrs)) {
- el[key] = value;
- }
- return el;
-}
-
-function add_a_to(parent, text, href) {
- add_child_to('a', parent, {textContent: text, href: href});
-}
-
-function add_player_btn_to(parent, label, command, disabled=false) {
- add_child_to('button', parent, {
- textContent: label,
- onclick: function() { player_command(command) },
- disabled: disabled
- });
-}
-
-function add_text_to(parent, text) {
- const node = document.createTextNode(text);
- parent.appendChild(node);
- return node;
-}
-
-var events_params = "";
-var events_stream = null;
-var event_handlers = {'ping': function(data) {}};
-
-window.addEventListener("beforeunload", function() {
- if (events_stream) {
- events_stream.close();
- }
-});
-
-function connect_events() {
- events_stream = new EventSource(`${PATH_EVENTS}?${events_params}`);
- events_stream.onmessage = function(event) {
- const payload = JSON.parse(event.data);
- for (const [key, data] of Object.entries(payload)) { event_handlers[key](data) }
- }
- events_stream.onerror = function(error) {
- const while_connecting = events_stream.readyState == events_stream.CONNECTING;
- console.log(`Error on ${PATH_EVENTS} connection:`, error);
- events_stream.close();
- if (while_connecting) {
- console.log("Error seemed connection-related, trying reconnect.");
- setTimeout(connect_events, RETRY_INTERVAL_S * MS_IN_S);
- } else {
- console.log("Error does not seem connection-related, therefore aborting.");
- }
- }
-}
-
-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_kwargs ? fetch(target, fetch_kwargs) : fetch(target));
- if (200 != response.status) {
- console.log(`Got unexpected fetch response from ${target}:`, response);
- } else {
- return response;
- }
- } catch(error) {
- console.log(`Error on sending to ${target}:`, error);
- }
+<script type="module" src="/{{pagename}}.js"></script>
+<style>
+body {
+ background-color: {{background_color}};
}
-
-async function wrapped_post(target, body, verbose=false) {
- const fetch_kwargs = {
- method: "POST",
- headers: {"Content-Type": "application/json"},
- body: JSON.stringify(body)
- };
- const response = await wrapped_fetch(target, fetch_kwargs, verbose);
- return response;
+table {
+ width: 100%;
}
-
-function player_command(command) {
- wrapped_post(PATH_PLAYER, {command: [command]})
+td, th {
+ vertical-align: top;
+ text-align: left;
+ margin: 0;
+ padding: 0;
}
-
-function add_tag_links_to(parent_element, tags) {
- tags.forEach((tag) => {
- add_a_to(parent_element, tag, `${PATH_FILES}?needed_tag=${encodeURIComponent(tag)}`);
- add_text_to(parent_element, ' ');
- });
+#header {
+ position: sticky;
+ top: 0;
}
-
-var timestamp_interval = null;
-
-event_handlers.player = function(data) {
- const div = document.getElementById("player_controls");
- div.innerHTML = "";
- add_player_btn_to(div, "prev", "prev", !data.can_play);
- add_player_btn_to(div, "next", "next", !data.can_play);
- add_player_btn_to(div, data.is_playing ? "pause" : "play", "play", !data.can_play);
- add_text_to(div, " · ");
- add_text_to(div, data.is_running ? (data.is_playing ? "playing" : "paused")
- : "stopped");
- if (data.title_digest) {
- function format_seconds(total_seconds) {
- if (total_seconds < 0) {
- return "?";
- }
- const seconds = total_seconds % 60;
- const minutes = Math.floor(total_seconds / 60);
- return `${minutes}:` + (seconds < 10 ? '0' : '') + `${seconds}`
- }
- add_text_to(div, " (");
- const timestamp_span = add_child_to("span", div);
- clearInterval(timestamp_interval);
- let timestamp = data.timestamp;
- timestamp_span.textContent = format_seconds(timestamp);
- if (data.is_playing) {
- timestamp_interval = setInterval(function() {
- timestamp += 1;
- timestamp_span.textContent = format_seconds(timestamp);
- }, MS_IN_S / data.speed);
- }
- add_text_to(div, "/" + format_seconds(data.duration) + "): ");
- add_a_to(div, data.title, `${PATH_PREFIX_FILE}${data.title_digest}`);
- add_text_to(div, " · ");
- add_tag_links_to(div, data.title_tags);
-
- }
-};
-
-{% block script %}
-{% endblock %}
-
-connect_events();
-</script>
-<style>
-body { background-color: {{background_color}}; }
-table { width: 100%; }
-td, th { vertical-align: top; text-align: left; margin: 0; padding: 0; }
-#header { position: sticky; top: 0; background-color: {{background_color}}; }
{% block css %}
{% endblock %}
</style>
</head>
+
<body>
<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") }}
-· {{ macros.link_if("downloads" != selected, page_names.downloads) }}
+{{ section_link(pagename, "playlist") }} ·
+{{ section_link(pagename, "files") }} ·
+{{ section_link(pagename, "tags") }} ·
+{{ section_link(pagename, "yt_queries", "queries") }} ·
+{{ section_link(pagename, "downloads") }}
<hr />
+
<div id="player_controls"></div>
<hr />
+
{% block body %}
{% endblock %}
+
</body>
</html>
-{% macro link_if(cond, target, display_name = false ) %}
-{% if cond %}<a href="/{{target}}">{% endif %}
-{% 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 = add_child_to('tr', table);
- tr.classList.add("sortable_row");
- populate_list_item_row(tr, item);
- });
+button.sorter {
+ background-color: transparent;
+}
+button.sorting {
+ background-color: white;
+ color: black;
}
-
-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();
+button.reverse {
+ background-color: black;
+ color: white;
}
{% endmacro %}
-
--- /dev/null
+{% extends '_base.tmpl' %}
+
+
+{% block body %}
+<table id="download_rows">
+</table>
+{% endblock %}
--- /dev/null
+/*
+eslint
+"capitalized-comments": [
+ "error",
+ "never"
+],
+"line-comment-position": [
+ "error",
+ { "position": "beside" }
+],
+"max-lines-per-function": [
+ "error",
+ 112
+],
+"max-params": [
+ "error",
+ 4
+],
+"multiline-ternary": [
+ "error",
+ "always-multiline"
+],
+"newline-after-var": "off",
+"no-extra-parens": "off",
+"no-inline-comments": "off",
+"no-multi-spaces": [
+ "error",
+ { "ignoreEOLComments": true }
+],
+"no-ternary": "off",
+"padded-blocks": [
+ "error",
+ "never"
+],
+*/
+
+import {
+ BUTTONS_UP_DOWN,
+ CMD_RM,
+ IDX_START,
+ PATH_DOWNLOADS_JSON,
+ PATH_PREFIX_FILE,
+ PATH_PREFIX_YT_RESULT,
+ SYMBOL_DOWN,
+ SYMBOL_RM,
+ SYMBOL_UP,
+ addATdTo,
+ addButtonTo,
+ addTdTo,
+ drawTable,
+ eventHandlers,
+ subscribeEvents,
+ wrappedCommand
+} from "./_base.js";
+
+const
+ CLS_ROW = "download_row",
+ IDX_INC = 1,
+ ID_TABLE = "download_rows",
+ PARAM_DOWNLOADS = "downloads=1";
+
+eventHandlers.downloads = (data) => {
+ const
+ addCommandButtonTo = (
+ parent,
+ label,
+ disabled,
+ command
+ ) => addButtonTo(
+ parent,
+ label,
+ disabled,
+ () => wrappedCommand(
+ PATH_DOWNLOADS_JSON,
+ command
+ )
+ ),
+ drawDownloadTable = (
+ items,
+ populateRow,
+ emptyFirst = true
+ ) => drawTable(
+ ID_TABLE,
+ CLS_ROW,
+ items,
+ populateRow,
+ emptyFirst
+ );
+ drawDownloadTable(
+ data.downloaded,
+ (
+ tr,
+ downloaded
+ ) => {
+ addATdTo( // col 1
+ tr,
+ downloaded.path,
+ `${PATH_PREFIX_FILE}${downloaded.digest}`
+ );
+ addATdTo( // col 2
+ tr,
+ downloaded.title,
+ `${PATH_PREFIX_YT_RESULT}${downloaded.yt_id}`
+ );
+ }
+ );
+ drawDownloadTable(
+ data.downloading ? [data.downloading] : [],
+ (
+ tr,
+ downloading
+ ) => {
+ const tdEntryControl = addTdTo(tr); // col 1
+ addCommandButtonTo(
+ tdEntryControl,
+ SYMBOL_RM,
+ false,
+ CMD_RM
+ );
+ addTdTo( // col 2
+ tr,
+ {"textContent": downloading.status}
+ );
+ addATdTo( // col 3
+ tr,
+ downloading.title,
+ `${PATH_PREFIX_YT_RESULT}${data.downloading.yt_id}`
+ );
+ },
+ false
+ );
+ drawDownloadTable(
+ data.to_download,
+ (
+ tr,
+ toDownload,
+ idx,
+ arr
+ ) => {
+ addCommandButtonTo( // col 1
+ tr,
+ SYMBOL_RM,
+ false,
+ `${CMD_RM}_${idx}`
+ );
+ const tdEntryControl = addTdTo(tr); // col 2
+ for (
+ const [
+ symbol,
+ prefix
+ ] of BUTTONS_UP_DOWN
+ ) {
+ const disabled =
+ (idx === IDX_START &&
+ symbol === SYMBOL_UP) ||
+ ((idx + IDX_INC) === arr.length &&
+ symbol === SYMBOL_DOWN);
+ addCommandButtonTo(
+ tdEntryControl,
+ symbol,
+ disabled,
+ `${prefix}_${idx}`
+ );
+ }
+ addATdTo( // col 3
+ tr,
+ toDownload.title,
+ `${PATH_PREFIX_YT_RESULT}${toDownload.yt_id}`
+ );
+ },
+ false
+ );
+};
+
+subscribeEvents(PARAM_DOWNLOADS);
+
+++ /dev/null
-{% extends '_base.tmpl' %}
-
-
-{% block script %}
-events_params += 'downloads=1';
-event_handlers.downloads = function(data) {
- let table = document.getElementById('downloaded_rows');
- table.innerHTML = '';
- for (let i = 0; i < data.downloaded.length; i++) {
- const download_ = data.downloaded[i];
- const tr = add_child_to(
- tag='tr',
- parent=table);
- add_a_to(
- parent=add_child_to(
- tag='td',
- parent=tr),
- text=download_.path,
- href="/{{page_names.file}}/" + download_.digest);
- add_a_to(
- parent=add_child_to(
- tag='td',
- parent=tr),
- text=download_.title,
- href="/{{page_names.yt_result}}/" + download_.yt_id);
- }
-
- add_child_to(
- tag='td',
- parent=add_child_to(
- tag='tr',
- parent=table),
- attrs={
- textContent: 'DOWNLOADING'});
-
- if (data.downloading) {
- const tr = add_child_to(
- tag='tr',
- parent=table);
- // col 1
- const td_entry_control = add_child_to(
- tag='td',
- parent=tr);
- add_child_to(
- tag='button',
- parent=td_entry_control,
- attrs={
- textContent: 'x',
- onclick: function() {
- wrapped_post(
- target="{{page_names.downloads_json}}",
- body={
- command: ['abort']}); }
- })
- // col 2
- add_child_to(
- tag='td',
- parent=tr,
- attrs={
- textContent: data.downloading.status});
- // col 3
- add_a_to(
- parent=add_child_to(
- tag='td',
- parent=tr),
- text=data.downloading.title,
- href="/{{page_names.yt_result}}/" + data.downloading.yt_id);
- }
-
- for (let i = 0; i < data.to_download.length; i++) {
- const download_ = data.to_download[i];
- if (download_.status == 'downloaded') {
- continue;
- }
- const tr = add_child_to(
- tag='tr',
- parent=table);
- // col 1
- add_child_to(
- tag='button',
- parent=tr,
- attrs={
- textContent: 'x',
- onclick: function() {
- wrapped_post(
- target="{{page_names.downloads_json}}",
- body={
- command: [`unqueue_${i}`]}); }
- })
- // col 2
- const td_entry_control = add_child_to(
- tag='td',
- parent=tr);
- for (const [symbol, prefix] of [['^', 'up'],
- ['v', 'down']]) {
- add_child_to(
- tag='button',
- parent=td_entry_control,
- attrs={
- textContent: symbol,
- onclick: function() {
- wrapped_post(
- target="{{page_names.downloads_json}}",
- body={
- command: [`${prefix}_${i}`]}); }
- })
- }
- // col 3
- add_a_to(
- parent=add_child_to(
- tag='td',
- parent=tr),
- text=download_.title,
- href="/{{page_names.yt_result}}/" + download_.yt_id);
- }
-}
-{% endblock %}
-
-
-{% block body %}
-<table id="downloaded_rows">
-</table>
-<table id="to_download_rows">
-</table>
-{% endblock %}
--- /dev/null
+{% extends '_base.tmpl' %}
+
+
+{% block css %}
+td.top_field {
+ width: 100%;
+}
+td.tag_checkboxes {
+ width: 1em;
+}
+td.dangerous {
+ text-align: right
+}
+td.dangerous > form > input[type=submit], td.dangerous > button {
+ background-color: black;
+ color: red;
+}
+{% endblock %}
+
+
+{% block body %}
+</div>
+
+<table id="file_table">
+
+<tr>
+<th>path:</th>
+<td class="top_field">{{file.rel_path}}</td>
+</tr>
+
+<tr>
+<th>present:</th>
+<td id="presence">
+</td>
+</tr>
+
+<tr>
+<th>YouTube ID:</th>
+<td><a href="/yt_result/{{file.yt_id}}">{{file.yt_id}}</a>
+</tr>
+
+<tr>
+<th>duration</th>
+<td>{{file.duration()}}</td>
+</tr>
+
+<tr>
+<th>tags</th>
+<td>
+<table id="tags_table">
+{% if allow_edit %}
+<tr>
+<td class="tag_checkboxes"><button id="clickable:add_tag_button">add:</button></td>
+<td>
+<input id="added_tag" list="unused_tags" autocomplete="off" />
+<datalist id="unused_tags" />
+</datalist>
+</td>
+</tr>
+{% endif %}
+</table>
+</td>
+</tr>
+
+{% if allow_edit %}
+<tr>
+<th>options</th>
+<td>
+<table>
+<tr>
+<td>
+<input id="clickable:sync_checkbox" type="checkbox"/> do sync<br />
+</td>
+<td class="dangerous">
+<button id="clickable:unlink_button"/>delete locally</button>
+<form action="/file_kill/{{file.digest.b64}}" method="POST" />
+<input type="submit" value="KILL" />
+</form>
+</td>
+</tr>
+</table>
+</td>
+</tr>
+{% endif %}
+
+</table>
+{% endblock %}
--- /dev/null
+/* global
+document,
+window
+*/
+/*
+eslint
+"function-paren-newline": "off",
+"no-extra-parens": "off",
+"max-lines-per-function": [
+ "error",
+ 73
+],
+"max-statements": [
+ "error",
+ 16
+],
+"newline-after-var": "off",
+"padded-blocks": [
+ "error",
+ "never"
+],
+*/
+import {
+ CMD_ADD_NEXT,
+ CMD_ADD_PLAY,
+ IDX_PATH_ID,
+ LABEL_ADD_NEXT,
+ LABEL_ADD_PLAY,
+ LEN_EMPTY,
+ PATH_FILES,
+ PATH_PREFIX_FILE,
+ PATH_PREFIX_FILE_JSON,
+ PREFIX_CLICKABLE,
+ addATo,
+ addChildTo,
+ addPlayerBtnTo,
+ addTdTo,
+ addTextTo,
+ assignOnclicks,
+ drawTable,
+ subscribeEvents,
+ wrappedFetch,
+ wrappedPost
+} from "./_base.js";
+
+const
+ CLS_TAG_CHECKBOX = "tag_checkboxes",
+ CLS_TAG_LISTED = "listed_tags",
+ EDIT_BUTTONS = [
+ "add_tag_button",
+ "sync_checkbox",
+ "unlink_button"
+ ],
+ FLAG_NO_SYNC = "do not sync",
+ IDX_TAG_CHECKBOX_TD = 0,
+ IDX_TAG_CHECKBOX_TD_INPUT = 0,
+ IDX_TAG_LINK_TD = 1,
+ IDX_TAG_LINK_TD_A = 0,
+ ID_BTN_ADD_TAG = `${PREFIX_CLICKABLE}:add_tag_button`,
+ ID_BTN_SYNC = `${PREFIX_CLICKABLE}:sync_checkbox`,
+ ID_BTN_UNLINK = `${PREFIX_CLICKABLE}:unlink_button`,
+ ID_PRESENCE = "presence",
+ ID_TAGS_TABLE = "tags_table",
+ ID_TAGS_UNUSED = "unused_tags",
+ ID_TAG_ADDED = "added_tag",
+ LABEL_ABSENT = "no",
+ LABEL_PRESENT = "yes",
+ PARAM_TAG_NEEDED = "needed_tag",
+ PATH_FILE_JSON = (
+ PATH_PREFIX_FILE_JSON +
+ window.location.pathname.split("/")[IDX_PATH_ID]
+ );
+
+let
+ allowEdit = null;
+
+const updateFileData = async () => { // eslint-disable-line one-var
+ const
+ fetched = await wrappedFetch(PATH_FILE_JSON),
+ fileData = await fetched.json(),
+ tdPresent = document.getElementById(ID_PRESENCE);
+ drawTable(
+ ID_TAGS_TABLE,
+ CLS_TAG_LISTED,
+ fileData.tags_showable,
+ (
+ tr,
+ tag
+ ) => {
+ if (allowEdit) {
+ const tdCheckbox = addTdTo(tr);
+ tdCheckbox.classList.add(CLS_TAG_CHECKBOX);
+ addChildTo(
+ "input",
+ tdCheckbox,
+ {
+ "checked": true,
+ /* eslint-disable-next-line no-use-before-define */
+ "onclick": sendUpdate,
+ "type": "checkbox"
+ }
+ );
+ }
+ addATo(
+ addTdTo(tr),
+ tag,
+ `${PATH_FILES}?${PARAM_TAG_NEEDED}=${encodeURIComponent(tag)}`
+ );
+ }
+ );
+ if (allowEdit) {
+ const dataList = document.getElementById(ID_TAGS_UNUSED);
+ dataList.innerHTML = "";
+ document.getElementById(ID_BTN_SYNC).checked =
+ !fileData.flags.includes(FLAG_NO_SYNC);
+ document.getElementById(ID_TAG_ADDED).value = "";
+ fileData.unused_tags.forEach(
+ (tag) => addChildTo(
+ "option",
+ dataList,
+ {"textContent": tag}
+ )
+ );
+ }
+ tdPresent.innerHTML = "";
+ if (fileData.present) {
+ addATo(
+ tdPresent,
+ LABEL_PRESENT,
+ `${PATH_PREFIX_FILE}${fileData.yt_id}`
+ );
+ addTextTo(
+ tdPresent,
+ " "
+ );
+ addPlayerBtnTo(
+ tdPresent,
+ LABEL_ADD_NEXT,
+ `${CMD_ADD_NEXT}_${fileData.digest}`
+ );
+ addPlayerBtnTo(
+ tdPresent,
+ LABEL_ADD_PLAY,
+ `${CMD_ADD_PLAY}_${fileData.digest}`
+ );
+ } else {
+ tdPresent.textContent = LABEL_ABSENT;
+ }
+};
+
+const sendUpdate = async ( // eslint-disable-line one-var
+ button
+) => {
+ const
+ addedTag = document.getElementById(ID_TAG_ADDED).value,
+ flags = [],
+ tags = [];
+ Array.from(document.getElementsByClassName(CLS_TAG_LISTED)).forEach(
+ (tr) => {
+ if (
+ tr.children[
+ IDX_TAG_CHECKBOX_TD
+ ].children[
+ IDX_TAG_CHECKBOX_TD_INPUT
+ ].checked
+ ) {
+ tags.push(
+ tr.children[
+ IDX_TAG_LINK_TD
+ ].children[
+ IDX_TAG_LINK_TD_A
+ ].textContent
+ );
+ }
+ }
+ );
+ if (
+ addedTag.length > LEN_EMPTY &&
+ button === document.getElementById(ID_BTN_ADD_TAG)
+ ) {
+ tags.push(addedTag);
+ }
+ if (!document.getElementById(ID_BTN_SYNC).checked) {
+ flags.push(FLAG_NO_SYNC);
+ }
+ await wrappedPost(
+ PATH_FILE_JSON,
+ {
+ "delete_locally": (
+ button ===
+ document.getElementById(ID_BTN_UNLINK)
+ ),
+ flags,
+ tags
+ }
+ );
+ updateFileData();
+};
+
+window.addEventListener(
+ "load",
+ () => {
+ allowEdit = document.getElementById(ID_TAG_ADDED) !== null;
+ updateFileData();
+ if (allowEdit) {
+ assignOnclicks(
+ EDIT_BUTTONS,
+ (btn) => sendUpdate(btn)
+ );
+ }
+ }
+);
+
+subscribeEvents();
+
+++ /dev/null
-{% extends '_base.tmpl' %}
-
-
-{% block script %}
-const PATH_FILE_JSON = `/{{page_names.file_json}}/` + "{{file.digest.b64}}";
-
-async function update_file_data() {
- const file_data = await wrapped_fetch(PATH_FILE_JSON).then((response) => response.json());
- Array.from(document.getElementsByClassName("listed_tags")).forEach((row) => row.remove());
- file_data.tags_showable.forEach((tag) => {
- const tr = add_child_to("tr", document.getElementById("tags_table"));
- tr.classList.add("listed_tags");
- {% if allow_edit %}
- td_checkbox = add_child_to("td", tr);
- td_checkbox.classList.add("tag_checkboxes");
- add_child_to("input", td_checkbox, {type: "checkbox", checked: true, onclick: send_update});
- {% endif %}
- add_a_to(add_child_to("td", add_child_to("td", tr)), tag,
- `${PATH_FILES}?needed_tag=${encodeURIComponent(tag)}`);
- });
- {% if allow_edit %}
- document.getElementById("sync_checkbox").checked = ! file_data.flags.includes("do not sync");
- document.getElementById("added_tag").value = "";
- const datalist = document.getElementById("unused_tags");
- datalist.innerHTML = "";
- file_data.unused_tags.forEach((tag) => { add_child_to("option", datalist, {textContent: tag}); });
- {% endif %}
- td_present = document.getElementById("presence");
- td_present.innerHTML = "";
- if (file_data.present) {
- add_a_to(td_present, "yes", "/{{page_names.download}}/{{file.yt_id}}");
- add_text_to(td_present, " ");
- add_player_btn_to(td_present, 'add as next', `inject_${file_data.digest}`);
- add_player_btn_to(td_present, 'add and play', `injectplay_${file_data.digest}`);
- } else {
- td_present.textContent = "no";
- }
-}
-
-async function send_update(button) {
- let tags = []
- Array.from(document.getElementsByClassName("listed_tags")).forEach((tr) => {
- if (tr.children[0].children[0].checked) {
- tags.push(tr.children[1].children[0].textContent);
- }
- });
- const added_tag = document.getElementById("added_tag").value;
- if (added_tag.length > 0 && button == document.getElementById("add_tag_button")) {
- tags.push(added_tag);
- }
- let flags = []
- if (! document.getElementById("sync_checkbox").checked) {
- flags.push("do not sync");
- }
- await wrapped_post(PATH_FILE_JSON, {
- flags: flags,
- tags: tags,
- delete_locally: button == document.getElementById("unlink_button")
- });
- update_file_data();
-}
-
-window.addEventListener('load', update_file_data);
-{% endblock %}
-
-
-{% block css %}
-td.top_field { width: 100%; }
-td.tag_checkboxes { width: 1em; }
-td.dangerous { text-align: right}
-td.dangerous > form > input[type=submit], td.dangerous > button { background-color: black; color: red; }
-{% endblock %}
-
-
-{% block body %}
-</div>
-
-<table>
-
-<tr>
-<th>path:</th>
-<td class="top_field">{{file.rel_path}}</td>
-</tr>
-
-<tr>
-<th>present:</th>
-<td id="presence">
-</td>
-</tr>
-
-<tr>
-<th>YouTube ID:</th>
-<td><a href="/{{page_names.yt_result}}/{{file.yt_id}}">{{file.yt_id}}</a>
-</tr>
-
-<tr>
-<th>duration</th>
-<td>{{file.duration()}}</td>
-</tr>
-
-<tr>
-<th>tags</th>
-<td>
-<table id="tags_table">
-{% if allow_edit %}
-<tr>
-<td class="tag_checkboxes"><button id="add_tag_button" onclick="send_update(this)" >add:</button></td>
-<td>
-<input id="added_tag" list="unused_tags" autocomplete="off" />
-<datalist id="unused_tags" />
-</datalist>
-</td>
-</tr>
-{% endif %}
-</table>
-</td>
-</tr>
-
-{% if allow_edit %}
-<tr>
-<th>options</th>
-<td>
-<table>
-<tr>
-<td>
-<input id="sync_checkbox" type="checkbox" onclick="send_update()" /> do sync<br />
-</td>
-<td class="dangerous">
-<button id="unlink_button" onclick="send_update(this)" />delete locally</button>
-<form action="/{{page_names.file_kill}}/{{file.digest.b64}}" method="POST" />
-<input type="submit" value="KILL" />
-</form>
-</td>
-</tr>
-</table>
-</td>
-</tr>
-{% endif %}
-
-</table>
-{% endblock %}
--- /dev/null
+{% extends '_base.tmpl' %}
+{% import '_macros.tmpl' as macros %}
+
+
+{% block css %}
+{{ macros.css_sortable_table() }}
+{% endblock %}
+
+
+{% block body %}
+filename pattern: <input id="input_filter_path" /><br />
+{% if allow_show_absent %}
+show absent: <input id="input_show_absent" type="checkbox"/><br />
+{% endif %}
+needed tags: <select id="tags_select"></select><br />
+<span id="tags"></span>
+<hr />
+</div>
+<p>
+known files (shown: <span id="files_count">?</span>):
+<button id="inject_all">add all as next</button>
+</p>
+<table id="sortable_table">
+<tr>
+<th><button id="clickable:size" class="sorter">size</button></th>
+<th><button id="clickable:duration" class="sorter">duration</button></th>
+<th>actions</th>
+<th>tags <button id="clickable:n_tags" class="sorter">count</button></th>
+<th><button id="clickable:rel_path" class="sorter">path</button></th>
+<th><button id="clickable:-last_updates" class="sorter">last update</button></th>
+</tr>
+</table>
+{% endblock %}
--- /dev/null
+/* global
+URLSearchParams,
+console,
+document,
+window,
+*/
+/*
+eslint
+"function-paren-newline": "off",
+"newline-after-var": "off",
+no-console: [
+ "error",
+ { "allow": ["dir", "log", "warn", "error"] }
+],
+"max-statements": [
+ "error",
+ 13
+],
+"padded-blocks": [
+ "error",
+ "never"
+],
+*/
+import {
+ CMD_ADD_NEXT,
+ CMD_ADD_PLAY,
+ IDX_START,
+ LABEL_ADD_NEXT,
+ LABEL_ADD_PLAY,
+ LEN_EMPTY,
+ PARAM_TAG_NEEDED,
+ PATH_FILES_JSON,
+ PATH_PREFIX_FILE,
+ PREFIX_MINUS,
+ SYMBOL_RM,
+ addATdTo,
+ addButtonTo,
+ addChildTo,
+ addPlayerBtnTo,
+ addTagLinksTo,
+ addTdTo,
+ addTextTo,
+ drawSortableTable,
+ playerCommand,
+ sortableTableSetup,
+ subscribeEvents,
+ wrappedFetch
+} from "./_base.js";
+
+const
+ COLNAME_DURATION = "duration",
+ COLNAME_LAST_UPDATES = "last_updates",
+ COLNAME_N_TAGS = "n_tags",
+ COLNAME_REL_PATH = "rel_path",
+ COLNAME_SIZE = "size",
+ ID_ADD_ALL = "inject_all",
+ ID_FILES_COUNT = "files_count",
+ ID_INPUT_FILTER_PATH = "input_filter_path",
+ ID_INPUT_SHOW_ABSENT = "input_show_absent",
+ ID_TAGS = "tags",
+ ID_TAGS_SELECT = "tags_select",
+ LABEL_ADD = "add",
+ PARAM_FILTER_PATH = "filter_path",
+ PARAM_SHOW_ABSENT = "show_absent",
+ TAGS_SHOWABLE = {{showable_tags|tojson|safe}};
+
+let
+ inputFilterPath = null,
+ inputShowAbsent = null,
+ neededTags = new URLSearchParams(
+ window.location.search
+ ).getAll(PARAM_TAG_NEEDED),
+ tagsSelect = null,
+ tagsSpan = null;
+sortableTableSetup.itemsForSortableTable = [];
+sortableTableSetup.sortCols = [
+ COLNAME_SIZE,
+ COLNAME_DURATION,
+ COLNAME_N_TAGS,
+ COLNAME_REL_PATH,
+ `${PREFIX_MINUS}${COLNAME_LAST_UPDATES}`
+];
+sortableTableSetup.sortKey = COLNAME_REL_PATH;
+sortableTableSetup.populateListItemRow = (
+ tr,
+ file
+) => {
+ addTdTo(
+ tr,
+ {"textContent": file.size}
+ );
+ addTdTo(
+ tr,
+ {"textContent": file.duration}
+ );
+ const tdInject = addTdTo(tr);
+ addPlayerBtnTo(
+ tdInject,
+ LABEL_ADD_NEXT,
+ `${CMD_ADD_NEXT}_${file.digest}`,
+ !file.present
+ );
+ addPlayerBtnTo(
+ tdInject,
+ LABEL_ADD_PLAY,
+ `${CMD_ADD_PLAY}_${file.digest}`,
+ !file.present
+ );
+ addTagLinksTo(
+ addTdTo(tr),
+ file.tags_showable
+ );
+ addATdTo(
+ tr,
+ file.rel_path,
+ `${PATH_PREFIX_FILE}${file.digest}`
+ );
+ addTdTo(
+ tr,
+ {"textContent": file.last_update}
+ );
+};
+
+const updateFilesList = async ( // eslint-disable-line one-var
+) => {
+ const filterPath = encodeURIComponent(inputFilterPath.value);
+ let target = `${PATH_FILES_JSON}?${PARAM_FILTER_PATH}=${filterPath}`;
+ if (inputShowAbsent && inputShowAbsent.checked) {
+ target = `${target}&${PARAM_SHOW_ABSENT}=1`;
+ }
+ neededTags.forEach((tag) => {
+ target = `${target}&${PARAM_TAG_NEEDED}=${encodeURIComponent(tag)}`;
+ });
+ try {
+ const fetched = await wrappedFetch(target);
+ sortableTableSetup.itemsForSortableTable = await fetched.json();
+ document.getElementById(ID_FILES_COUNT).textContent =
+ `${sortableTableSetup.itemsForSortableTable.length}`;
+ drawSortableTable();
+ } catch (error) {
+ console.log(`Error updating ${target} files list: ${error.message}`);
+ console.dir(error);
+ throw error;
+ }
+};
+
+
+const updateFilterInputs = ( // eslint-disable-line one-var
+) => {
+ tagsSpan.innerHTML = "";
+ while (tagsSelect.options.length > LEN_EMPTY) {
+ tagsSelect.remove(IDX_START);
+ }
+ addChildTo(
+ "option",
+ tagsSelect,
+ {"textContent": LABEL_ADD}
+ );
+ TAGS_SHOWABLE.forEach((tag) => {
+ if (!neededTags.includes(tag)) {
+ addChildTo(
+ "option",
+ tagsSelect,
+ {"textContent": tag}
+ );
+ }
+ });
+ neededTags.forEach((chosenTag) => {
+ addTextTo(
+ tagsSpan,
+ ` ${chosenTag} `
+ );
+ addButtonTo(
+ tagsSpan,
+ SYMBOL_RM,
+ false,
+ () => {
+ neededTags = neededTags.filter((tag) => tag !== chosenTag);
+ updateFilterInputs();
+ }
+ );
+ });
+ updateFilesList();
+};
+
+window.addEventListener(
+ "load",
+ () => {
+ inputFilterPath = document.getElementById(ID_INPUT_FILTER_PATH);
+ inputFilterPath.oninput = updateFilesList;
+ tagsSelect = document.getElementById(ID_TAGS_SELECT);
+ tagsSpan = document.getElementById(ID_TAGS);
+ updateFilterInputs();
+ inputShowAbsent = document.getElementById(ID_INPUT_SHOW_ABSENT);
+ if (inputShowAbsent) {
+ inputShowAbsent.onclick = updateFilesList;
+ }
+ document.getElementById(ID_ADD_ALL).onclick = () => {
+ sortableTableSetup.itemsForSortableTable.forEach(
+ (file) => playerCommand(`${CMD_ADD_NEXT}_${file.digest}`)
+ );
+ };
+ tagsSelect.onchange = () => {
+ if (tagsSelect.selectedIndex > IDX_START) {
+ neededTags.push(
+ document.getElementById(ID_TAGS_SELECT).value
+ );
+ }
+ updateFilterInputs();
+ };
+ }
+);
+
+subscribeEvents();
+
+++ /dev/null
-{% extends '_base.tmpl' %}
-
-
-{% block script %}
-const PATH_FILES_JSON = "/{{page_names.files_json}}";
-
-const all_tags = {{showable_tags|tojson|safe}};
-var needed_tags = {{needed_tags|tojson|safe}};
-
-function select_tag() {
- if (tags_select.selectedIndex < 1) {
- return;
- }
- const chosen_tag = document.getElementById('tags_select').value;
- needed_tags.push(chosen_tag);
- update_filter_inputs();
-}
-
-function update_filter_inputs() {
- const tags_select = document.getElementById('tags_select');
- while (tags_select.options.length > 0) {
- tags_select.remove(0);
- }
- add_child_to('option', tags_select, {textContent: 'add'});
- all_tags.forEach((tag) => {
- if (needed_tags.includes(tag)) {
- return;
- }
- add_child_to('option', tags_select, {textContent: tag});
- });
- const tags_div = document.getElementById("tags");
- tags_div.innerHTML = '';
- needed_tags.forEach((chosen_tag) => {
- const tag_text_node = add_text_to(tags_div, ` ${chosen_tag} `);
- add_child_to('button', tags_div, {
- textContent: 'x',
- onclick: function() {
- tag_text_node.remove();
- btn_del.remove();
- needed_tags = needed_tags.filter(tag => tag !== chosen_tag);
- update_filter_inputs();
- }
- });
- });
- update_files_list();
-}
-
-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 allow_show_absent %}
- if (document.getElementById("input_show_absent").checked) { target = `${target}&show_absent=1`; }
- {% endif %}
- needed_tags.forEach((tag) => target = `${target}&needed_tag=${encodeURIComponent(tag)}`);
- 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() {
- items_for_sortable_table.forEach((file) => { player_command(`inject_${file.digest}`) });
-}
-
-{{ macros.js_manage_sortable_table("rel_path") }}
-var items_for_sortable_table = [];
-function populate_list_item_row(tr, file) {
- add_child_to('td', tr, {textContent: file.size});
- add_child_to('td', tr, {textContent: file.duration});
- const td_inject = add_child_to('td', tr);
- add_player_btn_to(td_inject, 'add as next', `inject_${file.digest}`, !file.present)
- add_player_btn_to(td_inject, 'add as play', `injectplay_${file.digest}`, !file.present)
- add_tag_links_to(add_child_to('td', tr), file.tags_showable);
- add_a_to(add_child_to('td', tr), file.rel_path, `${PATH_PREFIX_FILE}${file.digest}`);
- add_child_to('td', tr, {textContent: file.last_update});
-}
-window.addEventListener('load', update_filter_inputs);
-{% endblock %}
-
-
-{% block css %}
-{{ macros.css_sortable_table() }}
-{% endblock %}
-
-
-{% block body %}
-filename pattern: <input id="input_filter_path" oninput="update_files_list()" /><br />
-{% if allow_show_absent %}
-show absent: <input id="input_show_absent" type="checkbox" onclick="update_files_list()" /><br />
-{% endif %}
-needed tags: <select id="tags_select" onchange="select_tag()"></select><br />
-<span id="tags"></span>
-<hr />
-</div>
-<p>
-known files (shown: <span id="files_count">?</span>):
-<button onclick="inject_all();">add all as next</button>
-</p>
-<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, 'n_tags'); ">count</button></th>
-<th><button class="sorter" onclick="sort_by(this, 'rel_path'); ">path</button></th>
-<th><button class="sorter" onclick="sort_by(this, '-last_update'); ">last update</button></th>
-</tr>
-</table>
-{% endblock %}
--- /dev/null
+{% extends '_base.tmpl' %}
+
+
+{% block css %}
+html {
+ scroll-padding-top: 7.5em; /* so anchor jumps pad off sticky header */
+}
+td.entry_control {
+ width: 6em;
+}
+{% endblock %}
+
+
+{% block body %}
+<a href="#playing">jump to playing</a>
+<button id="clickable:rebuild">rebuild</button>
+<button id="clickable:empty">empty</button>
+<hr />
+<table id="playlist_rows">
+</table>
+{% endblock %}
--- /dev/null
+/* global
+window
+*/
+/*
+eslint
+"capitalized-comments": [
+ "error",
+ "never"
+],
+"function-paren-newline": "off",
+"line-comment-position": [
+ "error",
+ { "position": "beside" }
+],
+"max-lines-per-function": [
+ "error",
+ 51
+],
+"max-statements": [
+ "error",
+ 12
+],
+"newline-after-var": "off",
+"no-inline-comments": "off",
+"no-multi-spaces": [
+ "error",
+ { "ignoreEOLComments": true }
+],
+"padded-blocks": [
+ "error",
+ "never"
+],
+ */
+import {
+ BUTTONS_UP_DOWN,
+ CMD_RM,
+ PATH_PREFIX_FILE,
+ SYMBOL_RM,
+ addATdTo,
+ addATo,
+ addPlayerBtnTo,
+ addTdTo,
+ assignOnclicks,
+ drawTable,
+ eventHandlers,
+ playerCommand,
+ subscribeEvents
+} from "./_base.js";
+
+const
+ BUTTONS_ENTRY = [
+ [
+ ">",
+ "jump"
+ ]
+ ].concat(BUTTONS_UP_DOWN).concat([
+ [
+ SYMBOL_RM,
+ CMD_RM
+ ]
+ ]),
+ BUTTONS_PLAYLIST = [
+ "empty",
+ "rebuild"
+ ],
+ CLS_ENTRY_CTL = "entry_control",
+ CLS_PLAYLIST_ROW = "playlist_row",
+ ID_TABLE = `${CLS_PLAYLIST_ROW}s`,
+ PARAM_PLAYLIST = "playlist",
+ TOK_ANCHOR = "#",
+ TOK_PLAYING = "playing";
+
+let
+ firstLoad = true;
+
+eventHandlers.playlist = (
+ update
+) => { // update playlist
+ drawTable(
+ ID_TABLE,
+ CLS_PLAYLIST_ROW,
+ update.playlist_files,
+ (
+ tr,
+ file,
+ idx
+ ) => {
+ addATo(
+ addTdTo(
+ tr,
+ {"id": `${idx}`}
+ ),
+ TOK_ANCHOR,
+ `#${idx}`
+ );
+ const tdEntryControl = addTdTo(tr);
+ tdEntryControl.classList.add(CLS_ENTRY_CTL);
+ addATdTo(
+ tr,
+ file.rel_path,
+ `${PATH_PREFIX_FILE}${file.digest}`
+ );
+ if (idx === update.idx) { // currently playing
+ tdEntryControl.textContent = TOK_PLAYING;
+ tdEntryControl.id = TOK_PLAYING;
+ if (firstLoad) { // replace anchor jump to #playing
+ firstLoad = false; // (initially un-built, won"t work)
+ tdEntryControl.scrollIntoView({"block": "center"});
+ }
+ } else { // only non-playing items get playlist manip. buttons
+ for (
+ const [
+ symbol,
+ prefix
+ ] of BUTTONS_ENTRY
+ ) {
+ addPlayerBtnTo(
+ tdEntryControl,
+ symbol,
+ `${prefix}_${idx}`
+ );
+ }
+ }
+ }
+ );
+};
+
+window.addEventListener(
+ "load",
+ () => assignOnclicks(
+ BUTTONS_PLAYLIST,
+ (
+ ignore,
+ command
+ ) => playerCommand(command)
+ )
+);
+
+subscribeEvents(`${PARAM_PLAYLIST}=1`);
+
+++ /dev/null
-{% extends '_base.tmpl' %}
-
-
-{% block css %}
-html { scroll-padding-top: 7.5em; } /* so anchor jumps pad off sticky header */
-td.entry_control { width: 6em; }
-{% endblock %}
-
-
-{% block script %}
-const CLS_PLAYLIST_ROW = 'playlist_row';
-events_params += 'playlist=1';
-var first_load = true;
-
-event_handlers.playlist = function(data) { // update playlist
- const table = document.getElementById('playlist_rows');
- var old_rows = document.getElementsByClassName(CLS_PLAYLIST_ROW);
- while (old_rows[0]) { old_rows[0].remove(); }
- for (let i = 0; i < data.playlist_files.length; i++) {
- const file = data.playlist_files[i];
- const tr = add_child_to('tr', table);
- tr.classList.add(CLS_PLAYLIST_ROW);
- add_a_to(add_child_to('td', tr, {id: `${i}`}), '#', `#${i}`);
- const td_entry_control = add_child_to('td', tr);
- td_entry_control.classList.add('entry_control');
- if (i == data.idx) {
- td_entry_control.textContent = 'playing';
- td_entry_control.id = 'playing';
- if (first_load) { // to replace anchor jump to #playing, which on first
- first_load = false; // load will not work yet since element not built yet
- td_entry_control.scrollIntoView({block: 'center'});
- }
- } else {
- for (const [symbol, prefix] of [['>', 'jump'],
- ['^', 'up'],
- ['v', 'down'],
- ['x', 'rm']]) {
- add_player_btn_to(td_entry_control, symbol, `${prefix}_${i}`);
- }
- }
- add_a_to(add_child_to('td', tr), file.rel_path, `${PATH_PREFIX_FILE}${file.digest}`);
- }
-};
-{% endblock %}
-
-
-{% block body %}
-<a href="#playing">jump to playing</a>
-<button onclick="player_command('rebuild')">rebuild</button>
-<button onclick="player_command('empty')">empty</button>
-<hr />
-</div>
-<table id="playlist_rows">
-</table>
-{% endblock %}
--- /dev/null
+{% extends '_base.tmpl' %}
+{% import '_macros.tmpl' as macros %}
+
+
+{% block css %}
+{{ macros.css_sortable_table() }}
+#sortable_table { width: auto; }
+{% endblock %}
+
+
+{% block body %}
+</div>
+
+<table id="sortable_table">
+<tr>
+<th>
+<button id="clickable:name" class="sorter">name</button>
+</th>
+<th>
+<button id="clickable:number" class="sorter">usage number</button>
+</th>
+</tr>
+</table>
+
+{% endblock %}
--- /dev/null
+/*
+eslint
+"capitalized-comments": [
+ "error",
+ "never"
+],
+"padded-blocks": [
+ "error",
+ "never"
+],
+ */
+import {
+ PARAM_TAG_NEEDED,
+ PATH_FILES,
+ addATdTo,
+ addTdTo,
+ sortableTableSetup,
+ subscribeEvents
+} from "./_base.js";
+
+const
+ COLNAME_NAME = "name",
+ COLNAME_NUMBER = "number";
+
+sortableTableSetup.itemsForSortableTable = {{tags|tojson|safe}};
+sortableTableSetup.sortCols = [
+ COLNAME_NAME,
+ COLNAME_NUMBER
+];
+sortableTableSetup.sortKey = COLNAME_NAME;
+sortableTableSetup.populateListItemRow = (
+ tr,
+ tag
+) => {
+ addATdTo(
+ tr,
+ tag.name,
+ `${PATH_FILES}?${PARAM_TAG_NEEDED}=${encodeURIComponent(tag.name)}`
+ );
+ addTdTo(
+ tr,
+ {"textContent": tag.number}
+ );
+};
+
+subscribeEvents();
+
+++ /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) {
- add_a_to(add_child_to('td', tr), tag.name,
- `${PATH_FILES}?needed_tag=${encodeURIComponent(tag.name)}`);
- add_child_to('td', tr, {textContent: tag.number});
-}
-window.addEventListener('load', draw_sortable_table);
-{% endblock %}
-
-
-{% block css %}
-{{ macros.css_sortable_table() }}
-#sortable_table { width: auto; }
-{% endblock %}
-
-
-{% block body %}
-</div>
-
-<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 %}
-
--- /dev/null
+{% extends '_base.tmpl' %}
+
+
+{% block body %}
+<p>quota: {{quota_count}}/100000</p>
+<form action="/yt_queries" method="POST" />
+<input name="query" />
+</form>
+</div>
+<table>
+<tr>
+<th>retrieved at</th>
+<th>query</th>
+</tr>
+{% for query in queries %}
+<tr>
+<td>{{query.retrieved_at[:19]}}</td>
+<td><a href="/yt_query/{{query.id_}}">{{query.text}}</a></td>
+</tr>
+{% endfor %}
+</table>
+{% endblock %}
--- /dev/null
+import {
+ subscribeEvents
+} from "./_base.js";
+
+subscribeEvents();
+
+++ /dev/null
-{% extends '_base.tmpl' %}
-
-
-{% block body %}
-<p>quota: {{quota_count}}/100000</p>
-<form action="/{{page_names.yt_queries}}" method="POST" />
-<input name="query" />
-</form>
-</div>
-
-<table>
-<tr>
-<th>retrieved at</th>
-<th>query</th>
-</tr>
-{% for query in queries %}
-<tr>
-<td>{{query.retrieved_at[:19]}}</td>
-<td><a href="/{{page_names.yt_query}}/{{query.id_}}">{{query.text}}</a></td>
-</tr>
-{% endfor %}
-</table>
-{% endblock %}
--- /dev/null
+{% extends '_base.tmpl' %}
+
+
+{% block body %}
+<p>query: {{query}}</p>
+<hr />
+</div>
+<table>
+{% for video in videos %}
+<tr>
+<td>
+<a href="/yt_result/{{video.id_}}"><img src="/thumbnails/{{video.id_}}.jpg" /></a>
+</td>
+<td>
+{{video.definition}}<br />
+{{video.duration}}
+</td>
+<td>
+<b><a href="/yt_result/{{video.id_}}">{{video.title}}</a></b> · {{video.description}}
+</td>
+</tr>
+{% endfor %}
+</table>
+{% endblock %}
--- /dev/null
+import {
+ subscribeEvents
+} from "./_base.js";
+
+subscribeEvents();
+
--- /dev/null
+{% extends '_base.tmpl' %}
+
+
+{% block body %}
+<table id="video_table" dat>
+<tr><th>title:</th><td>{{video_data.title}}</td></tr>
+<tr><th>thumbnail:</th><td><img src="/thumbnails/{{video_data.id_}}.jpg" /></td></tr>
+<tr><th>description:</th><td>{{video_data.description}}</td></tr>
+<tr><th>duration:</th><td>{{video_data.duration}}</td></tr>
+<tr><th>definition:</th><td>{{video_data.definition}}</td></tr>
+<tr><th>YouTube ID:</th><td>{% if youtube_prefix|length > 0 %}<a href="{{youtube_prefix}}{{video_data.id_}}">{{video_data.id_}}{% else %}{{video_data.id_}}{% endif %}</td></tr>
+<tr><th>download:</th><td id="status"></td></tr>
+<tr>
+<th>linked queries:</th>
+<td>
+<ul>
+{% for query in queries %}<li><a href="/yt_query/{{query.id_}}">{{query.text}}</a>{% endfor %}
+</ul>
+</td>
+</tr>
+</table>
+{% endblock %}
--- /dev/null
+/* global
+document,
+window
+*/
+/*
+eslint
+"function-paren-newline": "off",
+"newline-after-var": "off",
+"padded-blocks": [
+ "error",
+ "never"
+],
+*/
+import {
+ IDX_PATH_ID,
+ PATH_PREFIX_DOWNLOAD,
+ PATH_PREFIX_FILE,
+ addATo,
+ addTextTo,
+ eventHandlers,
+ subscribeEvents
+} from "./_base.js";
+
+const
+ ID_TD_STATUS = "status",
+ PARAM_DOWNLOAD = "download",
+ TOK_ABSENT = "absent",
+ TOK_PRESENT = "present",
+ TOK_Q_DOWNLOAD = "download?",
+ ytId = window.location.pathname.split("/")[IDX_PATH_ID];
+
+eventHandlers.download = (
+ data
+) => {
+ const td = document.getElementById(ID_TD_STATUS);
+ td.innerHTML = "";
+ if (data.status === TOK_ABSENT) {
+ addATo(
+ td,
+ TOK_Q_DOWNLOAD,
+ `${PATH_PREFIX_DOWNLOAD}${ytId}`
+ );
+ } else if (data.status === TOK_PRESENT) {
+ addATo(
+ td,
+ data.path,
+ `${PATH_PREFIX_FILE}${data.digest}`
+ );
+ } else {
+ addTextTo(
+ td,
+ `${data.status}`
+ );
+ }
+};
+
+subscribeEvents(`${PARAM_DOWNLOAD}=${ytId}`);
+
+++ /dev/null
-{% extends '_base.tmpl' %}
-
-
-{% block script %}
-events_params += 'download={{video_data.id_}}';
-event_handlers.download = function(data) {
- const td = document.getElementById("status");
- td.innerHTML = "";
- if ("absent" == data.status) {
- add_a_to(
- parent=td,
- text="download?",
- href="/{{page_names.download}}/{{video_data.id_}}");
- } else if ("present" == data.status) {
- add_a_to(
- parent=td,
- text=data.path,
- href="/{{page_names.file}}/" + data.digest);
- } else {
- add_text_to(
- parent=td,
- text=`${data.status}`); }
-}
-{% endblock %}
-
-
-{% block body %}
-<table>
-<tr><th>title:</th><td>{{video_data.title}}</td></tr>
-<tr><th>thumbnail:</th><td><img src="/{{page_names.thumbnails}}/{{video_data.id_}}.jpg" /></td></tr>
-<tr><th>description:</th><td>{{video_data.description}}</td></tr>
-<tr><th>duration:</th><td>{{video_data.duration}}</td></tr>
-<tr><th>definition:</th><td>{{video_data.definition}}</td></tr>
-<tr><th>YouTube ID:</th><td>{% if youtube_prefix|length > 0 %}<a href="{{youtube_prefix}}{{video_data.id_}}">{{video_data.id_}}{% else %}{{video_data.id_}}{% endif %}</td></tr>
-<tr><th>download:</th><td id="status"></td></tr>
-<tr>
-<th>linked queries:</th>
-<td>
-<ul>
-{% for query in queries %}<li><a href="/{{page_names.yt_query}}/{{query.id_}}">{{query.text}}</a>{% endfor %}
-</ul>
-</td>
-</tr>
-</table>
-{% endblock %}
+++ /dev/null
-{% extends '_base.tmpl' %}
-
-
-{% block body %}
-<p>query: {{query}}</p>
-<hr />
-</div>
-<table>
-{% for video in videos %}
-<tr>
-<td>
-<a href="/{{page_names.yt_result}}/{{video.id_}}"><img src="/{{page_names.thumbnails}}/{{video.id_}}.jpg" /></a>
-</td>
-<td>
-{{video.definition}}<br />
-{{video.duration}}
-</td>
-<td>
-<b><a href="/{{page_names.yt_result}}/{{video.id_}}">{{video.title}}</a></b> · {{video.description}}
-</td>
-</tr>
-{% endfor %}
-</table>
-{% endblock %}
PlomHttpHandler, PlomHttpServer, PlomQueryMap, MIME_APP_JSON)
from ytplom.db import Hash, DbConn
from ytplom.misc import (
- FILE_FLAGS, PATH_THUMBNAILS, YOUTUBE_URL_PREFIX, ensure_expected_dirs,
+ CMD_DOWN, CMD_RM, CMD_UP, FILE_FLAGS, PATH_THUMBNAILS, YOUTUBE_URL_PREFIX,
+ ensure_expected_dirs,
Config, DownloadsManager, FilterStr, FlagName, Player, QueryId, QueryText,
QuotaLog, TagSet, VideoFile, YoutubeId, YoutubeQuery, YoutubeVideo
)
_THUMBNAIL_URL_PREFIX = 'https://i.ytimg.com/vi/'
_THUMBNAIL_URL_SUFFIX = '/default.jpg'
-# template paths
-_PATH_TEMPLATES = PATH_APP_DATA.joinpath('templates')
-_NAME_TEMPLATE_DOWNLOADS = Path('downloads.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')
-
-# page names
-PAGE_NAMES: dict[str, str] = {
- 'download': 'dl',
- 'downloads': 'downloads',
- 'downloads_json': 'downloads.json',
- 'events': 'events',
- 'file': 'file',
- 'file_kill': 'file_kill',
- 'file_json': 'file.json',
- 'files': 'files',
- 'files_json': 'files.json',
- 'missing': 'missing',
- 'player': 'player',
- 'playlist': 'playlist',
- 'purge': 'purge',
- 'tags': 'tags',
- 'thumbnails': 'thumbnails',
- 'yt_queries': 'yt_queries',
- 'yt_query': 'yt_query',
- 'yt_result': 'yt_result'
-}
-
# misc
+_PATH_TEMPLATES = PATH_APP_DATA.joinpath('templates')
_PING_INTERVAL_S = 1
_EVENTS_UPDATE_INTERVAL_S = 0.1
_HEADER_CONTENT_TYPE = 'Content-Type'
def do_POST(self) -> None: # pylint:disable=invalid-name
"""Map POST requests to handlers for various paths."""
- if self.pagename == PAGE_NAMES['downloads_json']:
+ if self.pagename == 'downloads.json':
self._receive_downloads()
- elif self.pagename == PAGE_NAMES['file_json']:
+ elif self.pagename == 'file.json':
self._update_file()
- elif self.pagename == PAGE_NAMES['file_kill']:
+ elif self.pagename == 'file_kill':
self._kill_file()
- elif self.pagename == PAGE_NAMES['player']:
+ elif self.pagename == 'player':
self._receive_player_command()
- elif self.pagename == PAGE_NAMES['purge']:
+ elif self.pagename == 'purge':
self._purge_deleted_files()
- elif self.pagename == PAGE_NAMES['yt_queries']:
+ elif self.pagename == 'yt_queries':
self._receive_yt_query()
def _receive_downloads(self) -> None:
self.server.player.load_files_and_mpv(empty=True)
elif command.startswith('jump_'):
self.server.player.jump_to(int(command.split('_')[1]))
- elif command.startswith('up_'):
+ elif command.startswith(CMD_UP + '_'):
self.server.player.move_entry(int(command.split('_')[1]))
- elif command.startswith('down_'):
+ elif command.startswith(CMD_DOWN + '_'):
self.server.player.move_entry(int(command.split('_')[1]), False)
- elif command.startswith('rm_'):
+ elif command.startswith(CMD_RM + '_'):
self.server.player.remove_by_idx(int(command.split('_')[1]))
elif (command.startswith('inject_')
or command.startswith('injectplay_')):
query_data = YoutubeQuery.new_by_request_saved(
conn, self.server.config, query_txt)
conn.commit()
- self._redirect(Path('/')
- .joinpath(PAGE_NAMES['yt_query'])
- .joinpath(query_data.id_))
+ self._redirect(Path('/', 'yt_query', query_data.id_))
def do_GET(self) -> None: # pylint:disable=invalid-name
"""Map GET requests to handlers for various paths."""
try:
- if self.pagename == PAGE_NAMES['download']:
+ if self.pagename == 'download':
self._send_or_download_video()
- elif self.pagename == PAGE_NAMES['downloads']:
- self._send_downloads()
- elif self.pagename == PAGE_NAMES['events']:
+ elif self.pagename == 'events':
self._send_events()
- elif self.pagename == PAGE_NAMES['file']:
- self._send_file_data()
- elif self.pagename == PAGE_NAMES['file_json']:
+ elif self.pagename == 'file.json':
self._send_file_json()
- elif self.pagename == PAGE_NAMES['files']:
- self._send_files_index()
- elif self.pagename == PAGE_NAMES['files_json']:
+ elif self.pagename == 'files.json':
self._send_files_json()
- elif self.pagename == PAGE_NAMES['missing']:
+ elif self.pagename == 'missing':
self._send_missing_json()
- elif self.pagename == PAGE_NAMES['tags']:
- self._send_tags_index()
- elif self.pagename == PAGE_NAMES['thumbnails']:
+ elif self.pagename == 'thumbnails':
self._send_thumbnail()
- elif self.pagename == PAGE_NAMES['yt_result']:
- self._send_yt_result()
- elif self.pagename == PAGE_NAMES['yt_queries']:
- self._send_yt_queries_index_and_search()
- elif self.pagename == PAGE_NAMES['yt_query']:
- self._send_yt_query_page()
- else: # e.g. for /
- self._send_playlist()
+ elif self.pagename.endswith('.js'):
+ self._send_js()
+ else:
+ self._send_html()
except NotFoundException as e:
self.send_http(bytes(str(e), encoding='utf8'), code=404)
+ def _build_ctx(self, target_name: str) -> dict[str, Any]:
+ handler_name = '_send_' + target_name.replace('.', '_')
+ return (getattr(self, handler_name)() if hasattr(self, handler_name)
+ else {})
+
+ def _send_html(self) -> None:
+ tmpl_name = f'{self.pagename}.html'
+ tmpl_path = _PATH_TEMPLATES.joinpath(tmpl_name)
+ if not tmpl_path.exists():
+ self.pagename = 'playlist'
+ tmpl_name = f'{self.pagename}.html'
+ ctx = {'pagename': self.pagename,
+ 'background_color': self.server.config.background_color}
+ self.send_rendered(Path(tmpl_name), ctx | self._build_ctx(tmpl_name))
+
+ def _send_js(self) -> None:
+ ctx = self._build_ctx(self.pagename)
+ self.send_http(
+ bytes(self.server.jinja.get_template(self.pagename).render(**ctx),
+ encoding='utf8'),
+ [(_HEADER_CONTENT_TYPE, 'text/javascript')])
+
def _send_json(self, body: dict | list) -> None:
self.send_http(bytes(json_dumps(body), encoding='utf8'),
headers=[(_HEADER_CONTENT_TYPE, MIME_APP_JSON)])
) -> None:
tmpl_ctx['selected'] = tmpl_ctx.get('selected', '')
tmpl_ctx['background_color'] = self.server.config.background_color
- tmpl_ctx['page_names'] = PAGE_NAMES
self.send_rendered(tmpl_name, tmpl_ctx)
def _send_or_download_video(self) -> None:
file_data = VideoFile.get_by_yt_id(conn, video_id)
if not file_data.present:
raise NotFoundException
- self._redirect(Path('/')
- .joinpath(PAGE_NAMES['file'])
- .joinpath(file_data.digest.b64))
+ self._redirect(Path('/', 'file', file_data.digest.b64))
except NotFoundException:
self.server.downloads.q.put(f'queue_{video_id}')
- self._redirect(Path('/')
- .joinpath(PAGE_NAMES['yt_result'])
- .joinpath(video_id))
-
- def _send_downloads(self) -> None:
- self._send_rendered_template(_NAME_TEMPLATE_DOWNLOADS, {})
+ self._redirect(Path('/', 'yt_result', video_id))
def _send_events(self) -> None:
self.send_http(headers=[(_HEADER_CONTENT_TYPE, 'text/event-stream'),
last_updates['download'] = update['time']
payload['download'] = {k: update[k] for k in update
if k != 'time'}
- if 'downloads' in subscriptions:
- if last_updates['downloads'] < self.server.downloads.timestamp:
- last_updates['downloads'] = self.server.downloads.timestamp
- with DbConn() as conn:
- payload['downloads']\
- = self.server.downloads.overview(conn)
+ if 'downloads' in subscriptions\
+ and (last_updates['downloads']
+ < self.server.downloads.timestamp):
+ last_updates['downloads'] = self.server.downloads.timestamp
+ with DbConn() as conn:
+ payload['downloads']\
+ = self.server.downloads.overview(conn)
if not payload:
sleep(_EVENTS_UPDATE_INTERVAL_S)
- def _send_file_data(self) -> None:
+ def _send_file_html(self) -> dict[str, Any]:
with DbConn() as conn:
file = VideoFile.get_one(conn, Hash.from_b64(self.path_toks[2]))
- self._send_rendered_template(
- _NAME_TEMPLATE_FILE_DATA,
- {'allow_edit': self.server.config.allow_file_edit, 'file': file})
+ return {
+ 'allow_edit': self.server.config.allow_file_edit,
+ 'file': file}
def _send_file_json(self) -> None:
with DbConn() as conn:
file = VideoFile.get_one(conn, Hash.from_b64(self.path_toks[2]))
unused_tags = file.unused_tags(conn).as_str_list
- self._send_json(file.as_dict | {'unused_tags': unused_tags})
+ self._send_json(file.as_dict | {'unused_tags': unused_tags,
+ 'yt_id': file.yt_id})
- def _send_files_index(self) -> None:
+ def _send_files_js(self) -> dict[str, Any]:
with DbConn() as conn:
- showable_tags = sorted(list(VideoFile.all_tags_showable(conn)))
- self._send_rendered_template(_NAME_TEMPLATE_FILES, {
- 'allow_show_absent': self.server.config.allow_file_edit,
- 'showable_tags': showable_tags,
- 'needed_tags': self.params.all_for('needed_tag')})
+ showable_tags = VideoFile.all_tags_showable(conn)
+ return {'showable_tags': sorted(list(showable_tags))}
+
+ def _send_files_html(self) -> dict[str, Any]:
+ return {'allow_show_absent': self.server.config.allow_file_edit}
def _send_files_json(self) -> None:
with DbConn() as conn:
self._send_json([f.digest.b64 for f in VideoFile.get_all(conn)
if not f.present])
- def _send_tags_index(self) -> None:
+ def _send_tags_js(self) -> dict[str, Any]:
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})
+ return {
+ 'tags': [{'name': k, 'number': v} for k, v
+ in VideoFile.showable_tags_to_numbers(conn).items()]}
def _send_thumbnail(self) -> None:
filename = Path(self.path_toks[2])
with path_thumbnail.open('rb') as f:
self.send_http(f.read(), [(_HEADER_CONTENT_TYPE, 'image/jpg')])
- def _send_yt_result(self) -> None:
+ def _send_yt_result_html(self) -> dict[str, Any]:
video_id = YoutubeId(self.path_toks[2])
with DbConn() as conn:
linked_queries = [
video_data = YoutubeVideo.get_one(conn, video_id)
except NotFoundException:
video_data = YoutubeVideo(video_id)
- self._send_rendered_template(
- _NAME_TEMPLATE_YT_RESULT,
- {'video_data': video_data,
- 'queries': linked_queries,
- 'youtube_prefix': (
- YOUTUBE_URL_PREFIX if self.server.config.link_originals
- else '')})
-
- def _send_yt_queries_index_and_search(self) -> None:
+ return {
+ 'video_data': video_data,
+ 'queries': linked_queries,
+ 'youtube_prefix': (
+ YOUTUBE_URL_PREFIX if self.server.config.link_originals
+ else '')}
+
+ def _send_yt_queries_html(self) -> dict[str, Any]:
with DbConn() as conn:
quota_count = QuotaLog.current(conn)
queries_data = [
q for q in YoutubeQuery.get_all(conn)
if q.retrieved_at > self.server.config.queries_cutoff]
queries_data.sort(key=lambda q: q.retrieved_at, reverse=True)
- self._send_rendered_template(_NAME_TEMPLATE_YT_QUERIES,
- {'queries': queries_data,
- 'quota_count': quota_count,
- 'selected': 'yt_queries'})
+ return {'queries': queries_data,
+ 'quota_count': quota_count,
+ 'selected': 'yt_queries'}
- def _send_yt_query_page(self) -> None:
+ def _send_yt_query_html(self) -> dict[str, Any]:
query_id = QueryId(self.path_toks[2])
with DbConn() as conn:
- query = YoutubeQuery.get_one(conn, str(query_id))
- results = YoutubeVideo.get_all_for_query(conn, query_id)
- self._send_rendered_template(_NAME_TEMPLATE_YT_RESULTS,
- {'query': query.text, 'videos': results})
-
- def _send_playlist(self) -> None:
- self._send_rendered_template(_NAME_TEMPLATE_PLAYLIST,
- {'selected': 'playlist'})
+ return {'query': YoutubeQuery.get_one(conn, str(query_id)).text,
+ 'videos': YoutubeVideo.get_all_for_query(conn, query_id)}
def serve():
MILLION = 1000 * 1000
MEGA = 1024 * 1024
+CMD_UP = 'up'
+CMD_DOWN = 'down'
+CMD_RM = 'rm'
+
def ensure_expected_dirs(expected_dirs: list[Path]) -> None:
"""Ensure existance of expected_dirs _as_ directories."""
for attr_name in ('_downloaded', '_inherited'):
coll = getattr(self, attr_name)
if yt_id in coll:
- # NB: for some strange remove .remove on target
+ # NB: for some strange .remove on target
# collection will not satisfy assert below …
setattr(self, attr_name, [id_ for id_ in coll if id_ != yt_id])
assert yt_id not in self._downloaded + self._inherited
if command == 'download_next':
Thread(target=self._download_next, daemon=False).start()
continue
- if command == 'abort':
+ if command == CMD_RM:
yt_id = self._abort_downloading()
else:
command, arg = command.split('_', maxsplit=1)
if command == 'savefile':
yt_id = self._savefile(arg)
- elif command in {'unqueue', 'up', 'down'}:
+ elif command in {CMD_RM, CMD_UP, CMD_DOWN}:
idx = int(arg)
- if command == 'unqueue':
+ if command == CMD_RM:
yt_id = self._unqueue_download(idx)
else:
- self._move_in_queue(idx, upwards=command == 'up')
+ self._move_in_queue(idx, upwards=command == CMD_UP)
continue
else:
yt_id = arg
from ytplom.db import DbConn, DbFile, Hash, PATH_DB
from ytplom.misc import (PATH_TEMP, Config, FileDeletionRequest, FlagName,
QuotaLog, VideoFile, YoutubeQuery, YoutubeVideo)
-from ytplom.http import PAGE_NAMES
_PATH_DB_REMOTE = PATH_TEMP.joinpath('remote_db.sql')
def _urls_here_and_there(config: Config, page_name: str) -> tuple[str, ...]:
- return tuple(f'http://{host}:{port}/{PAGE_NAMES[page_name]}'
+ return tuple(f'http://{host}:{port}/{page_name}'
for host, port in ((config.remote, config.port_remote),
(config.host, config.port)))