Greasy Fork

Greasy Fork is available in English.

[RED] Cover Inspector

Adds cover sticker if needs updating for unsupported host / big size / small resolution

当前为 2022-06-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         [RED] Cover Inspector
// @namespace    http://greasyfork.icu/users/321857-anakunda
// @version      1.13.2
// @run-at       document-end
// @description  Adds cover sticker if needs updating for unsupported host / big size / small resolution
// @author       Anakunda
// @copyright    2020, Anakunda (http://greasyfork.icu/users/321857-anakunda)
// @license      GPL-3.0-or-later
// @iconURL      https://i.ibb.co/mh2prQR/clouseau.png
// @match        https://redacted.ch/torrents.php?id=*
// @match        https://redacted.ch/torrents.php
// @match        https://redacted.ch/torrents.php?action=advanced
// @match        https://redacted.ch/torrents.php?action=advanced&*
// @match        https://redacted.ch/torrents.php?*&action=advanced
// @match        https://redacted.ch/torrents.php?*&action=advanced&*
// @match        https://redacted.ch/torrents.php?action=basic
// @match        https://redacted.ch/torrents.php?action=basic&*
// @match        https://redacted.ch/torrents.php?*&action=basic
// @match        https://redacted.ch/torrents.php?*&action=basic&*
// @match        https://redacted.ch/torrents.php?page=*
// @match        https://redacted.ch/torrents.php?action=notify
// @match        https://redacted.ch/torrents.php?action=notify&*
// @match        https://redacted.ch/torrents.php?type=*
// @match        https://redacted.ch/artist.php?id=*
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_openInTab
// @require      https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
// @require      https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.min.js
// ==/UserScript==

const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
const httpParser = /^(https?:\/\/.+)*$/i;
const preferredHosts = ['https://ptpimg.me/'];
const preferredTypes = ['jpeg', 'webp', 'gif'];

try { var fileSizeCache = new Map(JSON.parse(sessionStorage.fileSizeCache)) } catch(e) { fileSizeCache = new Map }
try { var fileTypeCache = new Map(JSON.parse(sessionStorage.fileTypeCache)) } catch(e) { fileTypeCache = new Map }

function defaultErrorHandler(response) {
	console.error('HTTP error:', response);
	let reason = 'HTTP error ' + response.status;
	if (response.status == 0) reason += '/' + response.readyState;
	let statusText = response.statusText;
	if (response.response) try {
		if (typeof response.response.error == 'string') statusText = response.response.error;
	} catch(e) { }
	if (statusText) reason += ' (' + statusText + ')';
	return reason;
}
function defaultTimeoutHandler(response) {
	console.error('HTTP timeout:', response);
	let reason = 'HTTP timeout';
	if (response.timeout) reason += ' (' + response.timeout + ')';
	return reason;
}

function getRemoteFileSize(url) {
	if (!httpParser.test(url)) return Promise.reject('getRemoteFileSize(...): parameter not valid URL');
	if (fileSizeCache.has(url)) return Promise.resolve(fileSizeCache.get(url));
	const getByXHR = (method = 'GET') => new Promise(function(resolve, reject) {
		function success() {
			fileSizeCache.set(url, size);
			sessionStorage.fileSizeCache = JSON.stringify(Array.from(fileSizeCache));
			resolve(size);
		}
		let size, hXHR = GM_xmlhttpRequest({ method: method, url: url, binary: true, responseType: 'blob',
			onreadystatechange: function(response) {
				if (typeof size == 'number' && size >= 0 || response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
				if ((size = /^(?:Content-Length)\s*:\s*(\d+)\b/im.exec(response.responseHeaders)) != null
						&& (size = parseInt(size[1])) >= 0) success();
					else if (method == 'HEAD') reject('Content size missing in header'); else return;
				if (method != 'HEAD') hXHR.abort();
			},
			onload: function(response) { // fail-safe
				if (typeof size == 'number' && size >= 0) return;
				if (response.status >= 200 && response.status < 400) {
					/*if (response.response) {
						size = response.response.size;
						success();
					} else */if (response.responseText) {
						size = response.responseText.length;
						success();
					} else reject('Body missing');
				} else reject(defaultErrorHandler(response));
			},
			onerror: response => { reject(defaultErrorHandler(response)) },
			ontimeout: response => { reject(defaultTimeoutHandler(response)) },
		});
	});
	return getByXHR('GET')/*.catch(reason => getByXHR('GET'))*/;
}

function getRemoteFileType(url) {
	if (!httpParser.test(url)) return Promise.reject('getRemoteFileType: parameter not valid URL');
	if (fileTypeCache.has(url)) return Promise.resolve(fileTypeCache.get(url));
	const getByXHR = (method = 'GET') => new Promise(function(resolve, reject) {
		let contentType, hXHR = GM_xmlhttpRequest({ method: method, url: url,
			onreadystatechange: function(response) {
				if (contentType !== undefined || response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
				if ((contentType = /^(?:Content-Type)\s*:\s*(.+?)(?:\s*;(.+?))?\s*$/im.exec(response.responseHeaders)) != null) {
					fileTypeCache.set(url, contentType = contentType[1].toLowerCase());
					sessionStorage.fileTypeCache = JSON.stringify(Array.from(fileTypeCache));
					resolve(contentType);
				} else reject('MIME type missing in header');
				if (method != 'HEAD') hXHR.abort();
			},
			onerror: response => { reject(defaultErrorHandler(response)) },
			ontimeout: response => { reject(defaultTimeoutHandler(response)) },
		});
	});
	return getByXHR('HEAD')
		.catch(reason => /^HTTP error (403|416)\b/.test(reason) ? getByXHR('GET') : Promise.reject(reason));
}

function formattedSize(size) {
	return size < 1024**1 ? Math.round(size) + '\xA0B'
		: size < 1024**2 ? (Math.round(size * 10 / 2**10) / 10) + '\xA0KiB'
		: size < 1024**3 ? (Math.round(size * 100 / 2**20) / 100) + '\xA0MiB'
		: size < 1024**4 ? (Math.round(size * 100 / 2**30) / 100) + '\xA0GiB'
		: size < 1024**5 ? (Math.round(size * 100 / 2**40) / 100) + '\xA0TiB'
		: (Math.round(size * 100 / 2**50) / 100) + '\xA0PiB';
}

const groupId = document.location.pathname == '/torrents.php'
	&& parseInt(new URLSearchParams(document.location.search).get('id')) || undefined;

const imageHostHelper = ajaxApiKey ? (function() {
	const input = document.head.querySelector('meta[name="ImageHostHelper"]');
	return (input != null ? Promise.resolve(input) : new Promise(function(resolve, reject) {
		const mo = new MutationObserver(function(mutationsList, mo) {
			for (let mutation of mutationsList) for (let node of mutation.addedNodes) {
				if (node.nodeName != 'META' || node.name != 'ImageHostHelper') continue;
				clearTimeout(timer); mo.disconnect();
				return resolve(node);
			}
		}), timer = setTimeout(function(mo) {
			mo.disconnect();
			reject('Timeout reached');
		}, 10000, mo);
		mo.observe(document.head, { childList: true });
	})).then(function(node) {
		console.assert(node instanceof HTMLElement);
		const propName = node.getAttribute('propertyname');
		console.assert(propName);
		return unsafeWindow[propName] || Promise.reject('Assertion failed: \'' + propName + '\' not in unsafeWindow');
	});
})() : Promise.reject('AJAX API key not configured or unsupported site');

if (!document.tooltipster) document.tooltipster = typeof jQuery.fn.tooltipster == 'function' ?
		Promise.resolve(jQuery.fn.tooltipster) : new Promise(function(resolve, reject) {
	const script = document.createElement('SCRIPT');
	script.src = '/static/functions/tooltipster.js';
	script.type = 'text/javascript';
	script.onload = function(evt) {
		//console.log('tooltipster.js was successfully loaded', evt);
		if (typeof jQuery.fn.tooltipster == 'function') resolve(jQuery.fn.tooltipster);
			else reject('tooltipster.js loaded but core function was not found');
	};
	script.onerror = evt => { reject('Error loading tooltipster.js') };
	document.head.append(script);
	['style.css'/*, 'custom.css', 'reset.css'*/].forEach(function(css) {
		const styleSheet = document.createElement('LINK');
		styleSheet.rel = 'stylesheet';
		styleSheet.type = 'text/css';
		styleSheet.href = '/static/styles/tooltipster/' + css;
		//styleSheet.onload = evt => { console.log('style.css was successfully loaded', evt) };
		styleSheet.onerror = evt => { (css == 'style.css' ? reject : console.warn)('Error loading ' + css) };
		document.head.append(styleSheet);
	});
});

let acceptableCoverSize = GM_getValue('acceptable_cover_size');
if (!(acceptableCoverSize >= 0)) GM_setValue('acceptable_cover_size', acceptableCoverSize = 2048);
let acceptableCoverResolution = GM_getValue('acceptable_cover_resolution');
if (!(acceptableCoverResolution >= 0)) GM_setValue('acceptable_cover_resolution', acceptableCoverResolution = 300);

function getHostFriendlyName(imageUrl) {
	if (httpParser.test(imageUrl)) try { imageUrl = new URL(imageUrl) } catch(e) { console.error(e) }
	if (imageUrl instanceof URL) imageUrl = imageUrl.hostname.toLowerCase(); else return;
	const knownHosts = {
		'2i': ['2i.cz'],
		'Abload': ['abload.de'],
		'AllMusic': ['rovicorp.com'],
		'AllThePics': ['allthepics.net'],
		'Amazon': ['media-amazon.com', 'ssl-images-amazon.com', 'amazonaws.com'],
		'Apple': ['mzstatic.com'],
		'Archive': ['archive.org'],
		'Bandcamp': ['bcbits.com'],
		'Beatport': ['beatport.com'],
		'BilderUpload': ['bilder-upload.eu'],
		'Boomkat': ['boomkat.com'],
		'CasImages': ['casimages.com'],
		'Catbox': ['catbox.moe'],
		'CloudFront': ['cloudfront.net'],
		'CubeUpload': ['cubeupload.com'],
		'Deezer': ['dzcdn.net'],
		'Discogs': ['discogs.com'],
		'Discord': ['discordapp.net'],
		'eBay': ['ebayimg.com'],
		'Extraimage': ['extraimage.org'],
		'FastPic': ['fastpic.ru', 'fastpic.org'],
		'Forumbilder': ['forumbilder.com'],
		'FreeImageHost': ['freeimage.host'],
		'FunkyImg': ['funkyimg.com'],
		'GeTt': ['ge.tt'],
		'GeekPic': ['geekpic.net'],
		'GetaPic': ['getapic.me'],
		'Gifyu': ['gifyu.com'],
		'GooPics': ['goopics.net'],
		'HRA': ['highresaudio.com'],
		'imageCx': ['image.cx'],
		'ImageBan': ['imageban.ru'],
		'ImagensBrasil': ['imagensbrasil.org'],
		'ImageRide': ['imageride.com'],
		'ImageToT': ['imagetot.com'],
		'ImageVenue': ['imagevenue.com'],
		'ImgBank': ['imgbank.cz'],
		'ImgBB': ['ibb.co'],
		'ImgBox': ['imgbox.com'],
		'ImgCDN': ['imgcdn.dev'],
		'Imgoo': ['imgoo.com'],
		'ImgPile': ['imgpile.com'],
		'imgsha': ['imgsha.com'],
		'Imgur': ['imgur.com'],
		'ImgURL': ['png8.com'],
		'IpevRu': ['ipev.ru'],
		'Jerking': ['jerking.empornium.ph'],
		'JPopsuki': ['jpopsuki.eu'],
		'Juno': ['junodownload.com'],
		'Last.fm': ['lastfm.freetls.fastly.net', 'last.fm'],
		'Lensdump': ['lensdump.com'],
		'LightShot': ['prntscr.com'],
		'LostPic': ['lostpic.net'],
		'Lutim': ['lut.im'],
		'MetalArchives': ['metal-archives.com'],
		'Mobilism': ['mobilism.org'],
		'Mora': ['mora.jp'],
		'MusicBrainz': ['coverartarchive.org'],
		'NoelShack': ['noelshack.com'],
		'Photobucket': ['photobucket.com'],
		'PicaBox': ['picabox.ru'],
		'PicLoad': ['free-picload.com'],
		'PimpAndHost': ['pimpandhost.com'],
		'Pinterest': ['pinimg.com'],
		'PixHost': ['pixhost.to'],
		'PomfCat': ['pomf.cat'],
		'PostImg': ['postimg.cc'],
		'PTPimg': ['ptpimg.me'],
		'Qobuz': ['qobuz.com'],
		'Ra': ['thesungod.xyz'],
		'Radikal': ['radikal.ru'],
		'SavePhoto': ['savephoto.ru'],
		'Shopify': ['shopify.com'],
		'Slowpoke': ['slow.pics'],
		'SoundCloud': ['sndcdn.com'],
		'SM.MS': ['sm.ms'],
		'SVGshare': ['svgshare.com'],
		'Tidal': ['tidal.com'],
		'Traxsource': ['traxsource.com'],
		'Twitter': ['twimg.com'],
		'Upimager': ['upimager.com'],
		'Uupload.ir': ['uupload.ir'],
		'VgyMe': ['vgy.me'],
		'Wiki': ['wikimedia.org'],
		'Z4A': ['z4a.net'],
		'路过图床': ['imgchr.com'],
	};
	for (let name in knownHosts) if (knownHosts[name].some(function(domain) {
		domain = domain.toLowerCase();
		return imageUrl == domain || imageUrl.endsWith('.' + domain);
	})) return name;
}

const hostsBlacklist = GM_getValue('banned_from_click2go', [ ]);
const bannedFromClick2Go = friendlyHost => friendlyHost && Array.isArray(hostsBlacklist)
	&& hostsBlacklist.some(fh => fh.toLowerCase() == friendlyHost.toLowerCase());

function getImageMax(imageUrl) {
	const friendlyName = getHostFriendlyName(imageUrl);
	return imageHostHelper.then(ihh => (function() {
		const func = friendlyName && {
			'Deezer': 'getDeezerImageMax',
			'Discogs': 'getDiscogsImageMax',
		}[friendlyName];
		return func && func in ihh ? ihh[func](imageUrl) : Promise.reject('No imagemax function');
	})().catch(function(reason) {
		let sub = friendlyName && {
			'Bandcamp': [/_\d+(?=\.(\w+)$)/, '_10'],
			'Deezer': ihh.dzrImageMax,
			'Apple': ihh.itunesImageMax,
			'Qobuz': [/_\d{3}(?=\.(\w+)$)/, '_org'],
			'Boomkat': [/\/(?:large|medium|small)\//i, '/original/'],
			'Beatport': [/\/image_size\/\d+x\d+\//i, '/image/'],
			'Tidal': [/\/(\d+x\d+)(?=\.(\w+)$)/, '/1280x1280'],
			'Amazon': [/\._\S+?_(?=\.)/, ''],
			'HRA': [/_(\d+x\d+)(?=\.(\w+)$)/, ''],
		}[friendlyName];
		if (sub) sub = String(imageUrl).replace(...sub); else return Promise.reject('No imagemax substitution');
		return 'verifyImageUrl' in ihh ? ihh.verifyImageUrl(sub) : sub;
	}).catch(reason => 'verifyImageUrl' in ihh ? ihh.verifyImageUrl(imageUrl) : imageUrl));
}

function inspectImage(img, id) {
	console.assert(img instanceof HTMLImageElement, 'img instanceof HTMLImageElement');
	if (!(img instanceof HTMLImageElement)) return;
	img.onload = evt => { evt.currentTarget.hidden = false };
	img.parentNode.style.position = 'relative';
	const inListing = img.clientWidth > 0 && img.clientWidth < 100, image = new Image;
	if (id && /^(?:cover_(\d+))$/.test(img.id) & parseInt(RegExp.$1) > 0) id = undefined;
	const loadHandler = evt => { if (evt.currentTarget.style.opacity < 1) evt.currentTarget.style.opacity = 1 };
	const clickHandler = evt => { lightbox.init(evt.currentTarget.src, 220) };

	function editOnClick(elem) {
		function editOnClick(evt) {
			evt.stopPropagation();
			evt.preventDefault();
			//document.getSelection().removeAllRanges();
			const url = '/torrents.php?' + new URLSearchParams({
				action: 'editgroup',
				groupid: id,
			}).toString();
			if ((evt.shiftKey || evt.ctrlKey) && typeof GM_openInTab == 'function')
				GM_openInTab(document.location.origin + url, evt.shiftKey); else document.location.assign(url);
			return false;
		}
		console.assert(elem instanceof HTMLElement, 'elem instanceof HTMLElement');
		if (!(elem instanceof HTMLElement)) return;
		elem.classList.add('edit');
		elem.style.cursor = 'pointer';
		elem.style.userSelect = 'none';
		elem.style['-webkit-user-select'] = 'none';
		elem.style['-moz-user-select'] = 'none';
		elem.style['-ms-user-select'] = 'none';
		elem.onclick = editOnClick;
	}

	function imgsrcHandler(imageUrl) {
		const sticker = document.createElement('div');
		sticker.className = 'cover-inspector';
		sticker.style = `
position: absolute; color: white; border: thin solid lightgray;
font-family: "Segoe UI", sans-serif; font-weight: 700;
cursor: default; transition-duration: 0.25s; z-index: 1;
		` + (inListing ? 'right: 1px; bottom: 1px; padding: 0; font-size: 6pt; text-align: right;'
			: ' right: -3pt; bottom: -7pt; padding: 1px; font-size: 8.5pt;');

		function span(content, className, isOK = false, tooltip) {
			const span = document.createElement('SPAN');
			if (className) span.className = className;
			if (tooltip) {
				span.title = tooltip;
				document.tooltipster.then(() => { $(span).tooltipster({ content: tooltip.replace(/\r?\n/g, '<br>') }) })
					.catch(reason => { console.warn(reason) });
			}
			span.style.padding = inListing ? '0 2px' : '0 4px';
			if (!isOK) span.style.color = 'yellow';
			span.textContent = content;
			return span;
		}

		Promise.all([
			new Promise(function(resolve, reject) {
				image.onload = evt => { resolve(evt.currentTarget) };
				image.onerror = evt => { reject(evt.message || 'Image load error (' + evt.currentTarget.src + ')') };
				image.src = imageUrl;
			}),
			getRemoteFileSize(imageUrl).catch(function(reason) {
				console.warn('Failed to get remote image size (' + imageUrl + '):', reason);
				return undefined;
			}),
			getRemoteFileType(imageUrl).catch(function(reason) {
				console.warn('Failed to get remote image type (' + imageUrl + '):', reason);
				return undefined;
			}),
		]).then(function(results) {
			if (results[0].naturalWidth <= 0 || results[0].naturalHeight <= 0
					|| results[1] < 2 * 2**10 && results[0].naturalWidth == 400 && results[0].naturalHeight == 100
					|| results[1] == 503) return Promise.reject('Image is invalid');
			const isProxied = imageUrl.startsWith(document.location.origin + '/image.php?'),
						isPreferredHost = preferredHosts.some(preferredHost => imageUrl.startsWith(preferredHost)),
						isSizeOK = acceptableCoverSize == 0 || results[1] <= acceptableCoverSize * 2**10,
						isResolutionOK = acceptableCoverResolution == 0
							|| ((document.location.pathname == '/artist.php' || results[0].naturalWidth >= acceptableCoverResolution)
								&& results[0].naturalHeight >= acceptableCoverResolution),
						isTypeOK = !results[2] || preferredTypes.some(format => results[2] == 'image/' + format);

			const divisor = () => inListing ? document.createElement('BR') : '/';
			function isOutside(target, related) {
				if (target instanceof HTMLElement) {
					target = target.parentNode;
					while (related instanceof HTMLElement) if ((related = related.parentNode) == target) return false;
				}
				return true;
			}

			sticker.onmouseenter = img.onmouseenter = evt => { sticker.style.opacity = 1 };
			let rehost, convert;
			const friendlyHost = getHostFriendlyName(imageUrl);
			if (isPreferredHost && isSizeOK && isResolutionOK && isTypeOK) {
				sticker.style.backgroundColor = 'teal';
				sticker.style.opacity = 0;
				sticker.onmouseleave = img.onmouseleave =
					evt => { if (isOutside(evt.currentTarget, evt.relatedTarget)) sticker.style.opacity = 0 };
				if (results[2]) sticker.append(divisor(), span(results[2], 'mime-type', true));
			} else {
				sticker.style.backgroundColor = '#ae2300';
				sticker.style.opacity = 0.75;
				sticker.onmouseleave = img.onmouseleave =
					evt => { if (isOutside(evt.currentTarget, evt.relatedTarget)) sticker.style.opacity = 0.75 };
				if (inListing && id) editOnClick(sticker);
				if (!isTypeOK) {
					sticker.append(divisor(), convert = span(results[2], 'mime-type', false));
					if (id && httpParser.test(imageUrl) && (!inListing || !bannedFromClick2Go(friendlyHost))) imageHostHelper.then(function(ihh) {
						convert.style.cursor = 'pointer';
						convert.onclick = function(evt) {
							evt.stopPropagation();
							if (evt.currentTarget.hasAttribute('disabled')) return false;
							convert.setAttribute('disabled', 1);
							img.style.opacity = 0.3;
							ihh.reduceImageSize(imageUrl, 2160, 90)
								.then(output => ihh.rehostImages([output.uri]).then(ihh.singleImageGetter)
								.then(rehostedImgUrl => queryAjaxAPI('groupedit', { id: id }, {
									image: rehostedImgUrl,
									summary: 'Cover downsize', //+ ' (' + formattedSize(results[1]) + ' => ' + formattedSize(output.size) + ')',
								}).then(function(response) {
									console.log(response);
									sticker.remove();
									img.onload = loadHandler;
									if (typeof lightbox == 'object') img.onclick = clickHandler; //else document.location.reload();
									Promise.resolve(img.src = rehostedImgUrl).then(imgsrcHandler);
								}))).catch(function(reason) {
									if ('logFail' in ihh) ihh.logFail(reason);
									img.style.opacity = 1;
									convert.removeAttribute('disabled');
								});
						};
						convert.style.transitionDuration = '0.25s';
						convert.onmouseenter = evt => { evt.currentTarget.style.textShadow = '0 0 5px lime' };
						convert.onmouseleave = evt => { evt.currentTarget.style.textShadow = null };
						convert.title = 'Click to convert it to JPG';
						document.tooltipster.then(() => { $(convert).tooltipster() }).catch(reason => { console.warn(reason) });
					});
				}
			}
			sticker.prepend(span(results[0].naturalWidth + '×' + results[0].naturalHeight, 'resolution', isResolutionOK),
				divisor(), rehost = span(formattedSize(results[1]), 'size', isSizeOK));
			if (isProxied) {
				sticker.prepend(rehost = span('PROXY', 'proxy'), divisor());
				imageUrl = new URL(imageUrl);
				imageUrl = imageUrl.searchParams.get('i') || imageUrl.searchParams.values().next().value
					|| decodeURIComponent(imageUrl.search.slice(1));
			} else if (!isPreferredHost) {
				sticker.prepend(rehost = span(friendlyHost || 'XTRN',
					'external-host', false, 'Image at unpreferred image host'), divisor());
				if (bannedFromClick2Go(friendlyHost)) rehost.style.color = 'orange';
			} else if (isSizeOK) {
				rehost = null;
				if (convert instanceof HTMLElement && (!inListing || !bannedFromClick2Go(friendlyHost)))
						convert.classList.add('click2go');
			}
			if (rehost instanceof HTMLElement && id && httpParser.test(imageUrl)
					&& (!inListing || !bannedFromClick2Go(friendlyHost))) imageHostHelper.then(function(ihh) {
				rehost.classList.add('click2go');
				rehost.style.cursor = 'pointer';
				rehost.onclick = function(evt) {
					evt.stopPropagation();
					if (evt.currentTarget.hasAttribute('disabled')) return false;
					rehost.setAttribute('disabled', 1);
					img.style.opacity = 0.3;
					getImageMax(imageUrl).then(maxImgUrl => ihh.rehostImageLinks([maxImgUrl], true).then(ihh.singleImageGetter))
						.then(rehostedImgUrl => queryAjaxAPI('groupedit', { id: id }, {
							image: rehostedImgUrl,
							summary: 'Cover rehost',
						}).then(function(response) {
							console.log(response);
							sticker.remove();
							img.onload = loadHandler;
							if (typeof lightbox == 'object') img.onclick = clickHandler; //else document.location.reload();
							Promise.resolve(img.src = rehostedImgUrl).then(imgsrcHandler);
						})).catch(function(reason) {
							if ('logFail' in ihh) ihh.logFail(reason);
							img.style.opacity = 1;
							rehost.removeAttribute('disabled');
						});
				};
				rehost.style.transitionDuration = '0.25s';
				rehost.onmouseenter = evt => { evt.currentTarget.style.textShadow = '0 0 5px lime' };
				rehost.onmouseleave = evt => { evt.currentTarget.style.textShadow = null };
				rehost.title = 'Hosted at: ' + new URL(imageUrl).hostname + ' (click to rehost to preferred image host)';
				document.tooltipster.then(() => { $(rehost).tooltipster() }).catch(reason => { console.warn(reason) });
			});
			sticker.title = imageUrl;
			document.tooltipster.then(() => { $(sticker).tooltipster() }).catch(reason => { console.warn(reason) });
			img.insertAdjacentElement('afterend', sticker);
			return false;
		}).catch(function(reason) {
			sticker.append(span('INVALID'));
			sticker.style.left = 0;
			sticker.style.bottom = '-11pt';
			sticker.style.width = '100%';
			sticker.style.backgroundColor = 'red';
			if (id) editOnClick(sticker);
			sticker.title = reason;
			document.tooltipster.then(() => { $(sticker).tooltipster({ content: reason }) }).catch(reason => { console.warn(reason) });
			img.insertAdjacentElement('afterend', sticker);
			img.hidden = true; //img.remove();
		});
	}

	if (id) imageHostHelper.then(function(ihh) {
		img.classList.add('drop');
		img.ondragover = evt => false;
		if (img.clientWidth > 100) {
			img.ondragenter = evt => { evt.currentTarget.parentNode.parentNode.style.backgroundColor = '#7fff0040' };
			img[`ondrag${isFirefox ? 'exit' : 'leave'}`] =
				evt => { evt.currentTarget.parentNode.parentNode.style.backgroundColor = null };
		}
		img.ondrop = function(evt) {
			function dataSendHandler(endPoint) {
				const sticker = evt.currentTarget.parentNode.querySelector('div.cover-inspector');
				img.style.opacity = 0.3;
				if (sticker != null) sticker.disabled = true;
				endPoint([items[0]], true, false, true, {
					ctrlKey: evt.ctrlKey,
					shiftKey: evt.shiftKey,
					altKey: evt.altKey,
				}).then(ihh.singleImageGetter).then(imageUrl => queryAjaxAPI('groupedit', { id: id }, {
					image: imageUrl,
					summary: 'Cover update',
				}).then(function(response) {
					console.log(response);
					if (sticker != null) sticker.remove();
					img.onload = loadHandler;
					if (typeof lightbox == 'object') img.onclick = clickHandler; //else document.location.reload();
					Promise.resolve(img.src = imageUrl).then(imgsrcHandler);
				})).catch(function(reason) {
					if ('logFail' in ihh) ihh.logFail(reason);
					if (sticker != null) sticker.disabled = false;
					img.style.opacity = 1;
				});
			}

			evt.stopPropagation();
			var items = evt.dataTransfer.getData('text/uri-list');
			if (items) items = items.split(/\r?\n/); else {
				items = evt.dataTransfer.getData('text/x-moz-url');
				if (items) items = items.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
					else if (items = evt.dataTransfer.getData('text/plain'))
						items = items.split(/\r?\n/).filter(RegExp.prototype.test.bind(httpParser));
			}
			if (Array.isArray(items) && items.length > 0 && 'rehostImageLinks' in ihh) {
				if (confirm('Update torrent cover from the dropped URL?\n\n' + items[0]))
					dataSendHandler(ihh.rehostImageLinks);
			} else if (evt.dataTransfer.files.length > 0 && 'uploadFiles' in ihh) {
				items = Array.from(evt.dataTransfer.files)
					.filter(file => file instanceof File && file.type.startsWith('image/'));
				if (items.length > 0 && confirm('Update torrent cover from the dropped file?'))
					dataSendHandler(ihh.uploadFiles);
			}
			if (img.clientWidth > 100) evt.currentTarget.parentNode.parentNode.style.backgroundColor = null;
			return false;
		};
	});
	let imgSrc = img.dataset.gazelleTempSrc || img.src;
	if (typeof img.onclick == 'function' && /\b(?:lightbox\.init)\('(.+?)'/.test(img.onclick.toString())) imgSrc = RegExp.$1
		else if (imgSrc.startsWith('https://i.imgur.com/')) imgSrc = imgSrc.replace(/\/(\w{7,})m\.(\w+)$/, '/$1.$2');
	console.debug('imgSrc:', imgSrc);
	if (!imgSrc.includes('static/common/noartwork/')) imgsrcHandler(imgSrc); else if (id) editOnClick(img);
}

for (let img of document.body.querySelectorAll([
	'div#covers p > img',
	'div.box_image > div > img',
].join(', '))) inspectImage(img, groupId);

function setTooltip(elem, tooltip) {
	if (elem instanceof HTMLElement) document.tooltipster.then(function() {
		if (tooltip) tooltip = tooltip.replace(/\r?\n/g, '<br>')
		if ($(elem).data('plugin_tooltipster'))
			if (tooltip) $(elem).tooltipster('update', tooltip).tooltipster('enable');
				else $(elem).tooltipster('disable');
		else if (tooltip) $(elem).tooltipster({ content: tooltip });
	});
}

function setTableHandlers(table, hdr, marginLeft) {
	if (!(table instanceof HTMLElement) || !(hdr instanceof HTMLElement)) return;

	function getGroupId(root) {
		if (root instanceof HTMLElement) for (let a of root.getElementsByTagName('A'))
			if (a.origin == document.location.origin && a.pathname == '/torrents.php') {
				const urlParams = new URLSearchParams(a.search);
				if (urlParams.has('action')) continue;
				const id = parseInt(urlParams.get('id'));
				if (id >= 0) return id;
			}
		return null;
	}

	const a = document.createElement('A');
	a.className = 'brackets';
	a.style.marginLeft = marginLeft;
	a.textContent = 'Inspect all covers';
	a.href = '#';
	a.onclick = function(evt) {
		evt.currentTarget.style.visibility = 'collapse';
		for (let tr of table.querySelectorAll('tbody > tr.group, tbody > tr.torrent')) {
			const img = tr.querySelector('div.group_image > img');
			if (img != null) inspectImage(img, getGroupId(tr.querySelector('div.group_info')));
		}
		const currentTarget = evt.currentTarget;
		imageHostHelper.then(function(ihh) {
			currentTarget.textContent = 'Rehost all';
			currentTarget.onclick = function(evt) {
				evt.currentTarget.remove();
				for (let div of table.querySelectorAll('div.cover-inspector > span.click2go:not([disabled])')) div.click();
				return false;
			};
			setTimeout(a => { a.style.visibility = 'visible' }, 1000, currentTarget);
		}, reason => { currentTarget.remove() });
		return false;
	};
	hdr.append(a);
}

switch (document.location.pathname) {
	case '/artist.php': {
		const table = document.getElementById('discog_table');
		if (table != null) setTableHandlers(table, table.querySelector(':scope > div.box'), '2em');
		break;
	}
	case '/torrents.php': {
		const table = document.body.querySelector('table.torrent_table');
		if (table == null) break;
		const a = table.querySelector(':scope > tbody > tr.colhead > td > a');
		if (a != null) setTableHandlers(table, a.parentNode, '5em');
		break;
	}
	case '/collages.php': {
		const collageId = parseInt(new URLSearchParams(document.location.search).get('id'));
		if (!(collageId > 0)) throw 'Could not extract collage id';
		let userAuth = document.body.querySelector('input[name="auth"]');
		if (userAuth != null) userAuth = userAuth.value; else throw 'User auth could not be located';
		const removeFromCollage = torrentGroupId => new Promise(function(resolve, reject) {
			const xhr = new XMLHttpRequest, payLoad = new URLSearchParams({
				action: 'manage_handle',
				collageid: collageId,
				groupid: torrentGroupId,
				auth: userAuth,
				submit: 'Remove',
			});
			xhr.open('POST', '/collages.php', true);
			xhr.onreadystatechange = function() {
				if (xhr.readyState < XMLHttpRequest.DONE) return;
				if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.status); else reject(defaultErrorHandler(xhr));
			};
			xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
			xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
			xhr.send(payLoad);
		});
		imageHostHelper.then(function(ihh) {
			function fixCollagePage(evt) {
				function getLinks(body) {
					body = domParser.parseFromString(body, 'text/html').getElementsByTagName('A');
					if (body.length > 0) body = Array.prototype.map.call(body, a => a.target == '_blank' ? new URL(a) : null)
						.filter(Boolean);
					return body.length > 0 ? body : null;
				}

				evt.currentTarget.hidden = true;
				const domParser = new DOMParser;
				const bb2Html = bbBody => queryAjaxAPI('preview', undefined, { body: bbBody });
				const head = document.body.querySelector('table#discog_table > tbody > tr.colhead_dark');
				if (head != null && head.querySelector('td.status') == null) {
					const td = document.createElement('TD');
					td.className = 'status';
					td.textContent = 'Status';
					const tooltip = 'Result of attempt to add missing/broken cover\nHover the mouse over status for more details';
					td.title = tooltip; //setTooltip(td, tooltip);
					head.append(td);
				}
				document.body.querySelectorAll('table#discog_table > tbody > tr[id^="group_"]').forEach(function(tr) {
					function setStatus(status, tooltip) {
						let td = tr.querySelector('td.status');
						if (td == null) return; // assertion failed
						td.textContent = (status = Number(status) || 0) >= 2 ? 'success' : 'failed';
						td.className = 'status ' + td.textContent + ' status-code-' + status;
						if (tooltip) td.title = tooltip; else td.removeAttribute('title');
						//setTooltip(td, tooltip);
						td.style.color = ['red', 'orange', '#adad00', 'green'][status];
						td.style.opacity = 1;
						if (status <= 0 && (td = document.body.querySelector('a.hide-status-failed')) != null) td.hidden = false;
					}

					const status = document.createElement('TD');
					status.className = 'status';
					status.textContent = 'unknown';
					status.style.opacity = 0.3;
					tr.append(status);
					let groupId = tr.querySelector(':scope > td > strong > a:last-of-type');
					if (groupId == null || !((groupId = parseInt(new URLSearchParams(groupId.search).get('id'))) > 0)) {
						setStatus(0, 'Could not extract torrent id');
						return;
					}
					queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup =>
							ihh.verifyImageUrl(torrentGroup.group.wikiImage).then(function(imageUrl) {
						let tooltip = 'This release seems to have a valid image';
						setStatus(3, tooltip);
						tooltip = [tooltip];
						removeFromCollage(groupId).then(function(status) {
							tooltip.push('(removed from collage)');
							setStatus(3, tooltip.join('\n'));
						});
						ihh.rehostImageLinks([imageUrl], true, false, false).then(imageUrls => queryAjaxAPI('groupedit', { id: groupId }, {
							image: imageUrls[0],
							summary: 'Automated cover rehost',
						}).then(function(response) {
							tooltip.push('(' + response + ')');
							setStatus(3, tooltip.join('\n'));
							console.log('[Cover Inspector]', response);
						}));
					}).catch(function(reason) {
						// lookup cover in description
						let links = getLinks(torrentGroup.group.wikiBody);
						return links ? Promise.all(links.map(url => ihh.imageUrlResolver(url.href).catch(reason => null)))
								.then(imageUrls => imageUrls.filter(getHostFriendlyName)).then(function(imageUrls) {
							if (imageUrls.length <= 0) return Promise.reject('No of description links could extract a valid image');
							return ihh.rehostImageLinks(imageUrls, true, false, false).then(imageUrls => queryAjaxAPI('groupedit', { id: groupId }, {
								image: imageUrls[0],
								summary: 'Automated attempt to add cover from description link',
							}).then(function(response) {
								const tooltip = [response];
								if (imageUrls.length > 1) tooltip.push('(more external links in description require attention)');
								setStatus(imageUrls.length > 1 ? 2 : 3, tooltip.join('\n'));
								console.log('[Cover Inspector]', response);
								removeFromCollage(groupId).then(function(status) {
									tooltip.push('(removed from collage)');
									setStatus(imageUrls.length > 1 ? 2 : 3, tooltip.join('\n'));
								});
							}));
						}) : Promise.reject('No active external links found in dscriptions');
					}).catch(function(reason) {
						if (!Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0)
							return Promise.reject(reason);
						return Promise.all(torrentGroup.torrents.filter(torrent => torrent.description).map(torrent =>
								bb2Html(torrent.description).then(getLinks, reason => null))).then(function(urls) {
							if ((urls = urls.filter(url => url && ![
								document.location.hostname,
								'redacted.ch', 'orpheus.network',
							].includes(url.hostname))).length <= 0) return Promise.reject(reason);
							setStatus(1, 'No active external links in album description,\nbut release descriptions contain some');
							console.log('[Cover Inspector] Links found in torrent descriptions for', groupId, ':', urls);
						});
					})).catch(reason => { setStatus(0, reason) });
				});
				return false;
			}

			const td = document.body.querySelector('table#discog_table > tbody > tr.colhead_dark > td:nth-of-type(3)');
			if (td != null) {
				let a = document.createElement('A');
				a.className = 'brackets auto-add-covers';
				a.textContent = 'Try to add covers';
				a.style = 'float: right; margin-right: 1em; color: gold;';
				a.href = '#';
				a.onclick = fixCollagePage;
				td.append(a);
				a = document.createElement('A');
				a.className = 'brackets hide-status-failed';
				a.textContent = 'Hide failed';
				a.style = 'float: right; margin-right: 1em;';
				a.href = '#';
				a.onclick = function hideFailed(evt) {
					evt.currentTarget.hidden = true;
					document.body.querySelectorAll('table#discog_table > tbody > tr[id^="group_"] td.status.status-code-0')
						.forEach(td => { td.parentNode.hidden = true })
					return false;
				};
				a.hidden = true;
				td.append(a);
			}
		});
		break;
	}
}

if (groupId) imageHostHelper.then(function(ihh) {
	function setCoverFromLink(a) {
		console.assert(a instanceof HTMLAnchorElement, 'a instanceof HTMLAnchorElement');
		if (!(a instanceof HTMLAnchorElement)) throw 'Invalid invoker';
		ihh.rehostImageLinks([a.href], true).then(rehostedImages => queryAjaxAPI('groupedit', { id: groupId }, {
			image: ihh.singleImageGetter(rehostedImages),
			summary: 'Cover update',
		}).then(function(response) {
			console.log(response);
			document.location.reload();
		})).catch(ihh.logFail);
	}

	const contextId = '522a6889-27d6-4ea6-a878-20dec4362fbd', menu = document.createElement('menu');
	menu.type = 'context';
	menu.id = contextId;
	menu.className = 'cover-inspector';
	let menuInvoker;
	const setMenuInvoker = evt => { menuInvoker = evt.currentTarget };

	function addMenuItem(label, callback) {
		if (label) {
			const menuItem = document.createElement('MENUITEM');
			menuItem.label = label;
			if (typeof callback == 'function') menuItem.onclick = callback;
			menu.append(menuItem);
		}
		return menu.children.length;
	}

	addMenuItem('Set cover image from this source', evt => { setCoverFromLink(menuInvoker) });
	document.body.append(menu);

	function clickHandler(evt) {
		if (evt.altKey) evt.preventDefault(); else return true;
		if (confirm('Set torrent group cover from this source?')) setCoverFromLink(evt.currentTarget);
		return false;
	}

	function setAnchorHandlers(a) {
		console.assert(a instanceof HTMLAnchorElement, 'a instanceof HTMLAnchorElement');
		if (!(a instanceof HTMLAnchorElement)) return false;
		a.setAttribute('contextmenu', contextId);
		a.oncontextmenu = setMenuInvoker;
		if (a.protocol.startsWith('http') && !a.onclick) {
			a.onclick = clickHandler;
			a.title = 'Alt + click to set torrent image from this URL (or use context menu command)';
			document.tooltipster.then(() => { $(a).tooltipster() }).catch(reason => { console.warn(reason) });
		}
		return true;
	}

	document.body.querySelectorAll([
		'div.torrent_description > div.body a',
		'table#torrent_details > tbody > tr.torrentdetails > td > blockquote a',
	].join(', ')).forEach(setAnchorHandlers);
});