Image Host Helper

Directly upload local / rehost remote images or galleries to whatever supported image host by dropping/pasting them to target field

As of 08. 01. 2021. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Image Host Helper
// @namespace    https://greasyfork.org/users/321857-anakunda
// @version      1.086
// @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, Anakunda (https://greasyfork.org/cs/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
// @require      https://openuserjs.org/src/libs/Anakunda/xhrLib.js
// @require      https://openuserjs.org/src/libs/Anakunda/progressBars.js
// @require      https://openuserjs.org/src/libs/Anakunda/imageHostUploader.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 discogsKey = 'LWiNvIWBobGMRhfSCAiC';
const discogsSecret = 'HAQUKFmebpCSLyRNwjmSgOMgbnxsVQcp';
const lfmApiKey = '920db0d2f86108f2fbe1917b53d63858';

Array.prototype.flatten = function() {
	return this.reduce(function(flat, toFlatten) {
		return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten);
	}, []);
};

PTPimg.prototype.setSession = function() {
	return this.apiKey ? Promise.resolve(this.apiKey) : globalXHR(this.origin).then(response => {
		var apiKey = response.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 own 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;
	});
}

var cheveretoCustomHosts = GM_getValue('chevereto_custom_hosts');
if (cheveretoCustomHosts !== undefined) 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) { console.warn(e) } else GM_setValue('chevereto_custom_hosts', '[]');
console.log('Image host handlers:', imageHostHandlers);

['upload_hosts', 'rehost_hosts'].forEach(propName => { if (!GM_getValue(propName)) GM_setValue(propName, [
	'PTPimg', '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',
].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']],
].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
document.querySelectorAll('input[type="text"]').forEach(function(input) {
	if ([
		'image', 'img', 'picture', 'cover', 'photo', 'avatar', 'poster', 'screen',
	].some(pattern => input.name && input.name.toLowerCase().includes(pattern)
		|| input.id && input.id.toLowerCase().includes(pattern))) setInputHandlers(input);
});
// Set multiple inputs UI handlers
for (let textArea of document.getElementsByTagName('textarea')) {
	if (textArea.className != 'ua-input') setTextAreahandlers(textArea);
}

// site-specific extensions
switch (document.domain) {
	case 'passthepopcorn.me':
		// Auto-fill missing/invalid images from IMDB
		if (/\/artist\.php\?action=edit&artistid=(\d+)\b/i.test(document.URL)) {
			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 (/\/torrents\.php??action=editgroup&groupid=(\d+)\b/i.test(document.URL)) {
			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') });
				});
			});
		}
		// HJ Toolkit patch
		setTimeout(function() {
			if (document.querySelector('div.HJ-toolkit-badge') != null) {
				let hjtkTimer = setInterval(function() {
					document.querySelectorAll([
						'textarea[id^="HJMA"]',
						'textarea.form-control[name="screen"]',
						'textarea.form-control[name="comp"]',
						].join(',')).forEach(setTextAreahandlers);
				}, 1000);
			}
		}, 1000);
		break;
	case 'redacted.ch':
	case 'orpheus.network':
	case 'notwhat.cd':
	case 'dicmusic.club':
		if (document.location.pathname == '/upload.php')
			setInputHandlers(document.querySelector('input[type="text"][name="verification"]'));
		// Auto-fill missing/invalid artist images
		if (document.URL.includes('/artist.php?action=edit&')) {
			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*$/, ''];
					let results = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toASCII().replace(/\s+|/g, '').toLowerCase()
							== artist.replace(...tailingBracketStripper).toASCII().replace(/\s+/g, '').toLowerCase()), 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.response.nbHits <= 0) return Promise.reject('Qobuz: no matches');
						let results = resultsFilter(response.response.hits, result => result.name);
						if (results.length <= 0) return Promise.reject('Qobuz: no matches');
						//console.debug('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(response) {
						let results = resultsFilter(Array.from(response.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.debug('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')));
					}),
					// 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.response.items <= 0) return Promise.reject('Discogs: no matches');
						let results = resultsFilter(response.response.results.filter(result => result.type == 'artist'),
							result => result.title);
						if (results.length <= 0) return Promise.reject('Discogs: no matches');
						//console.debug('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);
						let artistCovers = results.map(result => {
							if (result.cover_image.includes('/spacer.gif')) return null;
							return result.cover_image.replace(/^(?:https?):\/\/(?:img\.discogs\.com)\/.+\/([\w\%\-]+\.\w+)\b(?:\.\w+)*$/i,
								'https://www.discogs.com/image/$1');
						});
						return httpParser.test(artistCovers[0]) ?
							artistCovers[0] : Promise.reject('Discogs: 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.response.resultCount <= 0) return Promise.reject('iTunes: no matches');
						let results = resultsFilter(response.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);
					}),
					// Deezer
					globalXHR('https://api.deezer.com/search/artist?' + new URLSearchParams({
						q: artist,
						order: 'RANKING',
						//strict: 'on',
					}).toString(), { responseType: 'json' }).then(function(response) {
						if (response.response.total <= 0) return Promise.reject('Deezer: no matches');
						let results = resultsFilter(response.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.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));
					}),
					// 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(response) {
							for (let script of response.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.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.response.error) return Promise.reject(response.response.message);
						//console.debug('Last.fm search result for "' + artist + '":', response.response.artist);
						const rx = /\/(\d+)x(\d+)\//;
						let biggest = response.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');
					}),
				];
				const lookUp = (index = 0) => index < lookupWorkers.length ?
					lookupWorkers[index].then(setCover.bind(input)).catch(reason => lookUp(index + 1))
						: Promise.reject('Image of this artist was not found');
				lookUp().catch(logFail);
			});
		}
		break;
	case 'tracker.czech-server.com':
		if (document.location.pathname == '/upload2.php')
			setInputHandlers(document.querySelector('input[type="text"][name="urlobr"]'));
		break;
}
switch (document.location.pathname) {
	case '/torrents.php': {
		if (!document.location.search.startsWith('?id=')) break;
		let a = document.querySelector('span.additional_add_artists > a');
		if (a != null) a.addEventListener('click', function() {
			document.querySelectorAll('input[name="image[]"]').forEach(setInputHandlers);
		});
		break;
	}
	case '/reportsv2.php': {
		function setReportHandlers(evt) {
			setTimeout(function() {
				document.querySelectorAll('input[id*="image"]').forEach(setInputHandlers);
				document.querySelectorAll('textarea').forEach(setTextAreahandlers);
			}, 3000);
		}

		setReportHandlers();
		let reportTypeSelect = document.querySelector('select#type');
		if (reportTypeSelect != null) reportTypeSelect.addEventListener('change', setReportHandlers);
		break;
	}
}

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

function coverPreview(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: absolute; bottom: 20px; right: 20px; border: thin solid silver; ' +
		'background-color: #8888; padding: 10px; opacity: 0; transition: opacity 1s ease-in-out;';
	const cleanUp = () => {
		if (div.parentNode == null) return;
		div.style.opacity = 0;
		setTimeout(() => { document.body.removeChild(div) }, 1000);
	};
	div.ondblclick = cleanUp;
	let img = document.createElement('img');
	img.style = 'width: 225px;';
	img.onload = function(evt) {
		document.body.append(div);
		setTimeout(() => { div.style.opacity = 1 });
		setTimeout(cleanUp, 12000);
		if (!img.naturalWidth || !img.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;';
		div.append(info);
		const resolution = img.naturalWidth + '×' + img.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 = function(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);
		coverPreview(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 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';
		}
		evt.target.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;
		evt.target.disabled = true;
		if (evt.target.hTimer) {
			clearTimeout(evt.target.hTimer);
			delete evt.target.hTimer;
		}
		evt.target.style.color = 'white';
		evt.target.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;
				evt.target.value = 'Uploading... [' + (progressBar.lastPct = pct) + '%]';
				progressBar.lastUpdate = Date.now();
			} else if (param == null) {
				progressBar = { current: worker };
				evt.target.value = 'Uploading...';
			}
		}
		const file = data.files[0];
		evt.target.disabled = true;
		checkImageSize(file, evt.target, 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) {
				evt.target.value = imageUrl;
				coverPreview(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() {
			evt.target.style.backgroundColor = '#008000';
			evt.target.hTimer = setTimeout(function() {
				evt.target.style.backgroundColor = null;
				evt.target.style.color = null;
				delete evt.target.hTimer;
			}, 10000);
		}, function(reason) {
			imageClear(evt);
			evt.target.style.backgroundColor = null;
			evt.target.style.color = null;
			Promise.resolve(reason).then(msg => { alert(msg) });
		}).then(() => { evt.target.disabled = false });
		return false;
	} else if (data.items.length > 0) {
		let links = data.getData('text/uri-list');
		if (links) links = links.split(/\r?\n/); else {
			links = data.getData('text/x-moz-url');
			if (links) links = links.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
				else if (links = data.getData('text/plain')) links = links.split(/\r?\n/);
		}
		if (!Array.isArray(links) || links.length <= 0) return true;
		console.time('Image URL Rehoster');
		imageUrlResolver(links[0], {
			altKey: evt.altKey,
			ctrlKey: evt.ctrlKey != (evt.target.name == 'image[]'),
			shiftKey: evt.shiftKey,
		}).then(verifyImageUrl).then(function(imageUrl) {
			evt.target.disabled = true;
			evt.target.value = imageUrl;
			let size = getRemoteFileSize(imageUrl);
			coverPreview(imageUrl, size);
			checkImageSize(imageUrl, evt.target, size).then(rehoster).catch(function(reason) {
				evt.target.value = imageUrl;
				Promise.resolve(reason).then(msg => { alert(msg + ' (not rehosted)') });
			}).then(() => { evt.target.disabled = false });
		}).then(() => { console.timeEnd('Image URL Rehoster') }, alert);
		return false;
	}
	return true;
}

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(results => { 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;
	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;
		evt.target.disabled = true;
		if (!['notwhat.cd'].some(hostname => document.domain == hostname))
			var progressBar = new ULProgressBar(evt.target, 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);
			evt.target.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, evt.target).catch(function(reason) {
			if (evt.ctrlKey)
				evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + content.join('\n') +
					evt.target.value.slice(evt.rangeOffset);
			else {
				if (evt.target.value.length > 0) evt.target.value += '\n\n';
				evt.target.value += content.join('\n');
			}
		});
		evt.stopPropagation();
		return false;
	}
	return true;

	function resultsHandler(results, groups = undefined) {
		if (results.length <= 0) return;
		if (evt.altKey && !evt.target.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 = `
<div 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"/>
</div>
`;
			document.body.append(modal);
			let form = document.getElementById('ihh-template-selector'),
					btnInsert = document.querySelector('div#ihh-template-selector input#btn-insert'),
					btnCancel = document.querySelector('div#ihh-template-selector 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('div#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 (evt.target.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(evt.target.value)) {
					evt.target.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)) {
				code = '[comparison=Source, Encode]' + code + '[/comparison]';
				if (template == 5) {
					code += nl;
					results.forEach((result, index) => { if (index % 2 != 0) code += nl + '[img]' + getImgUrl(result) + '[/img]' });
				}
			}
			if (evt.target.value.trimRight().length <= 0) evt.target.value = code; else if (evt.ctrlKey) {
				evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + code + evt.target.value.slice(evt.rangeOffset);
			} else evt.target.value = evt.target.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;
	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;
		evt.target.disabled = true;
		if (!['notwhat.cd'].some(hostname => document.domain == hostname))
			var progressBar = new ULProgressBar(evt.target, 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);
			evt.target.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, evt.target);
		evt.stopPropagation();
		return false;
	}
	return true;

	function resultsHandler(results, groups = undefined) {
		let selStart = evt.target.selectionStart, phpBB = '';
		results.forEach(function(result, index) {
			let thumb = evt.altKey && !evt.target.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 += evt.target.noBBCode ? phpBB += imgUrl : !thumb ? '[img]' + imgUrl + '[/img]'
				: '[url=' + imgUrl + '][img]' + result.thumb + '[/img][/url]';
		});
		if (phpBB.length <= 0) return;
		evt.target.value = evt.target.value.slice(0, selStart) + phpBB + evt.target.value.slice(evt.target.selectionEnd);
		evt.target.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, 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) coverPreview(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)) 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(response) {
			let ref = response.document.querySelector('div#tralbumArt > a.popupImage');
			ref = ref != null ? ref.href : getFromMeta(response.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(response) {
				let img = response.document.querySelector('img[itemprop="image"]');
				return img != null ? img.src : notFound;
			});
		else if (url.hostname.endsWith('geekpic.net')) return globalXHR(url).then(function(response) {
			let a = response.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(response) {
			let img = response.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(response) {
			let meta = getFromMeta(response.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':
				return url.pathname.startsWith('/a/') ? globalXHR(url, { responseType: 'text' }).then(function(response) {
					if (/^\s*(?:image)\s*:\s*(\{.+\}),\s*$/m.test(response.responseText)) try {
						return JSON.parse(RegExp.$1).album_images.images.map(image => 'https://i.imgur.com/' + image.hash + image.ext);
					} catch(e) { debug.warn(e) }
					return notFound;
				}) : globalXHR(url).then(response => response.document.querySelector('link[rel="image_src"]').href);
			case 'pixhost.to':
				if (url.pathname.startsWith('/gallery/')) return globalXHR(url).then(response =>
					Promise.all(Array.from(response.document.querySelectorAll('div.images > a')).map(a => imageUrlResolver(a.href, modifiers))));
				if (url.pathname.startsWith('/show/')) return globalXHR(url)
					.then(response => response.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(response =>
					Promise.all(Array.from(response.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(response) {
					let images = Array.from(response.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(response => response.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(response =>
						JSON.parse(response.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(response => response.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(response) {
					let types = Array.from(response.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(response) {
					let ref = response.document.querySelectorAll('ul#containerContent > li a.media-link');
					if (ref.length <= 0) return getFromMeta(response.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(response) {
					let elem = response.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(response => imageUrlResolver(response.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(response => response.document.querySelector('div.mainBlock img').src);
			case 'imageban.ru': case 'ibn.im':
				return globalXHR(url).then(response => response.document.querySelector('a[download]').href);
			case 'svgshare.com':
				return globalXHR(url).then(function(response) {
					let link;
					response.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(response) {
					let nodes = response.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 = response.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(response =>
						Array.from(response.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(response) {
					let elem = response.document.querySelector('div.logo > a');
					if (elem != null) return elem.href;
					elem = response.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.response.result.success) return Promise.reject(response.response.result.errors);
					if (Array.isArray(response.response.result.data.images))
						return response.response.result.data.images.map(image => image.url);
					return response.response.result.data.image ? response.response.result.data.image.url : notFound;
				});
			case 'sm.ms':
				if (url.pathname.startsWith('/image/')) return globalXHR(url).then(function(response) {
					let img = response.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(response) {
					let img = response.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(response) {
					let meta = getFromMeta(response.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(response => (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 = response.document.getElementById('all-versions-link');
					if (master == null) return Promise.reject('no master release for this page');
					return imageUrlResolver('https://www.discogs.com' + master.pathname, modifiers);
				})().catch(function(reason) {
					const imgMax = [/^(?:https?):\/\/(?:img\.discogs\.com)\/.+\/([\w\%\-]+\.\w+)\b(?:\.\w+)*$/i,
						'https://www.discogs.com/image/$1'];
					let elem = response.document.querySelector('div.image_gallery, div.image_gallery_large');
					if (elem != null) try {
						if ((elem = JSON.parse(elem.dataset.images)).length <= 0) throw 'empty';
						return elem.map(image => (image.full || image.thumb).replace(...imgMax));
					} catch(e) { console.warn('Invalid Discogs image gallery:', elem, '(' + e + ')') } else {
						console.warn('Missing Discogs image gallery record for', url.href);
					}
					return (elem = getFromMeta(response.document)) ? elem.replace(...imgMax) : notFound;
				}));
			case 'www.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(response => (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 = response.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 = response.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 = response.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 = response.document.querySelector('a.artwork-image')) != null ? elem.href
						: (elem = response.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(response) {
					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(image => (image.zoomURL || image.url).replace(...imageMax));
						} catch(e) {
							let img = dom.querySelector('div[class$="-cover"] img');
							if (img != null) return (img.dataset.largeurl || img.src).replace(...imageMax);
						}
						return notFound;
					}
					return (function() {
						const mainAlbum = response.document.querySelector('section.main-album a.album-title');
						if (mainAlbum == null) return Promise.reject('no main album');
						return globalXHR(mainAlbum.href).then(response => amImgsXtractor(response.document));
					})().catch(reason => amImgsXtractor(response.document));
				}); else if (url.pathname.startsWith('/artist/')) return globalXHR(url).then(function(response) {
					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(response.document.querySelector('div.sidebar > script').text);
						let imageGallery = JSON.parse(/(\[.+\]);/.exec(response.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 = response.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(response) {
						let environment = response.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.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(response => getFromMeta(response.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(response) {
					let img = response.document.querySelector('div.album-cover > img');
					if (img == null) return getFromMeta(response.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(response) {
					let img = response.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(response) {
					let meta = getFromMeta(response.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(response) {
					let meta = getFromMeta(response.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(response =>
					verifyImageUrl(response.document.querySelector('div.c-product-block__aside > a').href.replace(/\?\d+$/))); else break;
			case 'www.bontonland.cz':case 'bontonland.cz':
				return globalXHR(url).then(response => response.document.querySelector('a.detailzoom').href);
			case 'www.prostudiomasters.com': case 'prostudiomasters.com':
				if (url.pathname.includes('/album/')) return globalXHR(url).then(function(response) {
					let a = response.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(response) {
					let meta = getFromMeta(response.document);
					return meta ? meta.replace(/\/s\d+\//, '/s0/') : notFound;
				}); else break;
			case 'store.acousticsounds.com':
				return globalXHR(url).then(function(response) {
					let link = response.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(response => verifyImageUrl(response.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(response) {
					let elem = getFromMeta(response.document);
					return elem || ((elem = response.document.querySelector('div.artwork')) != null ?
						'https:' + elem.dataset.modalArtwork : 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(response) {
					let imgUrl = getFromMeta(response.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(response => verifyImageUrl(response.document.querySelector('meta[itemprop="image"]')
					.content.replace(/\?.*$/, '')).catch(reason => notFound));
			case 'vgmdb.net':
				if (url.pathname.includes('/album/')) return globalXHR(url).then(function(response) {
					let div = response.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(response) {
					let img = response.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(response) {
					let script = response.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(response) {
					let node = getFromMeta(response.document);
					if (node) return verifyImage(node.replace(/\/[sbl]\//i, '/')).catch(reason => node);
					node = response.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(response) {
					let a = response.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(response) {
					let meta = getFromMeta(response.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(response) {
					let elem = response.document.querySelector('div#gallery > a') || response.document.querySelector('div.cover a');
					if (elem != null) return 'https://www.deejay.de' + elem.pathname;
					return (elem = getFromMeta(response.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.response.album.picUrl ?
						response.response.album.picUrl.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 + '?countrycode=US&token=_DSTon1kC8pABnTw', {
					responseType: 'json',
				}).then(response => response.response.cover ? 'https://resources.tidal.com/images/' + response.response.cover.replace(/-/g, '/') + '/1280x1280.jpg' : notFound);
			case 'www.extrememusic.com': case 'extrememusic.com':
				if (url.pathname.startsWith('/albums/')) return globalXHR(url).then(function(response) {
					let meta = getFromMeta(response.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(response) {
					let imgUrl = getFromMeta(response.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(response) {
					let img = response.document.querySelector('figure > img.zoomImg');
					if (img != null) return img.src;
					img = response.document.querySelector('section.productView-images > figure');
					return img != null && img.dataset.zoomImage || notFound;
				});
			case 'music.youtube.com':
				return globalXHR(url).then(function(response) {
					for (let script of response.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;
				});
			// books-related
			case 'www.goodreads.com': case 'goodreads.com':
				if (url.pathname.includes('/show/')) return globalXHR(url).then(function(response) {
					let img = response.document.querySelector('div.editionCover > img')
						|| response.document.querySelector('img#coverImage');
					img = img != null ? img.src : getFromMeta(response.document);
					return img ? img.replace(/\._\w+_\./g, '.').replace(/\?.*/, '') : notFound;
				}); else break;
			// 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(response) {
					let node = response.document.querySelector('meta[property="og:image"][content]');
					return verifyImageUrl(node.content.replace(/\/p\/\w+\//i, '/p/original/')).catch(function(reason) {
						node = response.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(response) {
					let node = response.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(response => verifyImageUrl(response.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(response) {
					//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(response.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(response =>
					verifyImageUrl(document.location.protocol.concat(response.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(response => verifyImageUrl(response.document.querySelector('div.mojo-primary-image img').src));
			case 'www.metacritic.com': case 'metacritic.com':
				return globalXHR(url).then(function(response) {
					let image = response.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(response) {
					let a = response.document.querySelector('a.boxPlakaty');
					if (a == null) return Promise.reject('Invalid page structure');
					a.hostname = 'www.fdb.cz';
					return globalXHR(a.href).then(function(response) {
						let imgs = response.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';
					return 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)}`,
						];
					});
				}); 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(response) {
			if (url.pathname.startsWith('/album/')
					&& response.document.querySelector('div#tabbed-content-group > div.content-listing > div.pad-content-listing') != null)
				return new Chevereto(url.hostname).galleryResolver(url);
			let elem = response.document.querySelector('head > meta[name="generator"][content]');
			if (elem != null && elem.content.toLowerCase() == 'bandcamp') {
				elem = response.document.querySelector('div#tralbumArt > a.popupImage');
				elem = elem != null ? elem.href : getFromMeta(response.document);
				return httpParser.test(elem) ? elem.replace(/_\d+(?=\.\w+$)/, '_0') : notFound;
			}
			return getFromMeta(response.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);
}