Anakunda / gazelleApiLib

// ==UserScript==
// ==UserLibrary==
// @name         gazelleApiLib
// @namespace    https://openuserjs.org/users/Anakunda
// @version      1.04
// @author       Anakunda
// @license      GPL-3.0-or-later
// @copyright    2021, Anakunda (https://openuserjs.org/users/Anakunda)
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @exclude      *
// @require      https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
// ==/UserScript==
// ==/UserLibrary==

'use strict';

const siteApiTimeframeStorageKey = 'AJAX time frame';
let gazelleApiFrameReserve = 100; // reserve that amount of ms for service operations
let gazelleApiMutex = createMutex(), ajaxApiKey, ajaxApiLogger;

if (typeof GM_getValue == 'function') switch (document.domain) {
	case 'redacted.ch': ajaxApiKey = GM_getValue('redacted_api_key'); break;
}
for (let key of ['ajaxApiKey', 'ajax_api_key', 'api_key'])
	if (!ajaxApiKey && key in window.localStorage && (ajaxApiKey = window.localStorage[key]))
		GM_setValue('redacted_api_key', ajaxApiKey);

function setAjaxApiLogger(callback) { ajaxApiLogger = typeof callback == 'function' ? callback : undefined }

function queryAjaxAPI(action, params, postData) {
	return action ? new Promise(function(resolve, reject) {
		let xhr = new XMLHttpRequest, retryCount = 0;
		params = new URLSearchParams(Object.assign({ action: action }, params || undefined));
		const url = '/ajax.php?' + params.toString();
		if (postData && !(postData instanceof URLSearchParams)) switch (typeof postData) {
			case 'object': postData = new URLSearchParams(postData); break;
			case 'string': try { postData = new URLSearchParams(JSON.parse(postData)) } catch(e) { } break;
		}
		postData = postData instanceof URLSearchParams ? postData.toString() : undefined;
		(function attempt() {
			gazelleApiMutex.lock(function() {
				let timeStamp = Date.now(), apiTimeFrame;
				if (siteApiTimeframeStorageKey in window.localStorage) try {
					apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]);
				} catch(e) { apiTimeFrame = { } } else apiTimeFrame = { };
				if (!apiTimeFrame.timeLock || timeStamp > apiTimeFrame.timeLock) {
					apiTimeFrame.timeLock = timeStamp + 10000 + gazelleApiFrameReserve;
					apiTimeFrame.requestCounter = 1;
				} else ++apiTimeFrame.requestCounter;
				window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
				gazelleApiMutex.unlock();
				if (apiTimeFrame.requestCounter <= 5) {
					xhr.open(postData ? 'POST' : 'GET', url, true);
					xhr.setRequestHeader('Accept', 'application/json');
					if (postData) xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
					if (ajaxApiKey) xhr.setRequestHeader('Authorization', ajaxApiKey);
					xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
					xhr.responseType = 'json';
					xhr.onload = function() {
						if (xhr.status == 404) return reject('not found');
						if (xhr.status < 200 || xhr.status >= 400) return reject(defaultErrorHandler(xhr));
						if (xhr.response.status == 'success') return resolve(xhr.response.response);
						if (xhr.response.error == 'rate limit exceeded') {
							console.debug(xhr.response.error + ':', 'action=' + action, apiTimeFrame, timeStamp, retryCount);
							if (retryCount++ <= 10) return setTimeout(attempt, apiTimeFrame.timeLock - timeStamp);
						} else console.warn('queryAjaxAPI.attempt(…):', xhr.response.status, xhr.response.error);
						reject(xhr.response.status + ': ' + xhr.response.error);
					};
					xhr.onerror = () => { reject(defaultErrorHandler(xhr)) };
					xhr.ontimeout = () => { reject(defaultTimeoutHandler(xhr)) };
					xhr.timeout = 20000;
					xhr.send(postData);
				} else {
					setTimeout(attempt, apiTimeFrame.timeLock - timeStamp);
					if (typeof ajaxApiLogger == 'function') ajaxApiLogger(action, apiTimeFrame, timeStamp);
					console.debug('AJAX API request quota exceeded: action=' + action, apiTimeFrame, timeStamp, retryCount);
				}
			});
		})();
	}) : Promise.reject('Action missing');
}

const ajaxGetArtist = artistName => queryAjaxAPI('artist', { artistname: artistName });
const ajaxGetRequest = id => queryAjaxAPI('request', { id: id });

function defaultErrorHandler(response) {
	console.error('HTTP error:', response);
	let e = 'HTTP error ' + response.status;
	if (response.statusText) e += ' (' + response.statusText + ')';
	if (response.error) e += ' (' + response.error + ')';
	return e;
}

function defaultTimeoutHandler(response) {
	console.error('HTTP timeout:', response);
	const e = 'HTTP timeout';
	return e;
}