Anakunda / Image Host Helper

// ==UserScript==
// @name         Image Host Helper
// @namespace    https://greasyfork.org/users/321857-anakunda
// @version      1.90.5
// @description  Directly upload local / rehost remote images or galleries to whatever supported image host by dropping/pasting them to target field
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsSAAALEgHS3X78AAAHTklEQVR4nO1aCUwUVxj+38zuyuwuRGGByHIpqEXFo1hj1SZiW1MarVqPREgkattUadPUI4iNpGrVxph6VJR4Rm3UlhrsZaBnqtXEo1qqxqJFZGFBTmHZe2fm9c3CrhzLsbtDRtP9wsLO9b/vffPPfzxGhjGG/zNkUhOQGgEBpCYgNQICSE1AagQEkJqA1AgIIDUBqREQQGoCUiMggNQEpEZAADGNFetNk77Qc5mXHuPUR3aItfFISZpt2nWcAmBVNG4exkBpWjj1TbqWPp0cpqwSk4O3EEWAa3WmEbml/ObiRn4B5pEcKATQ9tMJPBnPwCFNSStoSlrwtLwKx9rl0a2f5yTKPosMZsxicPEWfguQ/69xQfZdbr/BgSKARoDo3s9Hrl/kvFYORewpx1t+bbSnHR6Hl0+OUJb6y8db+CXAJ3eM7+Te4/dhhOR9TdwTULsQt1rR1DnXHMVnnjfNSR2quuUPJ2/hswD5940Lcu9xeWTyMtTV170EIsGhzo7iMm6yhUVy88xxGqWu3miRsRgYOQKLRs2w/o3QM3wS4Hq9OTH7Hy5fjMm7IIhQY0MJ8/5kz2sUhoZHNhzmwChkEIUNEQpHw1g1XH9ZQ/+UqqEuRwUzRnFG9VGAjaXcJvLMa3xx+94giFBugTHlQjh0K4uggmxfewwzjlVxa2OCuNJlMez+VfGyw5Fq/wOn1wKQVJfyYwO/ENEi3foucM67q+kO25U2GLX5Hr+noMa+ZM8YPuvVKNUNf8bzWoCTem4pzyOF2He/v2gLnAjuGmHKvOvsL/vHGpdkDlcX+WrPKwHqjBblhSZIEyoaqSE8LmYeDX73FvdliMw4Z36s+oIvdrwSoMyE46ptEN/NRX2E638yvgZS4Torj0Ky7nBHk0PMUxMHK+u8teGVADorH83xWI4o/xXApCwcFYK4RjumG2x+iCBkDwtK2HafXXf0BVjn7fVeCdDKIgVgcSYfzSAoelFF/9XMwaKrZmCJXZ9TKolHBY/winVNlp1JoUytN5f2W4Amk4WpsfIaf91fcHs1IXwyhYF4JeX8bB49CDbctndom7yDQMnIoiF5Dx0rFtu5H9Q01RwdhB5FBDO2vq7tU4CzOuPM03o+/UoLfolUa1rkRwAUHnmKKLBnPAMzwp8MvX5kEIkvPH+k3EH5ml4pwiuvAm/N03FbFBRnilRA1ZTBjksZWvrE3BjVxZ6u61GAK6TDI03Ozt+b8BtOt++hw/MKHIbsUUGwPE7Rabdgc3cyQz0wYfitjuuzofIEQVwhNpG/lB1DcKUVkiqrIamghl32ykPD2U+TZBtSNMqyrtd5FODUQ+OsrFvcsWYWRQkTFyPoY1LYL46Vwxbi7p6gliHnY/HyH2YoNfLgq6ehjl9oQRhE/1wPi2e1OKYdTDalL4hTdUqX3QQ4pzNNX1HCnbFiNMQfd+8IzAFM1sjgAHH93jxcy1BwYhIDr182QaPD98zQEU4TRIgmFmkzS9gClcw0+zWt6prreCcBHraYQ9+/wx4huVW8yZOIr1W23d1QRd8zmjyEhvwJDGRctwBxZVFEECDYMXEoYtVt9vDFYMt0bQjTKuzvJMDeci6rygIjxSpzhYg/hPSzZ8hdHanuv6ILtXKotmFY87cVOHGoOOFstkxo3P4KdsXWZNgt7HMLUNJgjiqsxekgQpHjHpB8lHLAByocaG+53bm9bXQQJKg8i3Gu2gGnKh1OojwRT05uBMeLRqcNxPZXpGboJsAhHbtMb4PhYrmcE8SW3oLRqQqHe9fqBEwE8Hz6zRYOCnRCPUC1qUf5mXV64KSzQKJr0y1AYS2/iMWUwvNVfoznjsbtc+plRvL2Tm8gO01hCAcGdypyC1BtpcaLFfieAbhvg1sAFY11ZoxipeEjHdwCpIbB+fMN8Db2uSJ/dkAeQ/ciq1uAt2LoY5eb+flNLEQOzGLX0wEhNYcpoMa17RZgbqz66nS9oejbGsgcSB+Q9xJnBmiZsTNIWp0Zir53bXYqhD6Mp3cW1bKLSMZWDgQXweYdAw8mEoYxPHlFV/g2iPhljRUPQN57AuHuMzJs+CBetsu1r5MAM4aqbq9JMHy0/T7ehWlxmiAXBFvCRDNvWEhT2P395LYUiUCM1SZPcI5Iqqv1I6jsKZHK+6793ZqhbckhuxvshtBDFXgjpvxYpekBztLWg1Hcfmwgpu/Um7j+e8OoTbljgvM7HvPYDh9MCckdxrRWby/jP251oEhnRSZWUyKOmX7BNfHBClyzMZHKWf1c8PGu5/S4IJIzOjg/LcJ8Pr+CXVlcj+dW29Gw9grqWUgSmFSVVm0QfpAWjgpXxskPjg1TVno6sdclsQkapS5fAzk36037jlfxGd/Vsm9WWXACjyjNwPD2HxTmq+IZKJs9lC5cqpV9PV6j0vd2fr8WRSeGq/QTw2EHaZ92iEPz6UHgHSGpCUiNgABSE5AaAQGkJiA1AgJITUBqBASQmoDUCAggNQGpERBAagJS4z/F0X/2U+WfagAAAABJRU5ErkJggg==
// @author       Anakunda
// @copyright    2020-21, Anakunda (https://greasyfork.org/users/321857-anakunda)
// @license      GPL-3.0-or-later
// @match        https://passthepopcorn.me/*
// @match        https://redacted.ch/*
// @match        https://orpheus.network/*
// @match        https://broadcasthe.net/*
// @match        https://notwhat.cd/*
// @match        https://dicmusic.club/*
// @match        https://*/torrents.php?id=*
// @match        https://*/artist.php?id=*
// @match        https://*/artist.php?action=edit&artistid=*
// @match        https://*/reportsv2.php?action=report&id=*
// @match        https://*/forums.php?action=new*
// @match        https://*/forums.php?*action=viewthread*
// @match        https://*/requests.php?action=view*
// @match        https://*/collages.php?id=*
// @match        https://*/collages.php?action=edit&collageid=*
// @match        https://*/collages.php?action=comments&collageid=*
// @match        https://*/collages.php?action=new
// @match        http*://tracker.czech-server.com/*
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_info
// @require      https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js
// @require      https://openuserjs.org/src/libs/Anakunda/progressBars.min.js
// @require      https://openuserjs.org/src/libs/Anakunda/imageHostUploader.min.js
// ==/UserScript==

'use strict';

if (document.getElementById('upload-assistant') != null) return; // don't clash with Upload Assistant

const amEntityParser = /^(?:https?):\/\/(?:[\w\%\-]+\.)*apple\.com\/(?:\S+\/)?(album|artist|playlist)\/(?:[\w\%\-]+\/)?(?:id)?(\d+)\b/i;
const itunesImageMax = [/\/(\d+x\d+)\w*\.(\w+)$/, '/100000x100000-999.' +
	(GM_getValue('apple_get_png_cover', false) ? 'png' : '$2')];
const dzrEntityParser = /^(?:https?):\/\/(?:[\w\%\-]+\.)*deezer\.com\/(?:\S+\/)?(album|artist|track|comment|playlist|radio|user)\/(\d+)\b/i;
const dzrImageMax = GM_getValue('deezer_get_png_cover', false) ? [/\/(\d+x\d+)(?:\-\d+)*\.\w+$/, '/1400x1400.png']
	: [/\/(\d+x\d+)(?:\-\d+)*(?=\.\w+$)/,
			'/1400x1400-000000-' + (parseInt(GM_getValue('deezer_jpeg_quality')) || 100) + '-0-0'];
const discogsOrigin = 'https://www.discogs.com';
const discogsKey = GM_getValue('discogsKey', 'LWiNvIWBobGMRhfSCAiC');
const discogsSecret = GM_getValue('discogsSecret', 'HAQUKFmebpCSLyRNwjmSgOMgbnxsVQcp');
const lfmApiKey = GM_getValue('lfmApiKey', '920db0d2f86108f2fbe1917b53d63858');
const tidalClientId = GM_getValue('tidalClientId', 'PL-KYllTy1qPbCAk');

PTPimg.prototype.setSession = function() {
	return this.apiKey ? Promise.resolve(this.apiKey) : globalXHR(this.origin).then(({document}) => {
		var apiKey = document.getElementById('api_key');
		if (apiKey == null) {
			let counter = GM_getValue('ptpimg_reminder_read', 0);
			if (counter < 3) {
				alert(`
${this.alias} API key could not be captured. Please login to ${this.origin}/ and redo the action.
If you don\'t have PTPimg account or don\'t want to use it, consider to remove PTPimg from
'upload_hosts' and 'rehost_hosts' storage entries, and all sites' local hostlists where does it appear.
`);
				GM_setValue('ptpimg_reminder_read', ++counter);
			}
			return Promise.reject('API key not configured');
		} else if (!(this.apiKey = apiKey.value)) return Promise.reject('assertion failed: empty PTPimg API key');
		GM_setValue('ptpimg_api_key', this.apiKey);
		Promise.resolve(this.apiKey)
			.then(apiKey => { alert(`Your PTPimg API key [${apiKey}] was successfully configured`) });
		return this.apiKey;
	});
}

function getDiscogsImageMax(imageUrl) {
	if (!httpParser.test(imageUrl)) return Promise.reject('Image URL is not valid');
	if (imageUrl.endsWith('/images/spacer.gif')) return Promise.reject('Dummy image (placeholder)');
	const matches = [
		/^(?:https?):\/\/(?:(?:img|i)\.discogs\.com)\/.+\/([\w\%\-]+\.\w+)\b(?:\.\w+)*$/i,
	].map(rx => rx.exec(imageUrl));
	if (matches[0] != null) return verifyImageUrl(discogsOrigin + '/image/' + matches[0][1]).catch(reason => imageUrl);
	return Promise.resolve(imageUrl);
}

let cheveretoCustomHosts = GM_getValue('chevereto_custom_hosts');
if (cheveretoCustomHosts) try {
	JSON.parse(cheveretoCustomHosts).forEach(function(siteDef) {
		if (!siteDef.host_name || !siteDef.alias) {
			console.warn('Incomplete Chevereto custom site definition:', siteDef);
			return;
		}
		imageHostHandlers[siteDef.alias.replace(nonWordStripper, '').toLowerCase()] = new Chevereto(
			siteDef.host_name,
			siteDef.alias,
			siteDef.types,
			siteDef.size_limit, {
				sizeLimitAnonymous: siteDef.size_limit_anonymous,
				configPrefix: siteDef.config_prefix,
				apiEndpoint: siteDef.api_endpoint,
				apiFieldName: siteDef.api_field_name,
				apiResultKey: siteDef.api_result_key,
				jsonEndpoint: siteDef.json_endpoint,
			});
	});
} catch (e) {
	GM_setValue('chevereto_custom_hosts', '[]');
	console.warn(e);
}
console.log('Image host handlers:', imageHostHandlers);

const generalImgHosts = [
	'ImgBB', 'PixHost', 'ImgBox', 'FunkyIMG', 'Slowpoke', 'PostImage', 'Jerking', 'Gifyu',
	'Ra', 'Abload', 'VgyMe', 'GeekPic', 'LightShot', 'ImgURL', 'Radikal', 'Z4A', 'PicaBox', 'PimpAndHost', 'SMMS',
	'CasImages', 'CubeUpload', 'GooPics', 'ImageBan', 'UuploadIr',
	'Imgur', 'Catbox', 'ImageVenue', 'GetaPic', 'FastPic', 'SVGshare',
];
['upload_hosts', 'rehost_hosts'].forEach(propName => { if (!GM_getValue(propName))
	GM_setValue(propName, ['PTPimg'].concat(generalImgHosts).join(', ')) });
[
	['passthepopcorn.me', [
		'PTPimg', 'ImgBB', 'PixHost', 'ImgBox', 'Slowpoke', 'FunkyIMG', 'Jerking', 'Gifyu',
		'Ra', 'Abload', 'VgyMe', 'GeekPic', 'LightShot', 'ImgURL', 'Radikal', 'Z4A', 'PicaBox', 'PimpAndHost', 'SMMS',
		'CasImages', 'CubeUpload', 'GooPics', 'ImageBan', 'UuploadIr',
		'Catbox', 'ImageVenue', 'GetaPic',
	]],
	['notwhat.cd', ['NWCD']],
	['forum.mobilism.org', ['Mobilism'].concat(generalImgHosts)],
	['forum.mobilism.me', ['Mobilism'].concat(generalImgHosts)],
].forEach(hostDefaults => { if (!GM_getValue(hostDefaults[0])) GM_setValue(hostDefaults[0], hostDefaults[1].join(', ')) });

var imageHosts = new ImageHostManager(logFail,
	GM_getValue(document.domain) || GM_getValue('upload_hosts'),
	GM_getValue(document.domain) || GM_getValue('rehost_hosts'));

imageHostUploaderInit(inputDataHandler, textAreaDropHandler, textAreaPasteHandler, imageUrlResolver);

// Set single input UI handlers
let imageInputMatch = GM_getValue('image_input_match', '/(?:image|img|picture|cover|photo|avatar|poster|screen)/i');
if ((imageInputMatch = /^\/(.+)\/([dgimsuy]*)$/.exec(imageInputMatch)) != null) try {
	imageInputMatch = new RegExp(imageInputMatch[1], imageInputMatch[2]);
	for (let input of document.body.querySelectorAll('input[type="text"]'))
		if (['id', 'name'].some(attribute => imageInputMatch.test(input[attribute] || input.getAttribute(attribute))))
			setInputHandlers(input);
} catch(e) { console.warn('Image Host Helper: failed to compile image input matcher', e, imageInputMatch) }
	else console.warn('Image Host Helper: custom text inputs match expression not in proper regexp format; no text inputs will be handled');
// Set multiple inputs UI handlers
for (let textArea of document.body.getElementsByTagName('textarea'))
	if (!['ua-data'].some(id => textArea.id == id)
			&& ![ ].some(id => textArea.id == id)) setTextAreahandlers(textArea);

// site-specific extensions
switch (document.domain) {
	case 'passthepopcorn.me':
		// Auto-fill missing/invalid images from IMDB
		if (document.location.pathname == '/artist.php' && /^\?action=edit&artistid=(\d+)\b/i.test(document.location.search)
				&& GM_getValue('auto_lookup_artist_image', true)) {
			let artistId = parseInt(RegExp.$1), input = document.querySelector('input[name="image"]');
			if (input != null) verifyImageUrl(input.value).catch(function(reason) {
				if (input.value) input.value = '';
				localXHR('/artist.php?id=' + artistId).then(function(dom) {
					let imdb = dom.querySelector('div#artistinfo > div.panel__body > ul.list > li > a');
					if (imdb != null) imageUrlResolver(imdb.href)
						.then(setCover.bind(input), reason => { logFail('No IMDB photo of this artist') });
				});
			});
		} else if (document.location.pathname == '/torrents.php'
				&& /^\?action=editgroup&groupid=(\d+)\b/i.test(document.location.search)
				&& GM_getValue('auto_lookup_artist_image', true)) {
			let groupId = parseInt(RegExp.$1), input = document.querySelector('input[name="image"]');
			if (input != null) verifyImageUrl(input.value).catch(function(reason) {
				if (input.value) input.value = '';
				localXHR('/torrents.php?id=' + groupId).then(function(dom) {
					let imdb = dom.querySelector('a#imdb-title-link');
					if (imdb != null) imageUrlResolver(imdb.href)
						.then(setCover.bind(input), reason => { logFail('No IMDB poster for this movie') });
				});
			});
		}
		// hook to HJ Member Toolkit
		new MutationObserver(function(mutationsList, mo) {
			for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
				if (node.nodeName != 'DIV' || !node.classList.contains('HJ-toolkit-member-toolbar')) return;
				mo.disconnect();
				new MutationObserver(function(mutationsList, mo) {
					for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
						if (node.nodeName != 'DIV' || !node.classList.contains('HJ-toolkit-member-toolbar-flex')) return;
						mo.disconnect();
						node.querySelectorAll([
							//'textarea[id^="HJMA"]',
							'textarea[name="screenshots"]',
							'textarea[name="comparisons"]',
						].join(',')).forEach(setTextAreahandlers);
					});
				}).observe(node, { childList: true, subtree: true });
			});
		}).observe(document.body, { childList: true });
		break;
	case 'redacted.ch':
	case 'orpheus.network':
	case 'notwhat.cd':
	case 'dicmusic.club':
		if (document.location.pathname == '/upload.php')
			document.querySelectorAll('input[type="text"][name="verification"]').forEach(setInputHandlers);
		// Auto-fill missing/invalid artist images
		else if (document.location.pathname == '/artist.php' && document.location.search.startsWith('?action=edit&')
				&& GM_getValue('auto_lookup_artist_image', true)) {
			let input = document.querySelector('input[name="image"]');
			if (input != null) verifyImageUrl(input.value).catch(function() {
				if (input.value.length > 0) input.value = '';
				let artist = document.querySelector('div.header > h2 > a');
				if (artist != null) artist = artist.textContent.trim(); else throw 'Artist name not found';

				function resultsFilter(results0, nameExtractor) {
					const tailingBracketStripper = [/\s*\(([^\(\)]+)\)\s*$/, ''],
								norm = artist => artist && artist.replace(/\s+/g, '').toLowerCase();
					const transforms = [n => n && n.replace(...tailingBracketStripper),
						n => n && (n = tailingBracketStripper[0].exec(n)) && n[1]];
					let results = results0.filter(function(result) {
						let n = [artist, nameExtractor(result)].map(n => transforms.map(func => func(n)));
						for (let i = 0; i < 2; ++i) if (n[0][i]) for (let j = 0; j < 2; ++j)
							if (n[1][j] && norm(n[0][i]) == norm(n[1][j])) return true;
						return norm(n[0][0].toASCII()) == norm(n[1][0].toASCII());
					}), f;
					if (results.length > 1) {
						f = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toASCII().toLowerCase()
							== artist.replace(...tailingBracketStripper).toASCII().toLowerCase());
						if (f.length > 0) results = f;
					}
					if (results.length > 1) {
						f = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toLowerCase()
							== artist.replace(...tailingBracketStripper).toLowerCase());
						if (f.length > 0) results = f;
					}
					return results;
				}

				let lookupWorkers = [
					// Qobuz
					globalXHR('https://www.qobuz.com/shop', { responseType: 'text' }).then(function(response) {
						const rx = /^\s*(?:(?:window\.)?qobuz\.algolia(\d+))\s*=\s*(\{.*\});/gm;
						let result = [ ], m;
						while ((m = rx.exec(response.responseText)) != null) {
							let obj = JSON.parse(m[2]);
							if (obj.api_key && obj.application_id) result[parseInt(m[1]) - 1] = obj;
						}
						return result[0] && result[1] ? result : Promise.reject('unexpected page structure');
					}).then(algolia => globalXHR('https://' + algolia[1].application_id.toLowerCase() + '-1.algolianet.com/1/indexes/' + algolia[1].index.main_artists + '/query?' + new URLSearchParams({
						'x-algolia-application-id': algolia[1].application_id,
						'x-algolia-api-key': algolia[1].api_key,
					}).toString(), { responseType: 'json' }, { 'params': 'query=' + encodeURIComponent(artist) })).then(function({response}) {
						if (response.nbHits <= 0) return Promise.reject('Qobuz: no matches');
						let results = resultsFilter(response.hits, result => result.name);
						if (results.length <= 0) return Promise.reject('Qobuz: no matches');
						//console.log('Qobuz search results for "' + artist + '":', results);
						if (results.length > 1) return Promise.reject('Qobuz: ambiguity');
						if (results.length > 1) console.info('Qobuz returns ambiguous results for "' + artist + '":', results);
						return httpParser.test(results[0].image) ? results[0].image.replace(/(\/artists\/covers)\/\w+\//i, '$1/large/')
							: Promise.reject('Qobuz: artist exists but no photo');
					}),
					// AllMusic
					globalXHR('https://www.allmusic.com/search/artists/' + encodeURIComponent(artist)).then(function({document}) {
						let results = resultsFilter(Array.from(document.querySelectorAll('ul.search-results > li.artist')).map(function(li) {
							let result = {
								name: li.querySelector('div.name > a'),
								genres: li.querySelector('div.genres'),
								decades: li.querySelector('div.decades'),
							};
							Object.keys(result).forEach(key => {
								result[key] = result[key] != null ? result[key].textContent.trim() || undefined : undefined;
							});
							if (result.genres) result.genres = result.genres.split(/\s*,\s*/);
							result.url = li.querySelector('div.name > a');
							result.url = result.url != null ? result.url.href : undefined;
							if (/-(mw\d+)$/i.test(result.url)) result.id = RegExp.$1;
							result.image = li.querySelector('div.photo img');
							result.image = result.image != null ? result.image.src : undefined;
							return result;
						}), result => result.name);
						if (results.length <= 0) return Promise.reject('AllMusic: no matches');
						//console.log('AllMusic search results for "' + artist + '":', results);
						if (results.length > 1) return Promise.reject('AllMusic: ambiguity');
						if (results.length > 1) console.info('Qobuz returns ambiguous results for "' + artist + '":', results);
						if (!httpParser.test(results[0].image)) return Promise.reject('AllMusic: artist exists but no photo');
						return verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=6'))
							.catch(reason => verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=0')))
							.catch(reason => verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=5')));
					}),
					// NetEase
					globalXHR('https://music.163.com/api/cloudsearch/get/web?' + new URLSearchParams({
						s: '"' + artist + '"',
						type: 100,
						limit: 25,
						//csrf_token: '',
					}).toString(), { responseType: 'json' }).then(function({response}) {
						if (response.code != 200 || !response.result)
							return Promise.reject('API returns malformed result (' + response.msg + ')');
						return !response.abroad ? response.result : new Promise(function(resolve, reject) {
							function onCoreLoaded(elem = coreJS) {
								if ([/*'asrsea', */'settmusic'].every(function(pubSym) {
									try { return typeof eval(pubSym) == 'function' } catch(e) { return false }
								})) resolve(elem); else reject('core.js public functions not available');
							}
							function injectScript(src, errorHandler = reject) {
								if (!httpParser.test(src)) throw 'Assertion failed: invalid src';
								coreJS = document.createElement('script');
								coreJS.id = 'netease.core.js';
								coreJS.type = 'text/javascript';
								coreJS.async = false;
								coreJS.onload = evt => { onCoreLoaded(evt.target) };
								coreJS.onerror = function(evt) {
									document.head.removeChild(evt.target);
									console.error('Netease core.js (' + src + ') loading error', evt);
									if (typeof errorHandler == 'function') errorHandler(evt);
								};
								coreJS.src = src;
								document.head.append(coreJS);
							}

							var coreJS = document.getElementById('netease.core.js');
							if (coreJS != null) return onCoreLoaded();
							injectScript('https://s3.music.126.net/web/s/core.js', function(evt) {
								console.warn('NetEase generic core.js load failed, trying to fetch proper url from root doc');
								globalXHR('https://music.163.com/').then(function({document}) {
									var script = document.querySelector(':root > body > script[src*="/core"]');
									if (script != null) injectScript(script.src, evt => { reject('NetEase core.js loading error') });
										else reject('invalid root document structure');
								}, reject);
							});
						}).then(core => JSON.parse(decodeURIComponent(settmusic(response.result, 'fuck~#$%^&*(458'))));
					}).then(result => result.artistCount > 0 ? result.artists : Promise.reject('NetEase: no matches'), function(reason) {
						console.warn('NetEase search-list method failed:', reason);
						return globalXHR('https://music.163.com/api/search/suggest/web?'+ new URLSearchParams({
							s: '"' + artist + '"',
							type: 100,
							limit: 25,
							//csrf_token: '',
						}, { responseType: 'json' })).then(function({response}) {
							if (response.code != 200 || !response.result)
								return Promise.reject('API returns malformed result (' + response.msg + ')');
							return Array.isArray(response.result.artists) && response.result.artists.length > 0 ?
								response.result.artists : Promise.reject('NetEase: no matches');
						});
					}).then(function(artists) {
						console.assert(Array.isArray(artists) && artists.length > 0, "Array.isArray(artists) && artists.length > 0");
						if (!Array.isArray(artists) || artists.length <= 0) return Promise.reject('NetEase: no matches');
						let results = resultsFilter(artists/*.filter(artist => artist.picId > 0)*/, result => result.name);
						if (results.length <= 0) return Promise.reject('NetEase: no matches');
						console.log('NetEase search results for "' + artist + '":', artists);
						if (results.length > 1) return Promise.reject('NetEase: ambiguity');
						//if (results.length > 1) console.info('NetEase returns ambiguous results for "' + artist + '":', artists);
						const imgMax = imgUrl => imgUrl.replace(/\?.*$/, '').replace(/\b(?:p[123])(?=\.music\.\d+\.net\b)/i, 'p4');
						const isDummy = imgUrl => ['/5639395138885805.jpg'].some(dummy => imgUrl.toLowerCase().endsWith(dummy));
						if (artists[0].picId > 0 && httpParser.test(artists[0].picUrl) && !isDummy(artists[0].picUrl))
							return imgMax(artists[0].picUrl);
						if (artists[0].img1v1 > 0 && httpParser.test(artists[0].img1v1Url) && !isDummy(artists[0].img1v1Url))
							return imgMax(artists[0].img1v1Url);
						return Promise.reject('NetEase: artist exists but no photo');
					}),
					// Tidal
					globalXHR('https://listen.tidal.com/v1/search/artists?' + new URLSearchParams({
						query: artist,
						limit: 25,
						locale: 'en_US',
						countryCode: 'US',
						deviceType: 'BROWSER',
						token: tidalClientId,
					}), { responseType: 'json' }).then(function({response}) {
						if (response.totalNumberOfItems <= 0) return Promise.reject('Tidal: no matches');
						let results = resultsFilter(response.items, item => item.name);
						if (results.length <= 0) return Promise.reject('Tidal: no matches');
						//console.log('Tidal search results for "' + artist + '":', results);
						if (results.length > 1) return Promise.reject('Tidal: ambiguity');
						if (results.length > 1) console.info('Tidal returns ambiguous results for "' + artist + '":', results);
						if (!results[0].picture) return Promise.reject('Tidal: artist exists but no photo');
						return 'https://resources.tidal.com/images/' + results[0].picture.replace(/-/g, '/') + '/750x750.jpg';
					}),
					// Discogs
					globalXHR('https://api.discogs.com/database/search?' + new URLSearchParams({
						query: artist,
						type: 'artist',
						sort: 'score,desc',
						strict: false,
					}).toString(), {
						responseType: 'json',
						headers: { 'Authorization': 'Discogs key="' + discogsKey + '", secret="' + discogsSecret + '"' },
					}).then(({response}) => {
						if (response.items <= 0) return Promise.reject('Discogs: no matches');
						let results = resultsFilter(response.results.filter(result => result.type == 'artist'), result => result.title);
						if (results.length <= 0) return Promise.reject('Discogs: no matches');
						//console.log('Discogs search results for "' + artist + '":', results);
						//if (results.length > 1) return Promise.reject('Discogs: ambiguity');
						if (results.length > 1) console.info('Discogs returns ambiguous results for "' + artist + '":', results);
						return Promise.all(results.map(result => {
							if (result.cover_image.includes('/spacer.gif')) return null;
							return getDiscogsImageMax(result.cover_image);
						}).filter(Boolean)).then(artistCovers => httpParser.test(artistCovers[0]) ?
							artistCovers[0] : Promise.reject('Discogs: artist exists but no photo'));
					}),
					// Bandcamp
					globalXHR('https://bandcamp.com/search?q=' + encodeURIComponent('"' + artist + '"')).then(function({document}) {
						const results = resultsFilter(Array.from(document.querySelectorAll('div.results > ul.result-items > li.searchresult')).map(function(li) {
							try {
								var result = JSON.parse(li.dataset.search);
								if (result.type.toLowerCase() != 'b') return;
							} catch(e) {
								result = { }; // return;
								console.warn('Bandcamp: could not detect search result type', li);
							}
							if (!result.id) try {
								if (/\b(?:id)=(\d+)\b/.test(li.previousSibling.previousSibling.nodeValue))
									result.id = parseInt(RegExp.$1);
							} catch(e) { }
							let ref;
							if ((ref = li.querySelector('div.art > img')) != null) result.imageUrl = ref.src;
							if ((ref = li.querySelector('div.heading > a')) != null) {
								result.url = new URL(ref);
								result.url.search = '';
								result.name = ref.textContent.trim();
							}
							if ((ref = li.querySelector('div.subhead')) != null) result.location = ref.textContent.trim();
							if ((ref = li.querySelector('div.genre')) != null)
								result.genre = ref.textContent.trim().replace(/^(?:Genre:\s+)/i, '');
							if ((ref = li.querySelector('div.tags')) != null)
								result.tags = ref.textContent.trim().replace(/^(?:tags):\s+/, '').split(/\s*,\s*/);
							if (result.name) return result;
						}).filter(Boolean), result => result.name);
						if (results.length <= 0) return Promise.reject('Bandcamp: no matches');
						//console.log('Bandcamp search results for "' + artist + '":', results);
						if (results.length > 1) return Promise.reject('Bandcamp: ambiguity');
						if (results.length > 1) console.info('Bandcamp returns ambiguous results for "' + artist + '":', results);
						return httpParser.test(results[0].imageUrl) ? results[0].imageUrl.replace(/_\d+(\.\w+$)/, '_0')
							: Promise.reject('Bandcamp: artist exists but no photo');
					}),
					// Beatport
					globalXHR('https://www.beatport.com/search/artists?q=' + encodeURIComponent('"' + artist + '"')).then(function({document}) {
						const results = resultsFilter(Array.from(document.querySelectorAll('div.bucket.artists > ul.bucket-items > li.bucket-item')).map(function(li) {
							let result = {
								id: li.dataset.ecId,
								name: li.dataset.ecName,
								url: li.querySelector(':scope > a'),
								imageUrl: li.querySelector(':scope > a > img'),
							};
							if (result.url != null) {
								result.url = new URL(result.url);
								result.url.hostname = 'www.beatport.com';
							} else delete result.url;
							result.imageUrl = result.imageUrl != null ?
								result.imageUrl.src.replace(/\/image_size\/\d+x\d+\//, '/image/') : undefined;
							return result;
						}), result => result.name);
						if (results.length <= 0) return Promise.reject('Beatport: no matches');
						//console.log('Beatport search results for "' + artist + '":', results);
						if (results.length > 1) return Promise.reject('Beatport: ambiguity');
						if (results.length > 1) console.info('Beatport returns ambiguous results for "' + artist + '":', results);
						return httpParser.test(results[0].imageUrl) && ![
							'/0dc61986-bccf-49d4-8fad-6b147ea8f327.jpg',
							'/d02c012b-67d4-4058-a75f-3fbabdb8d19d.jpg',
						].some(id => results[0].imageUrl.endsWith(id)) ? results[0].imageUrl
							: Promise.reject('Beatport: artist exists but no photo');
					}),
					// iTunes
					globalXHR('https://itunes.apple.com/search?' + new URLSearchParams({
						term: '"' + artist + '"',
						media: 'music',
						entity: 'musicArtist',
						attribute: 'artistTerm',
						//country: 'US',
					}).toString(), { responseType: 'json' }).then(function({response}) {
						if (response.resultCount <= 0) return Promise.reject('iTunes: no matches');
						let results = resultsFilter(response.results.filter(result =>
							result.wrapperType == 'artist' && result.artistType == 'Artist'), result => result.artistName);
						if (results.length <= 0) return Promise.reject('iTunes: no matches');
						//console.debug('iTunes search results for "' + artist + '":', results);
						//if (results.length > 1) return Promise.reject('iTunes: ambiguity');
						if (results.length > 1) console.info('iTunes returns ambiguous results for "' + artist + '":', results);
						return imageUrlResolver(results[0].artistLinkUrl);
					}),
					// Spotify
					(function() {
						const isTokenValid = accessToken => typeof accessToken == 'object' && accessToken.token_type
							&& accessToken.access_token && accessToken.expires_at >= Date.now() + 30 * 1000;
						try {
							var accessToken = JSON.parse(window.localStorage.spotifyAccessToken);
							if (isTokenValid(accessToken)) return Promise.resolve(accessToken);
						} catch(e) { }
						const clientId = GM_getValue('spotify_clientid', '54468e0c92c24e0d86c61346155b32cd'),
									clientSecret = GM_getValue('spotify_clientsecret', '38cb34c7196d4cdca6dbb35b08e29e12');
						if (!clientId || !clientSecret) return Promise.reject('Spotify credentials not configured');
						const data = new URLSearchParams({
							'grant_type': 'client_credentials',
						}), timeStamp = Date.now();
						return globalXHR('https://accounts.spotify.com/api/token', { responseType: 'json', headers: {
							Authorization: 'Basic ' + btoa(clientId + ':' + clientSecret),
						} }, data).then(function({response}) {
							accessToken = response;
							const tzOffset = new Date().getTimezoneOffset() * 60 * 1000;
							if (!accessToken.timestamp) accessToken.timestamp = timeStamp; //else accessToken.timestamp -= tzOffset;
							if (!accessToken.expires_at) accessToken.expires_at = accessToken.timestamp + accessToken.expires_in * 1000;
								else accessToken.expires_at -= tzOffset;
							if (!isTokenValid(accessToken)) {
								console.warn('Received invalid Spotify token:', accessToken);
								return Promise.reject('invalid token received');
							}
							window.localStorage.spotifyAccessToken = JSON.stringify(accessToken);
							return accessToken;
						});
					})().then(credentials => globalXHR('https://api.spotify.com/v1/search?' + new URLSearchParams({
						q: artist,
						type: 'artist',
					}).toString(), {
						responseType: 'json',
						headers: {
							Accept: 'application/json',
							Authorization: credentials.token_type + ' ' + credentials.access_token,
						},
					})).then(function({response}) {
						if (response.artists.total <= 0) return Promise.reject('Spotify: no matches');
						let results = resultsFilter(response.artists.items.filter(item => item.type == 'artist'), item => item.name);
						if (results.length <= 0) return Promise.reject('Spotify: no matches');
						//console.debug('Spotify search results for "' + artist + '":', results);
						if (results.length > 1) return Promise.reject('Spotify: ambiguity');
						if (results.length > 1) console.info('iTunes returns ambiguous results for "' + artist + '":', results);
						return results[0].images && results[0].images.length > 0 ?
							results[0].images.sort((a, b) => (b.width * b.height) - (a.width * a.height))[0].url
								: Promise.reject('Spotify: artist exists but no photo');
					}),
					// Deezer
					globalXHR('https://api.deezer.com/search/artist?' + new URLSearchParams({
						q: artist,
						order: 'RANKING',
						//strict: 'on',
					}).toString(), { responseType: 'json' }).then(function({response}) {
						if (response.total <= 0) return Promise.reject('Deezer: no matches');
						let results = resultsFilter(response.data.filter(result => result.type == 'artist'),
							result => result.name);
						if (results.length <= 0) return Promise.reject('Deezer: no matches');
						//console.debug('Deezer search results for "' + artist + '":', results);
						//if (results.length > 1) return Promise.reject('Deezer: ambiguity');
						if (results.length > 1) console.info('Deezer returns ambiguous results for "' + artist + '":', results);
						return verifyImageUrl(results[0].picture).catch(function(reason) {
							console.warn('Deezer API image retrieval failed:', reason);
							return ['xl', 'big', 'medium', 'small'].reduce((acc, size) =>
								acc || response.data[0]['picture_' + size], null) || Promise.reject('no picture');
						}).then(imageUrl => imageUrl.includes('/images/artist//') ?
							Promise.reject('Deezer: artist exists but no photo') : getDeezerImageMax(imageUrl));
					}),
					// FLO
					globalXHR('https://www.music-flo.com/api/search/v2/search?' + new URLSearchParams({
						keyword: '"' + artist + '"',
						searchType: 'ARTIST',
						sortType: 'ACCURACY',
						size: 10,
					}).toString(), { responseType: 'json' }).then(function({response}) {
						if (response.code != 2000000) return Promise.reject(response.message);
						//if (response.data.totalCount <= 0) return Promise.reject('FLO: no matches');
						console.assert(Array.isArray(response.data.list), 'Array.isArray(response.data.list)', response);
						let results = resultsFilter(response.data.list[0].list, result => result.name);
						if (results.length <= 0) return Promise.reject('FLO: no matches');
						//console.debug('FLO search results for "' + artist + '":', response.data);
						if (results.length > 1) return Promise.reject('FLO: ambiguity');
						//if (results.length > 1) console.info('FLO returns ambiguous results for "' + artist + '":', results);
						const noPhoto = Promise.reject('FLO: artist exists but no photo');
						if (!Array.isArray(results[0].imgList) || results[0].imgList.length <= 0) return noPhoto;
						const imageUrl = results[0].imgList.reduce((acc, image) => image.url.replace(/\?.*$/, ''));
						return !imageUrl.includes('/000000000/000000000.') ? imageUrl : noPhoto;
					}),
					// OTOTOY
					globalXHR('https://ototoy.jp/find/?q=' + encodeURIComponent('"' + artist + '"')).then(function({document}) {
						const results = resultsFilter(Array.from(document.querySelectorAll('div.results_box > div.find-artist div.find-candidates')).map(function(div) {
							let result = {
								url: div.querySelector('div.artist-name > a'),
								imageUrl: div.querySelector('figure > a > img'),
							};
							if (result.url != null) {
								result.name = result.url.title || result.url.textContent.trim();
								result.url = new URL(result.url);
								result.url.hostname = 'ototoy.jp';
								result.id = /\/a\/(\d+)\b/i.exec(result.url.pathname);
								if (result.id != null) result.id = parseInt(result.id[1]); else delete result.id;
							} else delete result.url;
							if (result.imageUrl != null) {
								result.imageUrl = new URL(result.imageUrl.src);
								result.imageUrl = result.imageUrl.origin + new URLSearchParams(result.imageUrl.search).get('image');
							} else delete result.imageUrl;
							if (result.name) return result;
						}).filter(Boolean), result => result.name);
						if (results.length <= 0) return Promise.reject('OTOTOY: no matches');
						//console.log('OTOTOY search results for "' + artist + '":', results);
						if (results.length > 1) return Promise.reject('OTOTOY: ambiguity');
						//if (results.length > 1) console.info('OTOTOY returns ambiguous results for "' + artist + '":', results);
						return httpParser.test(results[0].imageUrl) && !results[0].imageUrl.endsWith('/0dc61986-bccf-49d4-8fad-6b147ea8f327.jpg') ?
							results[0].imageUrl : Promise.reject('OTOTOY: artist exists but no photo');
					}),
					// Recochoku
					globalXHR('https://recochoku.jp/search/artist?q=' + encodeURIComponent(artist)).then(({document}) =>
							Array.from(document.querySelectorAll('ul#artistContents > li > a')).map(function(a) {
						let result = {
							url: new URL(a.pathname, 'https://recochoku.jp'),
							id: /\/artist\/(\d+)\b/i.exec(a.pathname),
							name: a.querySelector('div > div[class$="title"]'),
							imageUrl: a.getElementsByTagName('IMG'),
						};
						if (result.name) result.name = result.name.textContent.trim(); else return null;
						if (result.imageUrl.length > 0) {
							result.imageUrl = new URL(result.imageUrl[0].dataset.src);
							result.imageUrl.searchParams.set('FFw', 999999999);
							result.imageUrl.searchParams.set('FFh', 999999999);
							result.imageUrl.searchParams.delete('h');
							result.imageUrl.searchParams.delete('option');
						} else return null;
						if (result.id != null) result.id = result.id[1]; else delete result.id;
						return result;
					}).filter(Boolean)).then(function(results) {
						if (results.length <= 0) return Promise.reject('Recochoku: no matches');
						console.log('Recochoku search results for "' + artist + '":', results);
						results = resultsFilter(results, result => result.name);
						if (results.length <= 0) return Promise.reject('Recochoku: no matches');
						if (results.length > 1) return Promise.reject('Recochoku: ambiguity');
						//if (results.length > 1) console.info('Recochoku returns ambiguous results for "' + artist + '":', results);
						return httpParser.test(results[0].imageUrl) && !results[0].imageUrl.endsWith('/noimage_artist.png') ?
							results[0].imageUrl : Promise.reject('Recochoku: artist exists but no photo');
					}),
					// QQ音乐
					globalXHR('https://c.y.qq.com/soso/fcgi-bin/client_search_cp?' + new URLSearchParams({
						format: 'json',
						t: 9,
						w: artist,
						inCharset: 'utf8',
						outCharset: 'utf-8',
					}).toString(), { responseType: 'json' }).then(function({response}) {
						if (response.data.singer.totalnum <= 0) return Promise.reject('QQ音乐: no matches');
						console.log('QQ音乐 search results for "' + artist + '":', response.data.singer);
						const results = resultsFilter(response.data.singer.list, singer => singer.singerName);
						if (results.length <= 0) return Promise.reject('QQ音乐: no matches');
						//if (results.length > 1) return Promise.reject('QQ音乐: ambiguity');
						if (results.length > 1) console.info('QQ音乐 returns ambiguous results for "' + artist + '":', results);
						return verifyImageUrl(results[0].singerPic.replace(/R\d+x\d+/, ''))
							.catch(reason => verifyImageUrl(results[0].singerPic))
							.catch(reason => Promise.reject('QQ音乐: artist exists but no photo'));
					}),
					// YouTTube Music
					(function() {
						if ('ytcfg' in sessionStorage) try { return Promise.resolve(JSON.parse(sessionStorage.ytcfg)) }
							catch(e) { console.warn('Invalid ytcfg format:', e) }
						return globalXHR('https://music.youtube.com/').then(function({document}) {
							for (let script of document.querySelectorAll('head > script[nonce]')) {
								let ytcfg = /^\s*\b(?:ytcfg\.set)\s*\(\s*(\{.+\})\s*\);/m.exec(script.text);
								if (ytcfg != null) try {
									ytcfg = JSON.parse(ytcfg[1]);
									if (ytcfg.INNERTUBE_API_KEY) {
										sessionStorage.ytcfg = JSON.stringify(ytcfg);
										return ytcfg;
									} else console.warn('YouTube Music API key missing:', ytcfg);
								} catch(e) { console.warn('Error parsing ytcfg:', ytcfg[1]) }
							}
							return Promise.reject('unable to extract YouTube config ot the config is invalid');
						});
					})().then(ytcfg => globalXHR('https://music.youtube.com/youtubei/v1/search?' + new URLSearchParams({
						alt: 'json',
						key: ytcfg.INNERTUBE_API_KEY,
					}).toString(), {
						responseType: 'json',
						headers: { 'Referer': 'https://music.youtube.com/' },
					}, {
						query: artist,
						params: encodeURIComponent('EgWKAQIgAWoKEAkQChADEAUQBA=='),
						context: {
							activePlayers: { }, capabilities: { },
							client: Object.assign({
								experimentIds: [ ], experimentsToken: "",
								locationInfo: {
									locationPermissionAuthorizationStatus: "LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED",
								},
								musicAppInfo: {
									musicActivityMasterSwitch: "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE",
									musicLocationMasterSwitch: "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE",
									pwaInstallabilityStatus: "PWA_INSTALLABILITY_STATUS_UNKNOWN",
								},
								utcOffsetMinutes: -new Date().getTimezoneOffset(),
							}, ytcfg.INNERTUBE_CONTEXT.client, { hl: 'en' }),
							request: {
								internalExperimentFlags: [
									{ key: "force_music_enable_outertube_search", value: "true" }
								],
							},
							user: { enableSafetyMode: false },
						},
					})).then(({response}) => response.contents.sectionListRenderer.contents[0].musicShelfRenderer.contents.map(function(item) {
						let result = {
							id: item.musicResponsiveListItemRenderer.navigationEndpoint.browseEndpoint.browseId,
							name: item.musicResponsiveListItemRenderer.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
							photoUrl: item.musicResponsiveListItemRenderer.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails,
						};
						result.webUrl = result.id ? 'https://music.youtube.com/channel/' + result.id : undefined;
						result.photoUrl = Array.isArray(result.photoUrl) && result.photoUrl.length > 0 ?
							result.photoUrl[0].url.replace(/(?:=[swh]\d+.*)?$/, '=s0') : undefined;
						return result;
					})).then(function(results) {
						if (results.length <= 0) return Promise.reject('YouTube Music: no matches');
						results = resultsFilter(results, result => result.name);
						if (results.length <= 0) return Promise.reject('YouTube Music: no matches');
						//console.debug('YouTube Music search results for "' + artist + '":', results);
						if (results.length > 1) return Promise.reject('YouTube Music: ambiguity');
						if (results.length > 1) console.info('YouTube Music returns ambiguous results for "' + artist + '":', results);
						return httpParser.test(results[0].photoUrl) ? results[0].photoUrl
							: Promise.reject('YouTube Music: artist exists but no photo');
					}),
					// Last.fm
					globalXHR('http://ws.audioscrobbler.com/2.0/?' + new URLSearchParams({
						method: 'artist.getinfo',
						artist: artist,
						format: 'json',
						api_key: lfmApiKey,
					}).toString(), { responseType: 'json' }).then(function({response}) {
						if (response.error) return Promise.reject(response.message);
						//console.debug('Last.fm search result for "' + artist + '":', response.artist);
						const rx = /\/(\d+)x(\d+)\//;
						let biggest = response.artist.image.map(im => im['#text']).reduce(function(a, b) {
							let r = [a, b].map(RegExp.prototype.exec.bind(rx))
								.map(r => r != null ? parseInt(r[1]) * parseInt(r[2]) : -Infinity);
							return r[1] > r[0] ? b : a;
						});
						return rx.test(biggest) && !biggest.endsWith('/2a96cbd8b46e442fc41c2b86b821562f.png') ?
							biggest : Promise.reject('Last.fm: artist exists but no photo');
					}),
				];
				let imageLookupChain = GM_getValue('artist_image_lookup_providers');
				if (!imageLookupChain || imageLookupChain == 'all') imageLookupChain = lookupWorkers; else {
					if (typeof imageLookupChain == 'string') imageLookupChain = imageLookupChain.split(/\W+/);
					if (!Array.isArray(imageLookupChain)
							|| (imageLookupChain = imageLookupChain.map(key => lookupWorkers[key]).filter(Boolean)).length <= 0)
						return Promise.reject('No image lookup providers matching user list');
				}
				const lookUp = (index = 0) => index >= 0 && index < imageLookupChain.length ?
					imageLookupChain[index].then(setCover.bind(input)).catch(reason => lookUp(index + 1))
						: Promise.reject('Image of this artist was not found');
				return lookUp();
			}).catch(logFail);
		}
		break;
	case 'tracker.czech-server.com':
		if (document.location.pathname == '/upload2.php')
			document.querySelectorAll('input[type="text"][name="urlobr"]').forEach(setInputHandlers);
		break;
}
switch (document.location.pathname) {
	case '/torrents.php': {
		if (!document.location.search.startsWith('?id=')) break;
		const addCoversForm = document.getElementById('add_cover');
		if (addCoversForm != null) new MutationObserver(function(mutationsList, mo) {
			for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
				if (node.nodeName == 'INPUT' && node.type == 'text' && node.name == 'image[]') setInputHandlers(node);
			});
		}).observe(addCoversForm, { childList: true });
		break;
	}
	case '/reportsv2.php': {
		const dynaForm = document.getElementById('dynamic_form');
		if (dynaForm == null) break;
		function setReportHandlers(root = dynaForm) {
			root.querySelectorAll('input[id*="image"]').forEach(setInputHandlers);
			for (let ta of root.getElementsByTagName('TEXTAREA')) setTextAreahandlers(ta);
		}
		new MutationObserver(function(mutationsList, mo) {
			for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
				if (node.nodeType == Node.ELEMENT_NODE) setReportHandlers(node);
			});
		}).observe(dynaForm, { childList: true });
		break;
	}
	case '/forums.php': {
		if (!document.location.search.startsWith('?action=viewthread&')) break;
		let container = document.querySelector('div#content > div.thin');
		if (container != null) new MutationObserver(function(mutationsList, mo) {
			for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
				if (node.nodeName == 'FORM') for (let elem of node.getElementsByTagName('TEXTAREA')) setTextAreahandlers(elem);
			});
		}).observe(container, { childList: true, subtree: true });
		break;
	}
}

let opti_PNG = GM_getValue('optipng', false);

function imagePreview(imgUrl, size) {
	let div = document.getElementById('image-preview');
	if (div != null) document.body.removeChild(div);
	if (!httpParser.test(imgUrl)) return;
	div = document.createElement('div');
	div.id = 'image-preview';
	div.style = 'position: fixed; bottom: 20px; right: 20px; border: thin solid silver; ' +
		'background-color: #8888; padding: 10px; opacity: 0; transition: opacity 1s ease-in-out; z-index: 999999999;';
	const cleanUp = function(div) {
		if (div.parentNode == null) return;
		div.style.opacity = 0;
		setTimeout(div => { document.body.removeChild(div) }, 1000, div);
	};
	div.ondblclick = evt => { cleanUp(evt.currentTarget) };
	let img = document.createElement('img');
	img.style = 'width: 225px;';
	img.onload = function(evt) {
		if (evt.currentTarget.parentNode.parentNode == null) document.body.append(evt.currentTarget.parentNode);
		setTimeout(div => { div.style.opacity = 1 }, 0, evt.currentTarget.parentNode);
		setTimeout(cleanUp, 12000, evt.currentTarget.parentNode);
		if (!evt.currentTarget.naturalWidth || !evt.currentTarget.naturalHeight) return; // invalid image
		let info = document.createElement('div');
		info.id = 'image-info';
		info.style = 'text-align: center; background-color: #29434b; padding: 5px; color: white;' +
			'font: 500 10pt "Segoe UI", Verdana, sans-serif;';
		evt.currentTarget.parentNode.append(info);
		const resolution = evt.currentTarget.naturalWidth + '×' + evt.currentTarget.naturalHeight;
		(size > 0 ? Promise.resolve(size) : size instanceof Promise ? size : getRemoteFileSize(imgUrl)).then(function(size) {
			if (!(size >= 0)) throw 'invalid size';
			let imageSizeLimit = GM_getValue('image_size_reduce_threshold'),
					html = resolution + ' (<span id="image-size"';
			if (imageSizeLimit > 0 && size > imageSizeLimit * 2**10) html += ' style="color: red;"';
			html += '>' + formattedSize(size) + '</span>)';
			info.innerHTML = html;
		}).catch(reason => { info.textContent = resolution });
	};
	img.onerror = evt => { console.warn('Image source couldnot be loaded:', evt, imgUrl) };
	img.src = imgUrl;
	div.append(img);
}

function writeInfo() {
	let input = document.querySelector('input[name="summary"]');
	if (input != null && !input.disabled && !input.value) input.value = 'Image update/rehost';
}

function setCover(url) {
	return verifyImageUrl(url).then(imageUrl => {
		this.value = imageUrl;
		writeInfo();
		let size = getRemoteFileSize(imageUrl);
		imagePreview(imageUrl, size);
		return checkImageSize(imageUrl, this, size).then(imageUrl => {
			this.disabled = true;
			return imageHosts.rehostImages([imageUrl]).then(singleImageGetter).then(imageUrl => {
				if (imageUrl == null) throw 'invalid image';
				this.value = imageUrl;
			});
		}).catch(reason => {
			this.value = imageUrl;
			logFail(reason + ' (not rehosted)');
		}).then(() => {
			this.disabled = false;
			return imageUrl;
		});
	});
}

function inputDataHandler(evt, data) {
	const input = evt.currentTarget;
	console.assert(input instanceof HTMLInputElement, 'input instanceof HTMLInputElement');

	const rehoster = imageUrl => imageHosts.rehostImages([imageUrl]).then(singleImageGetter).then(function(imageUrl) {
		if (!httpParser.test(imageUrl)) {
			console.warn('rehostImages returns invalid image URL:', imageUrl);
			throw 'invalid image URL';
		}
		input.value = imageUrl;
		writeInfo();
	});

	if (!data) return true;
	if (data.files.length > 0) {
		if (data.files[0].type && !data.files[0].type.startsWith('image/')) return true;
		input.disabled = true;
		if (input.hTimer) {
			clearTimeout(input.hTimer);
			delete input.hTimer;
		}
		input.style.color = 'white';
		input.style.backgroundColor = 'darkred';
		let progressBar = { };
		function progressHandler(worker, param = null) {
			if (param && typeof param == 'object') {
				if (param.readyState > 1 || progressBar.current != undefined && worker !== progressBar.current
						|| Date.now() < progressBar.lastUpdate + 100) return;
				let pct = Math.floor(Math.min(param.done * 100 / param.total, 100));
				if (pct <= progressBar.lastPct) return;
				input.value = 'Uploading... [' + (progressBar.lastPct = pct) + '%]';
				progressBar.lastUpdate = Date.now();
			} else if (param == null) {
				progressBar = { current: worker };
				input.value = 'Uploading...';
			}
		}
		const file = data.files[0];
		input.disabled = true;
		checkImageSize(file, input, progressHandler).catch(function(reason) {
			logFail('Downsizing of source image not possible (' + reason + '), uploading original size');
			return file;
		}).then(function(result) {
			const uploader = file => imageHosts.uploadImages([file], progressHandler).then(singleImageGetter).then(function(imageUrl) {
				input.value = imageUrl;
				imagePreview(imageUrl, file.size);
				writeInfo();
			});

			if (httpParser.test(result)) return rehoster(result).catch(function(reason) {
				logFail('Downsizing of source image failed (' + reason + '), uploading original size');
				return uploader(file);
			});
			if (result instanceof File) return uploader(result);
			console.warn('invalid checkImageSize(...) result:', result);
			return Promise.reject('invalid checkImageSize(...) result');
		}).then(function() {
			input.style.backgroundColor = '#008000';
			input.hTimer = setTimeout(function() {
				input.style.backgroundColor = null;
				input.style.color = null;
				delete input.hTimer;
			}, 10000);
		}, function(reason) {
			imageClear(evt);
			input.style.backgroundColor = null;
			input.style.color = null;
			Promise.resolve(reason).then(msg => { alert(msg) });
		}).then(() => { input.disabled = false });
		return false;
	} else if (data.items.length > 0) {
		let urls = data.getData('text/uri-list');
		if (urls) urls = urls.split(/\r?\n/); else {
			urls = data.getData('text/x-moz-url');
			if (urls) urls = urls.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
				else if (urls = data.getData('text/plain')) urls = urls.split(/\r?\n/);
		}
		if (!Array.isArray(urls) || urls.length <= 0) return true;
		input.disabled = true;
		console.time('Image URL Rehoster');
		imageUrlResolver(urls[0], {
			altKey: evt.altKey,
			ctrlKey: evt.ctrlKey != (input.name == 'image[]'),
			shiftKey: evt.shiftKey,
		}).then(verifyImageUrl).then(function(imageUrl) {
			input.disabled = true;
			input.value = imageUrl;
			const size = getRemoteFileSize(imageUrl);
			imagePreview(imageUrl, size);
			checkImageSize(imageUrl, input, size).then(rehoster).catch(function(reason) {
				input.value = imageUrl;
				Promise.resolve(reason).then(msg => { alert(msg + ' (not rehosted)') });
			}).then(() => { console.timeEnd('Image URL Rehoster') });
		}).catch(reason => { Promise.resolve(reason).then(alert) }).then(() => { input.disabled = false });
		return false;
	}
	return true;
}

unsafeWindow.uploadImages = function uploadImages(files, checkSize = true, preview = false) {
	if (!Array.isArray(files)) return Promise.reject('Invalid parameter (files)');
	if ((files = files.filter(file => file instanceof File && file.type.startsWith('image/'))).length <= 0)
		return Promise.reject('Invalid parameter (no valid images passed)');
	console.time('Image uploader');
	return checkSize || preview ? Promise.all(files.map(file => (checkSize ? checkImageSize(file).catch(function(reason) {
		logFail('Downsizing of source image not possible (' + reason + '), uploading original size');
		return file;
	}) : Promise.resolve(file)).then(function(result) {
		const uploader = file => imageHosts.uploadImages([file]).then(singleImageGetter).then(function(imageUrl) {
			if (preview) imagePreview(imageUrl, file.size);
			return imageUrl;
		});
		if (httpParser.test(result)) return imageHosts.rehostImages([result]).catch(function(reason) {
			logFail('Downsizing of source image failed (' + reason + '), uploading original size');
			return uploader(file);
		});
		if (result instanceof File) return uploader(result);
		console.warn('invalid checkImageSize(...) result:', result);
		return Promise.reject('invalid checkImageSize(...) result');
	}))) : imageHosts.uploadImages(files);
};
unsafeWindow.rehostImageLinks = function rehostImageLinks(urls, checkSize = true, enforceRehost = true, modifiers, preview = false) {
	if (!Array.isArray(urls) || urls.length <= 0) return Promise.reject('Invalid parameter (urls)');
	console.time('Image URL rehoster');
	return Promise.all(urls.map(url => imageUrlResolver(url, {
		altKey: Boolean(typeof modifiers == 'object' && modifiers.altKey),
		ctrlKey: Boolean(typeof modifiers == 'object' && modifiers.ctrlKey),
		shiftKey: Boolean(typeof modifiers == 'object' && modifiers.shiftKey),
	}).then(verifyImageUrl).then(function(imageUrl) {
		if (!checkSize) return imageUrl;
		const size = getRemoteFileSize(imageUrl);
		if (preview) imagePreview(imageUrl, size);
		return checkImageSize(imageUrl, null, size);
	}))).then(imageUrls => imageHosts.rehostImages(imageUrls).then(function(rehostedImages) {
		console.timeEnd('Image URL rehoster');
		return rehostedImages;
	}, reason => enforceRehost ? Promise.reject(reason) : Promise.resolve(imageUrls)));
};
unsafeWindow.ihhLogFail = logFail;
{
	const meta = document.createElement('META');
	meta.name = 'ImageHostHelper';
	meta.content = 'All endpoints exported';
	document.head.append(meta);
}

function rehoster(promises, resultsHandler, target = null) {
	if (!Array.isArray(promises)) throw 'invalid parameter';
	console.time('Image URL Resolver');
	return Promise.all(promises).then(function(resolved) {
		let resolvedUrls = resolved.flatten();
		if (target instanceof HTMLElement) {
			target.disabled = true;
			if (resolvedUrls.length > 1 && !['notwhat.cd'].some(hostname => document.domain == hostname))
				var progressBar = new RHProgressBar(target, resolvedUrls.length);
		}
		return (function() {
			if (!opti_PNG || !(target instanceof HTMLElement)) return Promise.resolve(resolvedUrls);
			return Promise.all(resolvedUrls.map(resolvedUrl => optiPNG(resolvedUrl).catch(reason => resolvedUrl)));
		})().then(srcUrls => imageHosts.rehostImages(srcUrls, RHProgressBar.prototype.update.bind(progressBar)).catch(function(reason) {
			logFail(reason + ' (not rehosted)');
			RHProgressBar.prototype.update.call(progressBar, -1, false);
			return verifyImageUrls(srcUrls);
		}).then(function(results) {
			resolved.forEach(function(elem, index) {
				if (!elem.caption) return;
				if (!Array.isArray(results.captions)) results.captions = [ ];
				results.captions.push(elem.caption);
			});
			resultsHandler(results, arrayGrouping(resolved).flatten());
		}).catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })).then(function() {
			RHProgressBar.prototype.cleanUp.call(progressBar);
			if (target instanceof HTMLElement) target.disabled = false;
			console.timeEnd('Image URL Resolver');
		});
	});
}

function textAreaDropHandler(evt) {
	if (!evt.dataTransfer || evt.shiftKey) return true;
	const textArea = evt.currentTarget;
	console.assert(textArea instanceof HTMLTextAreaElement, 'textArea instanceof HTMLTextAreaElement');
	if (evt.dataTransfer.files.length > 0) {
		let images = Array.from(evt.dataTransfer.files).filter(file => !file.type || file.type.startsWith('image/'));
		if (images.length <= 0) return true;
		textArea.disabled = true;
		if (!['notwhat.cd'].some(hostname => document.domain == hostname))
			var progressBar = new ULProgressBar(textArea, images.map(image => image.size));
		(function() {
			if (!opti_PNG || !images.every(image => image.type == 'image/png')) return Promise.reject('!optiPNG');
			ULProgressBar.prototype.update.call(progressBar, -1);
			return rehoster([Promise.all(images.map((image, index) => optiPNG(image, (param = null) =>
				ULProgressBar.prototype.update.call(progressBar, -1, param, index))))], resultsHandler);
		})().catch(reason => imageHosts.uploadImages(images, ULProgressBar.prototype.update.bind(progressBar)).then(resultsHandler))
		.catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })
		.then(function() {
			ULProgressBar.prototype.cleanUp.call(progressBar);
			textArea.disabled = false;
		});
		evt.stopPropagation();
		return false;
	} else if (evt.dataTransfer.items.length > 0) {
		let content = evt.dataTransfer.getData('text/uri-list');
		if (content) content = content.split(/(?:\r?\n)+/); else {
			content = evt.dataTransfer.getData('text/x-moz-url');
			if (content) content = content.split(/(?:\r?\n)+/).filter((item, ndx) => ndx % 2 == 0);
		};
		if (!Array.isArray(content) || content.length <= 0) return true;
		rehoster(content.map(url => imageUrlResolver(url, { ctrlKey: !evt.ctrlKey })), resultsHandler, textArea).catch(function(reason) {
			if (evt.ctrlKey)
				textArea.value = textArea.value.slice(0, evt.rangeOffset) + content.join('\n') +
					textArea.value.slice(evt.rangeOffset);
			else {
				if (textArea.value.length > 0) textArea.value += '\n\n';
				textArea.value += content.join('\n');
			}
		});
		evt.stopPropagation();
		return false;
	}
	return true;

	function resultsHandler(results, groups = undefined) {
		if (results.length <= 0) return;
		if (evt.altKey && !textArea.noBBCode) {
			let modal = document.createElement('div');
			modal.id = 'ihh-template-selector-background';
			modal.style = 'position: fixed; left: 0; top: 0; width: 100%; height: 100%; background-color: #0008;' +
				'opacity: 0; transition: opacity 0.15s linear;';
			modal.innerHTML = `
<form id="ihh-template-selector" style="background-color: darkslategray; position: absolute; top: 30%; left: 35%; border-radius: 0.5em; padding: 20px 30px;">
	<div style="color: white; margin-bottom: 20px;">Insert as:</div>
	<input id="btn-insert" type="button" value="Insert" style="margin-top: 30px"/>
	<input id="btn-cancel" type="button" value="Cancel" style="margin-top: 30px"/>
</form>
`;
			document.body.append(modal);
			let form = document.getElementById('ihh-template-selector'),
					btnInsert = form.querySelector('input#btn-insert'),
					btnCancel = form.querySelector('input#btn-cancel');
			if (form == null || btnInsert == null || btnCancel == null) {
				console.warn('Dialog creation error');
				insertResults();
				return;
			}
			[
				['BBcode: original size', 1],
				['BBcode: thumbnails with link to original', 2],
				['BBcode: thumbnails with link to share page', 3],
				['BBcode: screenshot comparison (PTP)', 4],
				['BBcode: screenshot comparison + encode images (PTP)', 5],
				['Markdown: original size', 9],
				['HTML: original size', 6],
				['HTML: thumbnails with link to original', 7],
				['HTML: thumbnails with link to share page', 8],
				['Raw links', 0],
			].forEach(function(item) {
				let radio = document.createElement('input');
				radio.type = 'radio';
				radio.name = 'template';
				radio.value = item[1];
				radio.style = 'margin: 5px 15px 5px 0px; cursor: pointer;';
				let label = document.createElement('label');
				label.style = 'color: white; cursor: pointer; -webkit-user-select: none; ' +
					'-moz-user-select: none; -ms-user-select: none; user-select: none;';
				label.append(radio);
				label.append(item[0]);
				form.insertBefore(label, btnInsert);
				let br = document.createElement('br');
				form.insertBefore(br, btnInsert);
			});
			if (!results.some(result => typeof result == 'object'
					&& httpParser.test(result.original) && httpParser.test(result.thumb))) disableItem(2, 7);
			if (!results.some(result => typeof result == 'object'
					&& httpParser.test(result.original) && httpParser.test(result.share))) disableItem(3, 8);
			if (results.length % 2 != 0) disableItem(4, 5);
			form.onclick = evt => { evt.stopPropagation() };
			btnInsert.onclick = function(evt) {
				let template = document.querySelector('form#ihh-template-selector input[name="template"]:checked');
				if (template != null) template = parseInt(template.value);
				modal.remove();
				insertResults(template);
			};
			modal.onclick = btnCancel.onclick = evt => { modal.remove() };
			window.setTimeout(() => { modal.style.opacity = 1 });

			function disableItem(...n) {
				n.forEach(function(n) {
					let radio = document.querySelector('div#ihh-template-selector input[type="radio"][value="' + n + '"]');
					if (radio == null) return;
					radio.parentNode.style.opacity = 0.5;
					radio.disabled = true;
				});
			}
		} else insertResults();

		function insertResults(template = 1) {
			if (textArea.noBBCode) template = 0;
			if (typeof template != 'number' || isNaN(template)) return;
			let code = '', nl = [6, 7, 8].includes(template) ? '<br>\n' : '\n', _template;
			results.forEach(function(result, index) {
				if (_template == 1 && /\[img\]\[\/img\]/i.test(textArea.value)) {
					textArea.value = RegExp.leftContext + '[img]' + getImgUrl(result) + '[/img]' + RegExp.rightContext;
					return;
				}
				_template = template;
				if (template == 2 && (typeof result != 'object' || !httpParser.test(result.original) || !httpParser.test(result.thumb))
						|| template == 3 && (typeof result != 'object' || !httpParser.test(result.share) || !httpParser.test(result.thumb)))
					_template = 1;
				else if (template == 7 && (typeof result != 'object' || !httpParser.test(result.original) || !httpParser.test(result.thumb))
						|| template == 8 && (typeof result != 'object' || !httpParser.test(result.share) || !httpParser.test(result.thumb)))
					_template = 6;
				else _template = template;
				if (index > 0) {
					let thumb = [2, 3, 7, 8].includes(_template);
					code += isGroupBoundary(groups, index) ? thumb ? nl : nl + nl : thumb ? ' ' : nl;
				}
				switch (_template) {
					case 0: case 4: case 5: code += getImgUrl(result); break;
					case 1: code += '[img]' + getImgUrl(result) + '[/img]'; break;
					case 2: code += '[url=' + getImgUrl(result) + '][img]' + result.thumb + '[/img][/url]'; break;
					case 3: code += '[url=' + result.share + '][img]' + result.thumb + '[/img][/url]'; break;
					case 6: code += '<img src="' + getImgUrl(result) + '">'; break;
					case 7: code += '<a href="' + getImgUrl(result) + '" target="_blank"><img src="' + result.thumb + '"></a>'; break;
					case 8: code += '<a href="' + result.share + '" target="_blank"><img src="' + result.thumb + '"></a>'; break;
					case 9: code += '![](' + getImgUrl(result) + ')'; break;
				}
			});
			if ([4, 5].includes(template)) {
				if (Array.isArray(results.captions)) {
					var captions = results.captions.shift();
					if (Array.isArray(captions)) captions = captions.join(', ');
				}
				code = '[comparison=' + (captions || 'Source, Encode') + ']' + code + '[/comparison]';
				if (template == 5) {
					code += nl;
					results.forEach((result, index) => { if (index % 2 != 0) code += nl + '[img]' + getImgUrl(result) + '[/img]' });
				}
			}
			if (textArea.value.trimRight().length <= 0) textArea.value = code; else if (evt.ctrlKey) {
				textArea.value = textArea.value.slice(0, evt.rangeOffset) + code + textArea.value.slice(evt.rangeOffset);
			} else textArea.value = textArea.value.trimRight() + nl + nl + code;

			function getImgUrl(result) {
				if (typeof result == 'object' && httpParser.test(result.original)) return result.original;
				if (typeof result == 'string' && httpParser.test(result)) return result;
				throw 'Invalid result format';
			}
		}
	}
}

function textAreaPasteHandler(evt) {
	if (!evt.clipboardData) return true;
	const textArea = evt.currentTarget;
	console.assert(textArea instanceof HTMLTextAreaElement, 'textArea instanceof HTMLTextAreaElement');
	if (evt.clipboardData.files.length > 0) {
		let images = Array.from(evt.clipboardData.files).filter(file => !file.type || file.type.startsWith('image/'));
		if (images.length <= 0) return true;
		textArea.disabled = true;
		if (!['notwhat.cd'].some(hostname => document.domain == hostname))
			var progressBar = new ULProgressBar(textArea, images.map(image => image.size));
		(function() {
			if (!opti_PNG || !images.every(image => image.type == 'image/png')) return Promise.reject('!optiPNG');
			ULProgressBar.prototype.update.call(progressBar, -1);
			return rehoster([Promise.all(images.map((image, index) => optiPNG(image, (param = null) =>
				ULProgressBar.prototype.update.call(progressBar, -1, param, index))))], resultsHandler);
		})().catch(reason => imageHosts.uploadImages(images, ULProgressBar.prototype.update.bind(progressBar)).then(resultsHandler))
		.catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })
		.then(function() { // __finally
			ULProgressBar.prototype.cleanUp.call(progressBar);
			textArea.disabled = false;
		});
		evt.stopPropagation();
		return false;
	} else if (evt.clipboardData.items.length > 0) {
		return true;
		let urls = evt.clipboardData.getData('text/plain').split(/(?:\r?\n)+/);
		if (urls.length <= 0 || !urls.every(RegExp.prototype.test.bind(httpParser))) return true;
		rehoster(urls.map(url => imageUrlResolver(url, { ctrlKey: !evt.ctrlKey })), resultsHandler, textArea);
		evt.stopPropagation();
		return false;
	}
	return true;

	function resultsHandler(results, groups = undefined) {
		let selStart = textArea.selectionStart, phpBB = '';
		results.forEach(function(result, index) {
			let thumb = evt.altKey && !textArea.noBBCode && typeof result == 'object'
				&& httpParser.test(result.originasl) && httpParser.test(result.thumb);
			if (index > 0) phpBB += isGroupBoundary(groups, index) ? thumb ? '\n' : '\n\n' : thumb ? ' ' : '\n';
			if (typeof result == 'object' && result.original) var imgUrl = result.original;
				else if (typeof result == 'string') imgUrl = result;
					else throw 'Invalid result format';
			phpBB += textArea.noBBCode ? phpBB += imgUrl : !thumb ? '[img]' + imgUrl + '[/img]'
				: '[url=' + imgUrl + '][img]' + result.thumb + '[/img][/url]';
		});
		if (phpBB.length <= 0) return;
		textArea.value = textArea.value.slice(0, selStart) + phpBB + textArea.value.slice(textArea.selectionEnd);
		textArea.setSelectionRange(selStart + phpBB.length, selStart + phpBB.length);
	}
}

function arrayGrouping(arr) {
	return Array.isArray(arr) ? arr.map(function(elem) {
		if (!Array.isArray(elem)) return 1;
		return elem.every(elem => !Array.isArray(elem)) ? elem.length : arrayGrouping(elem);
	}) : null;
}

function isGroupBoundary(groups, index) {
	return index > 0 && Array.isArray(groups)
		&& groups.some((len, ndx, arr) => index == arr.slice(0, ndx).reduce((acc, len) => acc + len, 0));
}

function getDeezerImageMax(imageUrl) {
	if (!httpParser.test(imageUrl)) return Promise.reject('invalid image URL');
	const dzrImgResParser = /\/(\d+x\d+)(?:\-\d+)*\.(\w+)$/;
	let ext = dzrImgResParser.exec(imageUrl);
	if (ext != null) ext = GM_getValue('deezer_get_png_cover', false) ? 'png' : ext[2]; else {
		console.warn('Unscalable Deezer image, returning unchanged:', imageUrl);
		return Promise.resolve(imageUrl);
	}
	const urlByResolution = resolution => imageUrl.replace(dzrImgResParser, '/' + resolution + 'x' + resolution) +
		(/^j(?:pe?g|fif)$/i.test(ext) ? `-000000-${parseInt(GM_getValue('deezer_jpeg_quality')) || 100}-0-0.${ext}` : '.' + ext);
	const deezerHighestResolution = Math.max(parseInt(GM_getValue('deezer_highest_resolution')) || 1500, 1200);
	const defaultMax = (res = deezerHighestResolution) => verifyImageUrl(urlByResolution(res)).catch(reason => imageUrl);
	const resolutions = [/*1200, */1400, 1425, 1440, 1500, 1600, 1800, 1920].filter(size => size <= deezerHighestResolution);
	return Math.max(...resolutions) > 1400 ? Promise.all(resolutions.map(res => new Promise(function(resolve, reject) {
		let img = document.createElement('img');
		img.onload = load => { resolve(load.target.naturalWidth * load.target.naturalHeight) };
		img.onerror = (message, source, lineno, colno, error) => { reject(message) };
		img.src = imageUrl.replace(dzrImgResParser, '/' + res + 'x' + res + '.png');
	}).catch(reason => -Infinity))).then(function(pixTotals) {
		let maxArea = Math.max(...pixTotals);
		if (maxArea <= 0) {
			console.warn('Deezer: no max variant returns valid image', pixTotals, imageUrl);
			return Promise.reject('all size variants failed to load'); //defaultMax()
		}
		return urlByResolution(resolutions[pixTotals.indexOf(maxArea)]);
	}) : defaultMax(deezerHighestResolution);
}

function checkImageSize(image, elem = null, param) {
	let imageSizeLimit = GM_getValue('image_size_reduce_threshold');
	if (!(imageSizeLimit > 0)) return Promise.resolve(image);
	if (!(elem instanceof HTMLElement)) elem = null;
	if (elem != null) elem.disabled = true;
	return (image instanceof File ? Promise.resolve(image.size) : param > 0 ? Promise.resolve(param)
			: param instanceof Promise ? param : getRemoteFileSize(image)).then(function(size) {
		if (size <= imageSizeLimit * 2**10) return image;
		const haveRhHosts = Array.isArray(imageHosts.rhHostChain) && imageHosts.rhHostChain.length > 0;
		if (!haveRhHosts && !GM_getValue('force_reduce', true)) return Promise.reject('no hosts to upload result');
		return reduceImageSize(image, GM_getValue('image_reduce_maxheight', 2160),
				GM_getValue('image_reduce_jpegquality', 90), typeof param == 'function' ? param : null).then(function(output) {
			if (elem != null) {
				elem.value = output.uri;
				if (image instanceof File) imagePreview(output.uri, output.size);
			}
			Promise.resolve(output.size).then(reducedSize => {
				console.log('cover size reduced by ' + Math.round((size - reducedSize) * 100 / size) +
					'% (' + Math.ceil(size / 2**10) + ' → ' + Math.ceil(reducedSize / 2**10) + ' KiB)');
			});
			return haveRhHosts ? output.uri : (function() {
				let fallbackHost = new Chevereto('imgcdn.dev', 'ImgCDN',
					['jpeg', 'png', 'gif', 'bmp', 'webp'], 30, { sizeLimitAnonymous: 20 });
				if (!fallbackHost.apiKey) fallbackHost.apiKey = '5386e05a3562c7a8f984e73401540836';
				return output.size > fallbackHost.sizeLimit * 2**20 ? Promise.reject('size limit exceeded')
					: fallbackHost.rehost([output.uri]).then(singleImageGetter);
			})().catch(function(reason) {
				console.warn('Upload to ImgCDN fail:', reason);
				return imageHostHandlers['pixhost'].rehost([output.uri]).then(singleImageGetter);
			});
		});
	}).catch(function(reason) {
		logFail('failed to get remote image size or optimize the image: ' + reason + ' (size reduction was not performed)');
		return image;
	}).then(function(finalResult) {
		if (elem != null) {
			if (httpParser.test(finalResult)) {
				if (finalResult != elem.value) elem.value = finalResult;
			} else elem.value = '';
			elem.disabled = false;
		}
		return finalResult;
	});
}

function imageUrlResolver(url, modifiers = { }) {
	return urlResolver(url).then(url => verifyImageUrl(url).catch(function(reason) {
		if (/^HTTP error (\d+)\b/.test(reason) && [
			401, 402, 404, 407, 408, 410, 451,
			502, 503, 504, 511,
		].includes(parseInt(RegExp.$1)) || /\b(?:timeout|timed out)\b/.test(reason)) return Promise.reject(reason);
		const notFound = Promise.reject('No title image for this URL');
		function getFromMeta(root) {
			let meta = root instanceof Document || root instanceof Element ? [
				'meta[property="og:image:secure_url"][content]',
				'meta[property="og:image"][content]',
				'meta[name="og:image"][content]',
				'meta[itemprop="og:image"][content]',
				'meta[itemprop="image"][content]',
			].reduce((elem, selector) => elem || root.querySelector(selector), null) : null;
			return meta != null && httpParser.test(meta.content) ? meta.content : undefined;
		}

		try { url = new URL(url) } catch(e) { return Promise.reject(e) }
		if (url.hostname.endsWith('pinterest.com'))
			return pinterestResolver(url);
		else if (url.hostname.endsWith('free-picload.com')) {
			if (url.pathname.startsWith('/album/')) return imageHostHandlers.picload.galleryResolver(url);
		} else if (url.hostname.endsWith('bandcamp.com')) return globalXHR(url).then(function({document}) {
			let ref = document.querySelector('div#tralbumArt > a.popupImage');
			ref = ref != null ? ref.href : getFromMeta(document);
			return ref ? Promise.resolve(ref.replace(/_\d+(?=\.\w+$)/, '_0')) : notFound;
		}); else if (url.hostname.endsWith('7digital.com') && url.pathname.startsWith('/artist/'))
			return globalXHR(url).then(function({document}) {
				let img = document.querySelector('img[itemprop="image"]');
				return img != null ? img.src : notFound;
			});
		else if (url.hostname.endsWith('geekpic.net')) return globalXHR(url).then(function({document}) {
			let a = document.querySelector('div.img-upload > a.mb');
			return a != null ? a.href : notFound;
		}); else if (url.hostname.endsWith('qq.com') && url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
			let img = document.querySelector('img#albumImg');
			const rx = /\/(T\d+)?(R\d+x\d+)?(M\w+?)(_\d+)?\.(\w+(?:\.\w+)*)(\?.*)?$/;
			return img != null ? verifyImageUrl(img.src.replace(rx, '/$1$3.$5'))
				.catch(() => img.src.replace(rx, '/$1$3$4.$5')).catch(() => img.src) : notFound;
		}); else if (url.hostname.startsWith('books.google.') && url.pathname.startsWith('/books')) return globalXHR(url).then(function({document}) {
			let meta = getFromMeta(document);
			return meta != null ? meta.replace(/\b(?:zoom=1)\b/, 'zoom=0') : notFound;
		}); else switch (url.hostname) {
			// general image hostings
			case 'www.imgur.com': case 'imgur.com': {
				let shareId = /^\/(?:(a)\/)?(\w+)\b/.exec(url.pathname);
				return shareId != null ? imageHostHandlers.imgur.setSession().then(clientId => globalXHR('https://api.imgur.com/post/v1/' + (shareId[1] == 'a' ? 'albums' : 'media') + '/' + shareId[2] + '?' + new URLSearchParams({
					client_id: clientId,
					include: 'media',
				}).toString(), { responseType: 'json' }).then(({response}) => response.media.map(media => media.url))).catch(reason => globalXHR(url, { responseType: 'text' }).then(function({responseText}) {
					let image = /^\s*(?:image)\s*:\s*(\{.+\}),\s*$/m.exec(responseText);
					if (image != null) try {
						return JSON.parse(image[1]).album_images.images.map(image => 'https://i.imgur.com/' + image.hash + image.ext);
					} catch(e) { console.warn(e) }
					return notFound;
				})) : globalXHR(url).then(function({document}) {
					let link = document.querySelector('link[rel="image_src"]');
					return link != null ? link.href : notFound;
				});
			}
			case 'pixhost.to':
				if (url.pathname.startsWith('/gallery/')) return globalXHR(url).then(({document}) =>
					Promise.all(Array.from(document.querySelectorAll('div.images > a')).map(a => imageUrlResolver(a.href, modifiers))));
				if (url.pathname.startsWith('/show/')) return globalXHR(url)
					.then(({document}) => document.querySelector('img#image').src);
				break;
			case 'malzo.com':
				if (url.pathname.startsWith('/al/')) return imageHostHandlers.malzo.galleryResolver(url); else break;
			case 'imgbb.com': case 'ibb.co':
				if (url.pathname.startsWith('/album/')) return imageHostHandlers.imgbb.galleryResolver(url); else break;
			case 'jerking.empornium.ph':
				if (url.pathname.startsWith('/album/')) return imageHostHandlers.jerking.galleryResolver(url); else break;
			case 'imgbox.com':
				if (url.pathname.startsWith('/g/')) return globalXHR(url).then(({document}) =>
					Promise.all(Array.from(document.querySelectorAll('div#gallery-view-content > a'))
						.map(a => imageUrlResolver('https://imgbox.com' + a.pathname, modifiers))));
				break;
			case 'postimage.org': case 'postimg.cc':
				if (!url.pathname.startsWith('/gallery/')) break;
				return PostImage.resultsHandler(url).then(results => results.map(result => result.original));
			case 'www.imagevenue.com': case 'imagevenue.com':
				return globalXHR(url, { headers: { Referer: 'http://www.imagevenue.com/' } }).then(function({document}) {
					let images = Array.from(document.querySelectorAll('div.card img')).map(function(img) {
						return img.src.includes('://cdn-images') ? Promise.resolve(img.src) : imageUrlResolver(img.parentNode.href, modifiers);
					});
					return images.length > 1 ? Promise.all(images) : images.length == 1 ? images[0] : notFound;
				});
			case 'www.imageshack.us': case 'imageshack.us':
				return globalXHR(url).then(({document}) => document.querySelector('a#share-dl').href);
			case 'www.flickr.com': case 'flickr.com':
				if (url.pathname.startsWith('/photos/')) return globalXHR(url).then(function(response) {
					if (/\b(?:modelExport)\s*:\s*(\{.+\}),/.test(response.responseText)) try {
						let urls = JSON.parse(RegExp.$1).main['photo-models'].map(function(photoModel) {
							let sizes = Object.keys(photoModel.sizes).sort((a, b) => photoModel.sizes[b].width *
								photoModel.sizes[b].height - photoModel.sizes[a].width * photoModel.sizes[a].height);
							return sizes.length > 0 ? 'https:'.concat(photoModel.sizes[sizes[0]].url) : null;
						});
						if (urls.length == 1) return urls[0]; else if (urls.length > 1) return urls;
					} catch(e) { console.warn(e) }
					return notFound;
				}); else break;
			case 'photos.google.com':
				return googlePhotosResolver(url);
			case 'www.500px.com': case 'web.500px.com': case '500px.com':
				if (/^\/photo\/(\d+)\b/i.test(url.pathname))
					return _500pxUrlHandler('photos?ids='.concat(RegExp.$1));
				else if (/\/galleries\/([\w\%\-]+)/i.test(url.pathname)) {
					let galleryId = RegExp.$1;
					return globalXHR(url, { rsponseType: 'text' }).then(function(response) {
						if (!/\b(?:App\.CuratorId)\s*=\s*"(\d+)"/.test(response.responseText)) return Promise.reject('Unexpected page structure');
						return _500pxUrlHandler('users/' + RegExp.$1 + '/galleries/' + galleryId + '/items?sort=position&sort_direction=asc&rpp=999');
					});
				} else break;
			case 'www.pxhere.com': case 'pxhere.com':
				if (url.pathname.includes('/photo/')) return globalXHR(url).then(({document}) =>
						JSON.parse(document.querySelector('div.hub-media-content > script[type="application/ld+json"]').text).contentUrl);
					else if (url.pathname.includes('/collection/')) return pxhereCollectionResolver(url);
				break;
			case 'www.unsplash.com': case 'unsplash.com':
				if (url.pathname.startsWith('/photos/')) return globalXHR(url.origin + url.pathname + '/download', { method: 'HEAD' })
						.then(response => response.finalUrl.replace(/\?.*$/, ''));
					else if (url.pathname.includes('/collections/')) return unsplashCollectionResolver(url);
				break;
			case 'www.pexels.com': case 'pexels.com':
				if (url.pathname.startsWith('/photo/')) return globalXHR(url)
						.then(({document}) => document.querySelector('meta[property="og:image"][content]').content.replace(/\?.*$/, ''));
					else if (url.pathname.startsWith('/collections/')) return pexelsCollectionResolver(url);
				break;
			case 'www.piwigo.org': case 'piwigo.org':
				/*if (url.pathname.includes('/picture/')) */return globalXHR(url, { responseType: 'text' }).then(function(response) {
					if (/^(?:RVAS)\s*=\s*(\{[\S\s]+?\})$/m.test(response.responseText)) try {
						let derivatives = eval('(' + RegExp.$1 + ')').derivatives.sort((a, b) => b.w * b.h - a.w * a.h);
						return derivatives.length > 0 ? 'https://piwigo.org/demo/'.concat(derivatives[0].url) : notFound;
					} catch(e) { console.warn(e) }
					return Promise.reject('Unexpected page structure');
				});
			case 'www.freeimages.com': case 'freeimages.com':
				if (url.pathname.startsWith('/photo/')) return globalXHR(url).then(function({document}) {
					let types = Array.from(document.querySelectorAll('ul.download-type > li > span.reso'))
						.sort((a, b) => eval(b.textContent.replace('x', '*')) - eval(a.textContent.replace('x', '*')));
					return types.length > 0 ? url.origin.concat(types[0].parentNode.querySelector('a').pathname) : notFound;
				}); else break;
			case 'redacted.ch':
				if (url.pathname == '/image.php') return globalXHR(url, { method: 'HEAD' }).then(response => response.finalUrl);
					else break;
			case 'demo.cloudimg.io': {
				if (!/\b(https?:\/\/\S+)$/.test(url.pathname.concat(url.search, url.hash))) break;
				let resolved = RegExp.$1;
				if (/\b(?:https?):\/\/(?:\w+\.)*discogs\.com\//i.test(resolved)) break;
				return imageUrlResolver(resolved, modifiers);
			}
			case 'www.pimpandhost.com': case 'pimpandhost.com':
				if (url.pathname.startsWith('/image/')) return globalXHR(url).then(function(response) {
					let elem = resopnse.document.querySelector('div.main-image-wrapper');
					if (elem != null && elem.dataset.src) return 'https:'.concat(elem.dataset.src);
					elem = resopnse.document.querySelector('div.img-wrapper > a > img');
					return elem != null ? 'https:'.concat(elem.src) : notFound;
				}); else break;
			case 'www.screencast.com': case 'screencast.com':
				return globalXHR(url).then(function({document}) {
					let ref = document.querySelectorAll('ul#containerContent > li a.media-link');
					if (ref.length <= 0) return getFromMeta(document) || notFound;
					return Promise.all(Array.from(ref).map(a => imageUrlResolver('https://www.screencast.com' + a.href, modifiers)));
				});
			case 'abload.de':
				if (url.pathname.startsWith('/image.php')) return globalXHR(url).then(function({document}) {
					let elem = document.querySelector('img#image');
					if (elem == null) return notFound;
					let src = new URL(elem.src);
					return imageHostHandlers.abload.origin + src.pathname + src.search;
				}); else break;
			case 'fastpic.ru':
				if (url.pathname.startsWith('/view/'))
					return globalXHR(url).then(({document}) => imageUrlResolver(document.querySelector('a.img-a').href, modifiers));
				else if (url.pathname.startsWith('/fullview/')) return globalXHR(url).then(function(response) {
					let node = response.document.getElementById('image');
					if (node != null) return node.src;
					return /\bvar\s+loading_img\s*=\s*'(\S+?)';/.test(response.responseText) ? RegExp.$1 : notFound;
				}); else break;
			case 'www.radikal.ru': case 'radikal.ru': case 'a.radikal.ru':
				return globalXHR(url).then(({document}) => document.querySelector('div.mainBlock img').src);
			case 'imageban.ru': case 'ibn.im':
				return globalXHR(url).then(({document}) => document.querySelector('a[download]').href);
			case 'svgshare.com':
				return globalXHR(url).then(function({document}) {
					let link;
					document.querySelectorAll('ul#shares > li > input[type="text"]')
						.forEach(input => { if (!link && /^(?:https?:\/\/.+\.svg)$/.test(input.value)) link = input.value; });
					return link || notFound;
				});
			case 'slow.pics':
				if (url.pathname.startsWith('/c/')) return globalXHR(url).then(function({document}) {
					let nodes = document.querySelectorAll('img.card-img-top');
					if (nodes.length > 1) return Array.from(nodes).map(img => img.src);
						else if (nodes.length > 0) return nodes[0].src;
					nodes = document.querySelectorAll('a#comparisons + div.dropdown-menu > a.dropdown-item');
					if (nodes.length > 0) return Promise.all(Array.from(nodes).map(a => globalXHR(url.origin + a.pathname)
						.then(({document}) => Array.from(document.querySelectorAll('div#preload-images > img')).map(img => img.src))));
					return notFound;
				}); else break;
			case 'www.amazon.com': case 'amazon.com':
			case 'www.amazon.ae': case 'www.amazon.com.au': case 'www.amazon.com.br': case 'www.amazon.ca':
			case 'www.amazon.cn': case 'www.amazon.de': case 'www.amazon.es': case 'www.amazon.fr':
			case 'www.amazon.co.uk': case 'www.amazon.in': case 'www.amazon.it': case 'www.amazon.co.jp':
			case 'www.amazon.com.mx': case 'www.amazon.nl': case 'www.amazon.sa': case 'www.amazon.se':
			case 'www.amazon.sg': case 'www.amazon.com.tr':
				return globalXHR(url).then(function(response) {
					const rx = /\._\S+?_(?=\.)/,
								getImgOrigin = colorImage => (colorImage.hiRes || colorImage.large || colorImage.thumb).replace(rx, '');
					let obj = /^\s*(?:var\s+obj\s*=\s*jQuery\.parseJSON)\('(\{.+\})'\);/m.exec(response.responseText);
					if (obj != null) {
						try { obj = JSON.parse(obj[1]) } catch(e) { try { obj = eval('(' + obj[1] + ')') } catch(e) { obj = { } } }
						let variants = Object.keys(obj.colorImages);
						if (variants.length > 0) return variants.map(key => obj.colorImages[key].map(getImgOrigin));
					}
					let colorImages = /^\s*'colorImages':\s*(\{.+\}),?$/m.exec(response.responseText);
					if (colorImages != null) {
						try { colorImages = JSON.parse(colorImages[1].replace(/'/g, '"')) }
						catch(e) { try { colorImages = eval('(' + colorImages[1] + ')') } catch(e) { colorImages = { } } }
						if (Array.isArray(colorImages.initial) && colorImages.initial.length > 0)
							return colorImages.initial.map(getImgOrigin);
					}
					let img = ['div#ppd-left img', 'img#igImage', 'img#imgBlkFront']
						.reduce((acc, sel) => acc || response.document.querySelector(sel), null);
					if (img == null) return notFound;
					if (img.dataset.aDynamicImage) try {
						let imgUrl = Object.keys(JSON.parse(img.dataset.aDynamicImage))[0];
						if (httpParser.test(imgUrl)) return imgUrl.replace(rx, '');
					} catch(e) { }
					return httpParser.test(img.src) ? img.src.replace(rx, '') : notFound;
				});
			case 'www.casimages.com': case 'casimages.com':
				if (url.pathname.startsWith('/i/')) return globalXHR(url).then(function({document}) {
					let elem = document.querySelector('div.logo > a');
					if (elem != null) return elem.href;
					elem = document.querySelector('div.logo img');
					return elem != null ? elem.src : notFound;
				}); else break;
			case 'www.getapic.me': case 'getapic.me':
				return globalXHR(url, { responseType: 'json' }).then(function({response}) {
					if (!response.result.success) return Promise.reject(response.result.errors);
					if (Array.isArray(response.result.data.images))
						return response.result.data.images.map(image => image.url);
					return response.result.data.image ? response.result.data.image.url : notFound;
				});
			case 'sm.ms':
				if (url.pathname.startsWith('/image/')) return globalXHR(url).then(function({document}) {
					let img = document.querySelector('img.image');
					return img != null ? img.src || img.parentElement.href : notFound;
				}); else break;
			case 'www.kizunaai.com': case 'kizunaai.com':
				//if (!url.pathname.includes('/music/')) break;
				return globalXHR(url).then(function({document}) {
					let img = document.querySelector('div.post-body span > img');
					return img != null ? img.src.replace(/-\d+x\d+(?=\.\w+$)/, '') : notFound;
				});
			case 'play.google.com':
				if (url.pathname.startsWith('/store/')) return globalXHR(url).then(function({document}) {
					let meta = getFromMeta(document);
					return meta != null ? meta.replace(/(?:=[swh]\d+.*)?$/, '=s0') : notFound;
				}); else break;
			// music-related
			case 'www.discogs.com': case 'discogs.com':
				return globalXHR(url).then(({document}) => (function() {
					if (url.pathname.includes('/master/')) return Promise.reject('this is master');
					if (modifiers.ctrlKey) return Promise.reject('master release inquiry avoided (force release gallery)');
					let master = document.getElementById('all-versions-link');
					if (master == null) return Promise.reject('no master release for this page');
					return imageUrlResolver(discogsOrigin + master.pathname, modifiers);
				})().catch(function(reason) {
					let elem = document.querySelector('div.image_gallery, div.image_gallery_large');
					if (elem != null) try {
						elem = JSON.parse(elem.dataset.images).map(image => image.full || image.thumb)
							.filter(RegExp.prototype.test.bind(httpParser));
						if (elem.length <= 0) throw 'empty imagem list';
						return Promise.all(elem.map(getDiscogsImageMax)).catch(function(reason) {
							console.error('One of getDiscogsImageMax workers rejected:', reason, elem);
							return elem;
						});
					} catch(e) { console.warn('Invalid Discogs image gallery:', elem, '(' + e + ')') } else {
						console.warn('Missing Discogs image gallery record for', url.href);
					}
					return (elem = getFromMeta(document)) ? getDiscogsImageMax(elem) : notFound;
				}));
			case 'www.musicbrainz.org': case 'beta.musicbrainz.org': case 'musicbrainz.org':
				if (url.pathname.startsWith('/release/')) {
					if (/^\/release\/([\w\-]+)(?=\/|$)/i.test(url.pathname)) url.pathname = '/release/' + RegExp.$1 + '/cover-art';
						else console.warn('Unexpected MusicBrainz release url path:', url.pathname);
				} else if (!url.pathname.startsWith('/release-group/')) break;
				return globalXHR(url).then(({document}) => (function() {
					if (url.pathname.startsWith('/release-group/')) return Promise.reject('this is release group');
					if (modifiers.ctrlKey) return Promise.reject('release group inquiry avoided (force release gallery)');
					let releaseGroup = document.querySelector('p.subheader > span.small > a');
					if (releaseGroup == null) return Promise.reject('no release group for this page');
					return imageUrlResolver('https://musicbrainz.org' + releaseGroup.pathname, modifiers);
				})().catch(function(reason) {
					let elem = document.querySelector('head > script[type="application/ld+json"]');
					if (elem != null) try {
						if (Array.isArray(elem = JSON.parse(elem.text).image)) {
							if (elem.length > 0) return elem.map(image => 'https:' + image.contentUrl);
						} else if (elem && elem.contentUrl) return 'https:' + elem.contentUrl;
					} catch(e) { console.warn('MusicBrainz: invalid meta record', elem) }
					elem = document.querySelectorAll('div#content > div.artwork-cont span.cover-art-image > img');
					if (elem.length > 0) return Array.from(elem).map(img => img.src.replace(/-\d+(?=(?:\.\w+)+$)/, ''));
					return (elem = document.querySelector('a.artwork-image')) != null ? elem.href
						: (elem = document.querySelector('div.cover-art > img')) != null ? elem.src : notFound;
				}));
			case 'www.allmusic.com': case 'allmusic.com':
				if (url.pathname.startsWith('/album/')) return globalXHR(url).then(function({document}) {
					const imageMax = [/\b(?:f)=(\d+)\b/i, 'f=0'];
					function amImgsXtractor(dom) {
						if (dom instanceof Document) try {
							//eval(dom.querySelector('div[class$="-cover"] script').text);
							let imageGallery = JSON.parse(/(\[.+\]);/.exec(dom.querySelector('div[class$="-cover"] script').text)[1]);
							if (imageGallery.length <= 0) throw 'empty gallery';
							return imageGallery.map(function(image) {
								image = image.zoomURL || image.url;
								return image && !image.includes('/images/no_image/album') ? image.replace(...imageMax) : null;
							}).filter(Boolean);
						} catch(e) {
							let img = dom.querySelector('div[class$="-cover"] img');
							if (img != null && !(img = img.dataset.largeurl || img.src).includes('/images/no_image/album'))
								return img.replace(...imageMax);
						}
						return notFound;
					}
					return (function() {
						const mainAlbum = document.querySelector('section.main-album a.album-title');
						if (mainAlbum == null) return Promise.reject('no main album');
						return globalXHR(mainAlbum.href).then(({document}) => amImgsXtractor(document));
					})().catch(reason => amImgsXtractor(document));
				}); else if (url.pathname.startsWith('/artist/')) return globalXHR(url).then(function({document}) {
					const imgMax = /\b(?:f)=(\d+)\b/i, imageMax = imgUrl => verifyImageUrl(imgUrl.replace(imgMax, 'f=6'))
						.catch(() => verifyImageUrl(imgUrl.replace(imgMax, 'f=0')))
						.catch(() => verifyImageUrl(imgUrl.replace(imgMax, 'f=5')));
					try {
						//eval(document.querySelector('div.sidebar > script').text);
						let imageGallery = JSON.parse(/(\[.+\]);/.exec(document.querySelector('div.sidebar > script').text)[1]);
						if (imageGallery.length <= 0) throw 'empty gallery';
						return Promise.all(imageGallery.map(image => imageMax(image.zoomURL || image.url)));
					} catch(e) {
						let img = document.querySelector('div.sidebar > div.artist-image img');
						if (img != null) return imageMax(img.dataset.largeurl || img.src);
					}
					return notFound;
				}); else break;
			case 'music.apple.com': case 'itunes.apple.com': {
				let appleId = amEntityParser.exec(url);
				if (appleId != null) return (function() {
					if ('appleMusicDesktopConfig' in sessionStorage) try {
						return Promise.resolve(JSON.parse(sessionStorage.appleMusicDesktopConfig));
					} catch(e) { console.warn('Apple Music invalid cached desktop config:', e) }
					return globalXHR(url).then(function({document}) {
						let environment = document.querySelector('meta[name="desktop-music-app/config/environment"][content]');
						if (environment != null) environment = JSON.parse(decodeURIComponent(environment.content));
							else return Promise.reject('Apple desktop environment missing');
						if (!environment.MEDIA_API.token) {
							console.warn('Apple Music received invalid desktop config:', environment);
							return  Promise.reject('Apple API token missing')
						}
						sessionStorage.appleMusicDesktopConfig = JSON.stringify(environment);
						return environment;
					});
				})().then(environment => globalXHR(environment.MUSIC.BASE_URL + '/catalog/us/' + appleId[1] + 's/' + parseInt(appleId[2]), {
					responseType: 'json',
					headers: { 'Referer': url, 'Authorization': 'Bearer ' + environment.MEDIA_API.token },
				})).then(function({response}) {
					const artwork = response.data[0].attributes.artwork;
					return artwork ? artwork.url.replace('{w}', artwork.width).replace('{h}', artwork.height) : notFound;
				}); else break;
			}
			case 'www.deezer.com': case 'deezer.com':
				if (dzrEntityParser.test(url)) return verifyImageUrl('https://api.deezer.com/' + RegExp.$1 + '/' + RegExp.$2 + '/image').catch(function(reason) {
					console.warn('Deezer API image retrieval failed:', reason, url);
					return globalXHR(url).then(({document}) => getFromMeta(document) || notFound);
				}).then(imageUrl => !modifiers.ctrlKey ? getDeezerImageMax(imageUrl)
					: verifyImageUrl(imageUrl.replace(...dzrImageMax)).catch(reason => imageUrl)); else break;
			case 'www.qobuz.com': case 'qobuz.com':
				if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
					let img = document.querySelector('div.album-cover > img');
					if (img == null) return getFromMeta(document) || notFound;
					return verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_org'))
						.catch(reason => verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_max')))
						.catch(reason => img.src);
				}); else break;
			case 'www.boomkat.com': case 'boomkat.com':
				if (url.pathname.startsWith('/products/')) return globalXHR(url).then(function({document}) {
					let img = document.querySelector('img[itemprop="image"]');
					if (img == null) return notFound;
					return verifyImageUrl(img.src.replace(/\/large\//i, '/original/')).catch(reason => img.src);
				}); else break;
			case 'www.bleep.com': case 'bleep.com':
				if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function({document}) {
					let meta = getFromMeta(document);
					return meta ? verifyImageUrl(meta.replace(/\/r\/[a-z]\//i, '/r/')).catch(reason => meta) : notFound;
				}); else break;
			case 'www.soundcloud.com': case 'soundcloud.com':
				return globalXHR(url).then(function({document}) {
					let meta = getFromMeta(document);
					return meta ? verifyImageUrl(meta.replace(/\b(?:t\d+x\d+)(?=\.\w+$)/, 'original')).catch(reason => meta) : notFound;
				});
			case 'www.prestomusic.com': case 'prestomusic.com':
				if (url.pathname.includes('/products/')) return globalXHR(url).then(({document}) =>
					verifyImageUrl(document.querySelector('div.c-product-block__aside > a').href.replace(/\?\d+$/))); else break;
			case 'www.bontonland.cz':case 'bontonland.cz':
				return globalXHR(url).then(({document}) => document.querySelector('a.detailzoom').href);
			case 'www.prostudiomasters.com': case 'prostudiomasters.com':
				if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
					let a = document.querySelector('img.album-art');
					return verifyImageUrl(a.currentSrc).catch(reason => a.src);
				}); else break;
			case 'www.e-onkyo.com': case 'e-onkyo.com':
				if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
					let meta = getFromMeta(document);
					return meta ? meta.replace(/\/s\d+\//, '/s0/') : notFound;
				}); else break;
			case 'store.acousticsounds.com':
				return globalXHR(url).then(function({document}) {
					let link = document.querySelector('div#detail > link[rel="image_src"]');
					return verifyImageUrl(link.href.replace(/\/medium\//i, '/xlarge/')).catch(reason => link.href);
				});
			case 'www.indies.eu': case 'indies.eu':
				if (url.pathname.includes('/alba/')) return globalXHR(url)
					.then(({document}) => verifyImageUrl(document.querySelector('div.obrazekDetail > img').src)); else break;
			case 'www.beatport.com': case 'classic.beatport.com': case 'pro.beatport.com': case 'beatport.com':
				if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function({document}) {
					let elem = getFromMeta(document);
					if (!elem && (elem = document.body.querySelector('div > img.interior-release-chart-artwork')) != null)
						elem = elem.src;
					if (!elem && (elem = document.body.querySelector('div.artwork')) != null && elem.dataset.modalArtwork) // BP Classic
						elem = 'https:' + elem.dataset.modalArtwork;
					return elem || notFound;
				}).then(imgUrl => imgUrl.replace(/\/image_size\/\d+x\d+\//i, '/image/')); else break;
			case 'www.beatsource.com': case 'beatsource.com':
				if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function({document}) {
					let imgUrl = getFromMeta(document);
					return imgUrl ? imgUrl.replace(/\/image_size\/\d+x\d+\//i, '/') : notFound;
				}); else break;
			case 'www.supraphonline.cz': case 'supraphonline.cz':
				if (!url.pathname.includes('/album/')) break;
				return globalXHR(url).then(function({document}) {
					let imageUrl = document.querySelector('div.sidebar div.sexycover > div.btn-group > button:last-of-type');
					if (imageUrl != null && /^(?:coverzoom):(\S+)\$$/.test(imageUrl.dataset.plugin)
							&& (imageUrl = imageUrl.parentNode.querySelector('script[type="data-plugin/' + RegExp.$1 + '"]')) != null)
						return 'https://www.supraphonline.cz' + eval(imageUrl.text);
					return (imageUrl = getFromMeta(document)) ? imageUrl.replace(/\?.*$/, '') : notFound;
				});
			case 'vgmdb.net':
				if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
					let div = document.querySelector('div#coverart');
					return verifyImageUrl(/\b(?:url)\s*\(\"(.*)"\)/i.test(div.style['background-image']) && RegExp.$1).catch(reason => notFound);
				}); else break;
			case 'www.ototoy.jp': case 'ototoy.jp':
				return globalXHR(url).then(function({document}) {
					let img = document.querySelector('div#jacket-full-wrapper > img'); // img[alt="album jacket"]
					return img != null ? img.dataset.src || img.src : notFound;
				});
			case 'music.yandex.ru':
				if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
					let script = document.querySelector('script.light-data');
					return verifyImageUrl(JSON.parse(script.text).image).catch(reason => notFound);
				}); else break;
			case 'www.pias.com': case 'store.pias.com': case 'pias.com':
				return globalXHR(url).then(function({document}) {
					let node = getFromMeta(document);
					if (node) return verifyImage(node.replace(/\/[sbl]\//i, '/')).catch(reason => node);
					node = document.querySelector('img[itemprop="image"]');
					return node != null ? verifyImage(node.src.replace(/\/[sbl]\//i, '/')).catch(reason => node.src) : notFound;
				});
			case 'www.eclassical.com': case 'eclassical.com':
				return globalXHR(url).then(function({document}) {
					let a = document.querySelector('div#articleImage > a');
					return a != null ? a.href : notFound;
				});
			case 'www.hdtracks.com': case 'hdtracks.com':
				if (!/\/album\/(\w+)\b/.test(url)) break;
				return fetch('https://hdtracks.azurewebsites.net/api/v1/album/' + RegExp.$1).then(response => response.json())
					.then(result => result.status.toLowerCase() == 'ok' ? result.cover : Promise.reject(result.status));
			case 'www.muziekweb.nl': case 'muziekweb.nl':
				if (/\/Link\/(\w+)\b/i.test(url)) return globalXHR(url).then(function({document}) {
					let meta = getFromMeta(document);
					return meta ? meta.replace(/\/COVER\/\w+\b/i, '/COVER/SUPERLARGE') : notFound;
				}); else break;
			case 'www.deejay.de': case 'deejay.de':
				return globalXHR(url).then(function({document}) {
					let elem = document.querySelector('div#gallery > a') || document.querySelector('div.cover a');
					if (elem != null) return 'https://www.deejay.de' + elem.pathname;
					return (elem = getFromMeta(document)) ? elem : notFound;
				}).then(imgUrl => verifyImageUrl(imgUrl.replace(/\/images\/\w+\//i, '/images/xxl/')).catch(() => imgUrl));
			case 'music.163.com':
				if (!/\/album.*\b(?:id)=(\d+)\b/i.test(url.href)) break;
				return globalXHR('https://music.163.com/api/album/' + RegExp.$1, { responseType: 'json' })
					.then(({response}) => response.album.picUrl ?
						response.album.picUrl.replace(/\?.*$/, '').replace(/\b(?:p[123])(?=\.music\.\d+\.net\b)/i, 'p4') : notFound);
			case 'www.tidal.com': case 'tidal.com':
				if (!(/\/album\/(\d+)(?:\/|$)/i.test(url.pathname) && !/\b(?:albumId)=(\d+)\b/i.test(url.search))) break;
				return globalXHR('https://api.tidal.com/v1/albums/' + RegExp.$1 + '?' + new URLSearchParams({
					locale: 'en_US',
					countryCode: 'US',
					deviceType: 'BROWSER',
					token: tidalClientId,
				}), { responseType: 'json' }).then(({response}) => response.cover ?
					'https://resources.tidal.com/images/' + response.cover.replace(/-/g, '/') + '/1280x1280.jpg' : notFound);
			case 'www.extrememusic.com': case 'extrememusic.com':
				if (url.pathname.startsWith('/albums/')) return globalXHR(url).then(function({document}) {
					let meta = getFromMeta(document);
					return meta ? meta.replace(/\/album\/\w+\//i, '/album/600/') : notFound;
				}); else break;
			case 'www.recochoku.jp': case 'recochoku.jp':
				if (url.pathname.startsWith('/album/')) return globalXHR(url).then(function({document}) {
					let imgUrl = getFromMeta(document);
					if (!imgUrl) return notFound;
					imgUrl = new URL(imgUrl);
					let params = new URLSearchParams(imgUrl.search);
					params.set('FFw', 999999999); params.set('FFh', 999999999);
					params.delete('h'); params.delete('option');
					imgUrl.search = params;
					return imgUrl;
				}); else break;
			case 'www.elusivedisc.com': case 'elusivedisc.com':
				return globalXHR(url).then(function({document}) {
					let img = document.querySelector('figure > img.zoomImg');
					if (img != null) return img.src;
					img = document.querySelector('section.productView-images > figure');
					return img != null && img.dataset.zoomImage || notFound;
				});
			case 'music.youtube.com':
				return globalXHR(url).then(function({document}) {
					for (let script of document.querySelectorAll('body > script[nonce]')) {
						let data = /\b(?:initialData\.push)\s*\(\s*\{\s*(?:path):\s*('\\\/browse'),\s*(?:params):\s*(.+?)\s*,\s*(?:data):\s*('.+?')\s*\}\s*\);/.exec(script.text);
						if (data != null) try {
							const imgMax = [/(?:=[swh]\d+.*)?$/, '=s0'];
							data = JSON.parse(eval(data[3]));
							if ('frameworkUpdates' in data) try {
								data = data.frameworkUpdates.entityBatchUpdate.mutations
									.find(mutation => mutation.payload && 'musicAlbumRelease' in mutation.payload);
								if (data != undefined && 'thumbnailDetails' in data.payload.musicAlbumRelease)
									return data.payload.musicAlbumRelease.thumbnailDetails.thumbnails[0].url.replace(...imgMax);
							} catch(e) { console.warn(e) }
							if ('header' in data) try {
								data = data.header.musicImmersiveHeaderRenderer.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails;
								if (data) return data[0].url.replace(...imgMax);
							} catch(e) { console.warn(e) }
						} catch(e) { console.warn(e) }
					}
					return notFound;
				});
			case 'www.kuwo.cn': case 'kuwo.cn':
				if (url.pathname.startsWith('/album_detail/')) return globalXHR(url).then(function({document}) {
					for (let script of document.querySelectorAll('body > script')) {
						if (!/\b(?:__NUXT__)\b/.test(script.text)) continue;
						if (/\b(?:pic):"(.+?)"/.test(script.text))
							return eval('"' + RegExp.$1 + '"').replace(/(\/albumcover)\/\d+\//i, '$1/0/');
					}
					notFound;
				}); else break;
			case 'www.melon.com': case 'melon.com':
				/*if (url.pathname.startsWith('/album/')) */return globalXHR(url).then(function({document}) {
					let imgUrl = getFromMeta(document);
					if (imgUrl) imgUrl = imgUrl.replace(/\?.*$/, ''); else return notFound;
					return verifyImageUrl(imgUrl.replace(/(?:_\d+)?(?=\.\w+$)/, '_1000')).catch(reason => imgUrl);
				});// else break;
			case 'music.bugs.co.kr':
				/*if (url.pathname.startsWith('/album/')) */return globalXHR(url).then(function({document}) {
					let imgUrl = getFromMeta(document);
					return imgUrl ? imgUrl.replace(/(\/album\/images)\/\w+\//i, '$1/original/') : notFound;
				}); //else break;
			// books-related
			case 'www.goodreads.com': case 'goodreads.com':
				if (url.pathname.includes('/show/')) return globalXHR(url).then(function({document}) {
					let img = document.querySelector('div.editionCover > img') || document.querySelector('img#coverImage');
					img = img != null ? img.src : getFromMeta(document);
					return img && !img.includes('/nophoto/book/') ? img.replace(/\._\w+_\./g, '.').replace(/\?.*$/, '') : notFound;
				}); else break;
			case 'www.databazeknih.cz': case 'databazeknih.cz':
				if (url.pathname.startsWith('/knihy/')) return globalXHR(url).then(function({document}) {
					let elem = document.querySelector('div#icover_mid > a');
					if (elem != null) return imageUrlResolver('https://www.databazeknih.cz' + elem.pathname, modifiers);
					const imageMax = imageUrl => httpParser.test(imageUrl) ? verifyImageUrl([
						[/\/\d+\/([a-z]+)(?=_)/, 'big'], [/\?.*$/, ''],
					].reduce((acc, def) => acc.replace(...def), imageUrl)).catch(reason => imageUrl) : Promise.reject('invalid url');
					if ((elem = document.querySelector('div#lbImage')) != null
							&& (elem = /\b(?:url)\("(.*)"\)/i.exec(elem.style.backgroundImage)) != null) return imageMax(elem[1]);
					return (elem = document.querySelector('img.kniha_img')) != null ? imageMax(elem.src) : notFound;
				}); else if (url.pathname.startsWith('/obalka-knihy/')) return globalXHR(url).then(function({document}) {
					let elem = document.querySelector('img.book_cover_big');
					return elem != null ? elem.src.replace(/\?.*/, '') : notFound;
				}); else break;
			case 'www.alza.cz': case 'alza.cz': case 'www.alza.sk': case 'alza.sk':
				return globalXHR(url).then(function({document}) {
					const imageMax = imgSrc => imgSrc.replace(/([\?\&])fd=(?:f\d+)\b\&?/i, '$1');
					let meta = document.querySelectorAll('div#galleryPreview a.lightBoxImage');
					if (meta.length > 0) return Array.from(meta)
						.map(a => imageMax(a.dataset.original || a.href || a.dataset.bigimage));
					meta = document.querySelector('div.detail-page > script[type="application/ld+json"]');
					if (meta != null) try { meta = JSON.parse(meta.text) } catch(e) { meta = null }
					if (meta != null && httpParser.test(meta.image)) return imageMax(meta.image);
					return (meta = getFromMeta(document)) ? imageMax(meta) : notFound;
				});
			// movie-related
			case 'www.imdb.com': case 'imdb.com':
				if (!['title/tt', 'name/nm'].some(cat => url.pathname.startsWith('/' + cat))) break;
				return globalXHR(url).then(function(response) {
					const galleryDetector = /\/mediaindex(?:[\/\?].*)?$/i, imgStripper = /\._V\d+_[\w\,]*(?=\.)/;
					if (!galleryDetector.test(response.finalUrl)) {
						let node = response.document.head.querySelector(':scope > script[type="application/ld+json"]');
						if (node != null) try {
							let image = JSON.parse(node.text).image;
							if (typeof image == 'string') return verifyImageUrl(image.replace(imgStripper, '')).catch(reason => notFound);
						} catch(e) { console.warn(e) }
						node = response.document.querySelector('meta[property="og:image"][content]');
						return node != null && !/\/imdb\w*_logo\./i.test(node.content) ?
							node.content.replace(imgStripper, '') : notFound;
					}
					var titleId = /\/title\/(tt\d+)\//i.test(response.finalUrl) && RegExp.$1;
					return titleId ? globalXHR(response.finalUrl.replace(galleryDetector, '/mediaviewer'), { responseType: 'text' }).then(function(response) {
						if (/\b(?:window\.IMDbMediaViewerInitialState)\s*=\s*(\{.*\});/.test(response.responseText)) try {
							let allImages = eval('(' + RegExp.$1 + ')').mediaviewer.galleries[titleId].allImages;
							if (allImages.length > 0) return allImages.map(image => image.src.replace(imgStripper, ''));
						} catch(e) { console.warn(e) }
						return notFound;
					}) : Promise.reject('title id not found');
				});
			case 'www.themoviedb.org': case 'themoviedb.org':
				if (!['movie', 'person'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
				return globalXHR(url).then(function({document}) {
					let node = document.querySelector('meta[property="og:image"][content]');
					return verifyImageUrl(node.content.replace(/\/p\/\w+\//i, '/p/original/')).catch(function(reason) {
						node = document.querySelector('div.image_content > img');
						return verifyImageUrl(node.dataset.src.replace(/\/p\/\w+\//i, '/p/original/'))
							.catch(reason => verifyImageUrl(node.src.replace(/\/p\/\w+\//i, '/p/original/')))
							.catch(reason => verifyImageUrl(dataset.src)).catch(reason => node.src);
					}).catch(reason => notFound);
				});
			case 'www.omdb.org': case 'omdb.org':
				if (!['movie', 'person'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
				return globalXHR(url).then(function({document}) {
					let node = document.querySelector('meta[property="og:image"][content]');
					return node != null ? verifyImageUrl(node.content) : notFound;
				});
			case 'www.thetvdb.com': case 'thetvdb.com':
				if (!['movies', 'series', 'people'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
				return globalXHR(url).then(({document}) => verifyImageUrl(document.querySelector('img.img-responsive').src));
			case 'www.rottentomatoes.com': case 'rottentomatoes.com':
				if (!['m', 'celebrity', 'tv'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
				return globalXHR(url).then(function({document}) {
					//if (/\b(?:context\.shell)\s*=\s*(\{.+?});/.test(response.responseText)) try {
					//	return JSON.parse(RegExp.$1).header.certifiedMedia.certifiedFreshMovieInTheater4.media.posterImg;
					//} catch(e) { console.warn(e) }
					return verifyImageUrl(document.querySelector('meta[property="og:image"]').content);
				});
			case 'www.bcdb.com': case 'bcdb.com':
				if (!['cartoon'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
				return globalXHR(url).then(({document}) =>
					verifyImageUrl(document.location.protocol.concat(document.querySelector('meta[property="og:image"]').content)));
			case 'www.boxofficemojo.com': case 'boxofficemojo.com':
				if (!['releasegroup'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
				return globalXHR(url).then(({document}) => verifyImageUrl(document.querySelector('div.mojo-primary-image img').src));
			case 'www.metacritic.com': case 'metacritic.com':
				return globalXHR(url).then(function({document}) {
					let image = document.querySelector('meta[property="og:image"]').content;
					return verifyImageUrl(image.replace(/-\d+h(?=(?:\.\w+)?$)/, '')).catch(reason => image);
				});
			case 'www.csfd.cz': case 'csfd.cz':
				if (!['film', 'tvurce'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
				return globalXHR(url).then(function(response) {
					const gallerySel = 'div.ct-general.photos > div.content > ul > li > div.photo';
					if (response.document.querySelectorAll(gallerySel).length > 0) return new Promise(function(resolve, reject) {
						let urls = [], origin = new URL(response.finalUrl).origin;
						loadPage(response.finalUrl.replace(/\/strana-\d+(?=$|\/|\?)/, ''));

						function loadPage(url) {
							GM_xmlhttpRequest({ method: 'GET', url: url,
								onload: function(response) {
									if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
									let dom = domParser.parseFromString(response.responseText, 'text/html');
									Array.prototype.push.apply(urls, Array.from(dom.querySelectorAll(gallerySel))
										.map(div => /^(?:url)\s*\("?(.+?)"?\)$/i.test(div.style.backgroundImage) ?
											'https:'.concat(RegExp.$1).replace(/\?.*$/, '') : null));
									let nextPage = dom.querySelector('div.paginator > a.next[href]');
									if (nextPage != null) loadPage(origin.concat(nextPage.pathname, nextPage.search)); else resolve(urls);
								},
								onerror: response => { reject(defaultErrorHandler(response)) },
								ontimeout: response => { reject(defaultTimeoutHandler(response)) },
							});
						}
					});
					let img = ['img.film-poster', 'img.creator-photo', 'div.image > img']
						.reduce((acc, selector) => acc || response.document.querySelector(selector), null);
					return img != null ? verifyImageUrl(img.src.replace(/\?.*$/, '')) : notFound;
				});
			case 'www.fdb.cz': case 'fdb.cz':
				//if (!url.pathname.startsWith('/film/')) break;
				return globalXHR(url).then(function({document}) {
					let a = document.querySelector('a.boxPlakaty');
					if (a == null) return Promise.reject('Invalid page structure');
					a.hostname = 'www.fdb.cz';
					return globalXHR(a.href).then(function({document}) {
						let imgs = document.querySelectorAll('span#popup_plakaty > img');
						return imgs.length > 0 ? verifyImageUrl(imgs[0].src) : notFound;
					});
				});
			case 'www.caps-a-holic.com': case 'caps-a-holic.com':
				if (url.pathname == '/c.php') return globalXHR(url).then(function(response) {
					function heightExtractor(n) {
						let node = response.document.querySelector('div.main > div.c_table > div[style]:nth-of-type(' + n + ')');
						if (node != null && /\b(\d{3,})\s?[x×]\s?(\d{3,})\b/.test(node.textContent)) return parseInt(RegExp.$2);
						console.warn(response.finalUrl, 'failed to get resolution (' + n + ')', node);
						return null;
					}
					const baseUrl = 'https://caps-a-holic.com/c_image.php?a=0&x=0&y=0&l=1';
					let result = Array.from(response.document.querySelectorAll('div.main > div[style] > a > img.thumb')).map(function(img) {
						let query = new URLSearchParams(new URL(img.parentNode.href).search);
						return [
							`${baseUrl}&s=${parseInt(query.get('s1'))}&max_height=${heightExtractor(2)}`,
							`${baseUrl}&s=${parseInt(query.get('s2'))}&max_height=${heightExtractor(3)}`,
						];
					});
					result.caption = Array.from(response.document.querySelectorAll('body > div.bdinfo > div.blue_bar:first-of-type')).map(function(div) {
						let caption = div.childNodes[0].textContent.trim();
						if (div.childNodes.length > 1) caption += ' (' + div.childNodes[1].textContent.trim() + ')';
						return caption;
					});
					return result;
				}); else break;
			case 'www.screenshotcomparison.com': case 'screenshotcomparison.com':
				if (url.pathname.startsWith('/comparison/')) return globalXHR(url).then(function(response) {
					const origin = new URL(response.finalUrl).origin;
					return Array.from(response.document.querySelectorAll('div#img_nav li > a')).map(function(a) {
						return globalXHR(origin.concat(a.pathname), { responseType: 'text' }).then(response => [
							/\b(?:images)\[1\]='(\S+?)'/.test(response.responseText) && RegExp.$1,
							/\b(?:images)\[0\]='(\S+?)'/.test(response.responseText) && RegExp.$1,
						].map(src => origin.concat(src)));
					});
				}); else break;
			case 'www.dvdbeaver.com': case 'dvdbeaver.com':
				if (url.pathname.startsWith('/film')) return globalXHR(url).then(function(response) {
					const origin = new URL(response.finalUrl).origin;
					return Array.from(response.document.querySelectorAll('div[align="center"] > table > tbody > tr > td > a[target="_blank"] > img'))
						.map(img => origin.concat(img.parentNode.pathname));
				}); else break;
		}
		return globalXHR(url, { headers: { 'Referer': url.origin } }).then(function({document}) {
			if (url.pathname.startsWith('/album/')
					&& document.querySelector('div#tabbed-content-group > div.content-listing > div.pad-content-listing') != null)
				return new Chevereto(url.hostname).galleryResolver(url);
			let elem = document.querySelector('head > meta[name="generator"][content]');
			if (elem != null && elem.content.toLowerCase() == 'bandcamp') {
				elem = document.querySelector('div#tralbumArt > a.popupImage');
				elem = elem != null ? elem.href : getFromMeta(document);
				return httpParser.test(elem) ? elem.replace(/_\d+(?=\.\w+$)/, '_0') : notFound;
			}
			return getFromMeta(document) || notFound;
		});
	}));
}

function logFail(message) {
	let log = document.getElementById('ihh-console');
	if (log == null) {
		log = document.createElement('div');
		log.id = 'ihh-console';
		log.style = 'position: fixed; bottom: 20px; right: 20px; width: 64em; border: solid lightsalmon 4px;' +
			' background-color: antiquewhite; padding: 10px; opacity: 1;' +
			' transition: opacity 1000ms linear; -webkit-transition: opacity 1000ms linear;';
		document.body.append(log);
	} else if (log.hTimer) {
		clearTimeout(log.hTimer);
		log.style.opacity = 1;
	}
	let div = document.createElement('div');
	div.style = 'font: 600 9pt Verdana, sans-serif; color: red;';
	div.textContent = message;
	log.append(div);
	log.hTimer = setTimeout(function(node) {
		node.style.opacity = 0;
		node.hTimer = setTimeout(function(node) { node.remove() }, 1000, node);
	}, 30000, log);
}