+/*
+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();
+ }
+ );
+ }
+ }
+);
+