mirror of
https://github.com/iv-org/invidious.git
synced 2025-06-28 01:28:30 +00:00
274 lines
8.2 KiB
JavaScript
274 lines
8.2 KiB
JavaScript
"use strict";
|
|
// Contains only auxiliary methods
|
|
// May be included and executed unlimited number of times without any consequences
|
|
|
|
// Polyfills for IE11
|
|
Array.prototype.find =
|
|
Array.prototype.find ||
|
|
function (condition) {
|
|
return this.filter(condition)[0];
|
|
};
|
|
|
|
Array.from =
|
|
Array.from ||
|
|
function (source) {
|
|
return Array.prototype.slice.call(source);
|
|
};
|
|
NodeList.prototype.forEach =
|
|
NodeList.prototype.forEach ||
|
|
function (callback) {
|
|
Array.from(this).forEach(callback);
|
|
};
|
|
String.prototype.includes =
|
|
String.prototype.includes ||
|
|
function (searchString) {
|
|
return this.indexOf(searchString) >= 0;
|
|
};
|
|
String.prototype.startsWith =
|
|
String.prototype.startsWith ||
|
|
function (prefix) {
|
|
return this.substr(0, prefix.length) === prefix;
|
|
};
|
|
Math.sign =
|
|
Math.sign ||
|
|
function (x) {
|
|
x = +x;
|
|
if (!x) return x; // 0 and NaN
|
|
return x > 0 ? 1 : -1;
|
|
};
|
|
if (
|
|
!window.hasOwnProperty("HTMLDetailsElement") &&
|
|
!window.hasOwnProperty("mockHTMLDetailsElement")
|
|
) {
|
|
window.mockHTMLDetailsElement = true;
|
|
const style = "details:not([open]) > :not(summary) {display: none}";
|
|
document.head.appendChild(document.createElement("style")).textContent =
|
|
style;
|
|
|
|
addEventListener("click", function (e) {
|
|
if (e.target.nodeName !== "SUMMARY") return;
|
|
const details = e.target.parentElement;
|
|
if (details.hasAttribute("open")) details.removeAttribute("open");
|
|
else details.setAttribute("open", "");
|
|
});
|
|
}
|
|
|
|
// Monstrous global variable for handy code
|
|
// Includes: clamp, xhr, storage.{get,set,remove}
|
|
window.helpers = window.helpers || {
|
|
/**
|
|
* https://en.wikipedia.org/wiki/Clamping_(graphics)
|
|
* @param {Number} num Source number
|
|
* @param {Number} min Low border
|
|
* @param {Number} max High border
|
|
* @returns {Number} Clamped value
|
|
*/
|
|
clamp: function (num, min, max) {
|
|
if (max < min) {
|
|
var t = max;
|
|
max = min;
|
|
min = t; // swap max and min
|
|
}
|
|
|
|
if (max < num) return max;
|
|
if (min > num) return min;
|
|
return num;
|
|
},
|
|
|
|
/** @private */
|
|
_xhr: function (method, url, options, callbacks) {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open(method, url);
|
|
|
|
// Default options
|
|
xhr.responseType = "json";
|
|
xhr.timeout = 10000;
|
|
// Default options redefining
|
|
if (options.responseType) xhr.responseType = options.responseType;
|
|
if (options.timeout) xhr.timeout = options.timeout;
|
|
|
|
if (method === "POST")
|
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
|
|
|
// better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963
|
|
xhr.onloadend = function () {
|
|
if (xhr.status === 200) {
|
|
if (callbacks.on200) {
|
|
// fix for IE11. It doesn't convert response to JSON
|
|
if (xhr.responseType === "" && typeof xhr.response === "string")
|
|
callbacks.on200(JSON.parse(xhr.response));
|
|
else callbacks.on200(xhr.response);
|
|
}
|
|
} else {
|
|
// handled by onerror
|
|
if (xhr.status === 0) return;
|
|
|
|
if (callbacks.onNon200) callbacks.onNon200(xhr);
|
|
}
|
|
};
|
|
|
|
xhr.ontimeout = function () {
|
|
if (callbacks.onTimeout) callbacks.onTimeout(xhr);
|
|
};
|
|
|
|
xhr.onerror = function () {
|
|
if (callbacks.onError) callbacks.onError(xhr);
|
|
};
|
|
|
|
if (options.payload) xhr.send(options.payload);
|
|
else xhr.send();
|
|
},
|
|
/** @private */
|
|
_xhrRetry: function (method, url, options, callbacks) {
|
|
if (options.retries <= 0) {
|
|
console.warn("Failed to pull", options.entity_name);
|
|
if (callbacks.onTotalFail) callbacks.onTotalFail();
|
|
return;
|
|
}
|
|
helpers._xhr(method, url, options, callbacks);
|
|
},
|
|
/**
|
|
* @callback callbackXhrOn200
|
|
* @param {Object} response - xhr.response
|
|
*/
|
|
/**
|
|
* @callback callbackXhrError
|
|
* @param {XMLHttpRequest} xhr
|
|
*/
|
|
/**
|
|
* @param {'GET'|'POST'} method - 'GET' or 'POST'
|
|
* @param {String} url - URL to send request to
|
|
* @param {Object} options - other XHR options
|
|
* @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests
|
|
* @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json]
|
|
* @param {Number} [options.timeout=10000]
|
|
* @param {Number} [options.retries=1]
|
|
* @param {String} [options.entity_name='unknown'] - string to log
|
|
* @param {Number} [options.retry_timeout=1000]
|
|
* @param {Object} callbacks - functions to execute on events fired
|
|
* @param {callbackXhrOn200} [callbacks.on200]
|
|
* @param {callbackXhrError} [callbacks.onNon200]
|
|
* @param {callbackXhrError} [callbacks.onTimeout]
|
|
* @param {callbackXhrError} [callbacks.onError]
|
|
* @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries
|
|
*/
|
|
xhr: function (method, url, options, callbacks) {
|
|
if (!options.retries || options.retries <= 1) {
|
|
helpers._xhr(method, url, options, callbacks);
|
|
return;
|
|
}
|
|
|
|
if (!options.entity_name) options.entity_name = "unknown";
|
|
if (!options.retry_timeout) options.retry_timeout = 1000;
|
|
const retries_total = options.retries;
|
|
let currentTry = 1;
|
|
|
|
const retry = function () {
|
|
console.warn(
|
|
"Pulling " +
|
|
options.entity_name +
|
|
" failed... " +
|
|
currentTry++ +
|
|
"/" +
|
|
retries_total,
|
|
);
|
|
setTimeout(function () {
|
|
options.retries--;
|
|
helpers._xhrRetry(method, url, options, callbacks);
|
|
}, options.retry_timeout);
|
|
};
|
|
|
|
// Pack retry() call into error handlers
|
|
callbacks._onError = callbacks.onError;
|
|
callbacks.onError = function (xhr) {
|
|
if (callbacks._onError) callbacks._onError(xhr);
|
|
retry();
|
|
};
|
|
callbacks._onTimeout = callbacks.onTimeout;
|
|
callbacks.onTimeout = function (xhr) {
|
|
if (callbacks._onTimeout) callbacks._onTimeout(xhr);
|
|
retry();
|
|
};
|
|
|
|
helpers._xhrRetry(method, url, options, callbacks);
|
|
},
|
|
|
|
/**
|
|
* @typedef {Object} invidiousStorage
|
|
* @property {(key:String) => Object} get
|
|
* @property {(key:String, value:Object)} set
|
|
* @property {(key:String)} remove
|
|
*/
|
|
|
|
/**
|
|
* Universal storage, stores and returns JS objects. Uses inside localStorage or cookies
|
|
* @type {invidiousStorage}
|
|
*/
|
|
storage: (function () {
|
|
// access to localStorage throws exception in Tor Browser, so try is needed
|
|
let localStorageIsUsable = false;
|
|
try {
|
|
localStorageIsUsable = !!localStorage.setItem;
|
|
} catch (e) {}
|
|
|
|
if (localStorageIsUsable) {
|
|
return {
|
|
get: function (key) {
|
|
let storageItem = localStorage.getItem(key);
|
|
if (!storageItem) return;
|
|
try {
|
|
return JSON.parse(decodeURIComponent(storageItem));
|
|
} catch (e) {
|
|
// Erase non parsable value
|
|
helpers.storage.remove(key);
|
|
}
|
|
},
|
|
set: function (key, value) {
|
|
let encoded_value = encodeURIComponent(JSON.stringify(value));
|
|
localStorage.setItem(key, encoded_value);
|
|
},
|
|
remove: function (key) {
|
|
localStorage.removeItem(key);
|
|
},
|
|
};
|
|
}
|
|
|
|
// TODO: fire 'storage' event for cookies
|
|
console.info(
|
|
"Storage: localStorage is disabled or unaccessible. Cookies used as fallback",
|
|
);
|
|
return {
|
|
get: function (key) {
|
|
const cookiePrefix = key + "=";
|
|
function findCallback(cookie) {
|
|
return cookie.startsWith(cookiePrefix);
|
|
}
|
|
const matchedCookie = document.cookie.split("; ").find(findCallback);
|
|
if (matchedCookie) {
|
|
const cookieBody = matchedCookie.replace(cookiePrefix, "");
|
|
if (cookieBody.length === 0) return;
|
|
try {
|
|
return JSON.parse(decodeURIComponent(cookieBody));
|
|
} catch (e) {
|
|
// Erase non parsable value
|
|
helpers.storage.remove(key);
|
|
}
|
|
}
|
|
},
|
|
set: function (key, value) {
|
|
const cookie_data = encodeURIComponent(JSON.stringify(value));
|
|
|
|
// Set expiration in 2 year
|
|
const date = new Date();
|
|
date.setFullYear(date.getFullYear() + 2);
|
|
|
|
document.cookie =
|
|
key + "=" + cookie_data + "; expires=" + date.toGMTString();
|
|
},
|
|
remove: function (key) {
|
|
document.cookie = key + "=; Max-Age=0";
|
|
},
|
|
};
|
|
})(),
|
|
};
|