// ==UserScript==
// @name [RED] Cover Inspector
// @namespace https://greasyfork.org/users/321857-anakunda
// @version 1.11.1
// @run-at document-end
// @description Adds cover sticker if needs updating for unsupported host / big size / small resolution
// @author Anakunda
// @copyright 2020, Anakunda (https://greasyfork.org/users/321857-anakunda)
// @license GPL-3.0-or-later
// @match https://redacted.ch/torrents.php?id=*
// @match https://redacted.ch/artist.php?id=*
// @connect *
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @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/'];
function getRemoteFileSize(url, forced = true) {
return httpParser.test(url) ? new Promise(function(resolve, reject) {
let size, abort = GM_xmlhttpRequest({ method: forced ? 'GET' : 'HEAD', url: url, //responseType: 'blob',
onreadystatechange: function(response) {
if (size >= 0 || response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
if (/^(?:Content-Length)\s*:\s*(\d+)\b/im.test(response.responseHeaders) && (size = parseInt(RegExp.$1)) >= 0)
resolve(size);
else if (!forced) reject(undefined); else return;
abort.abort();
},
onload: function(response) { // fail-safe
if (size >= 0) return;
if (response.status < 200 || response.status >= 400) return reject('File not accessible');
//console.debug('responseText.length:', response.responseText.length);
resolve(response.responseText.length);
// console.time('GM_xmlhttpRequest response size getter');
// size = response.response.size; // response.responseText.length;
// console.timeEnd('GM_xmlhttpRequest response size getter');
// console.debug('response.size:', size);
// resolve(size);
},
onerror: response => { reject('File not accessible') },
ontimeout: response => { reject('File not accessible') },
});
}) : Promise.reject('getRemoteFileSize: parameter not valid URL');
}
function formattedSize(size) {
return size >= 0 ? size < 1024**1 ? Math.round(size) + ' B'
: size < 1024**2 ? (Math.round(size * 10 / 2**10) / 10) + ' KiB'
: size < 1024**3 ? (Math.round(size * 100 / 2**20) / 100) + ' MiB'
: size < 1024**4 ? (Math.round(size * 100 / 2**30) / 100) + ' GiB'
: size < 1024**5 ? (Math.round(size * 100 / 2**40) / 100) + ' TiB'
: (Math.round(size * 100 / 2**50) / 100) + ' PiB' : NaN;
}
const id = parseInt(new URLSearchParams(document.location.search).get('id'));
const rehostImageLinks = ajaxApiKey && document.location.pathname == '/torrents.php' ? (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(meta) {
console.assert(typeof unsafeWindow.rehostImageLinks == 'function', "typeof unsafeWindow.rehostImageLinks == 'function'");
return (typeof unsafeWindow.rehostImageLinks == 'function') ? unsafeWindow.rehostImageLinks
: Promise.reject('rehostImageLinks not accessible'); // assertion failed!
});
})() : Promise.reject('AJAX API key not configured or unsupported page');
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 inspectImage(img) {
console.assert(img instanceof HTMLImageElement, 'img instanceof HTMLImageElement');
if (!(img instanceof HTMLImageElement)) return;
img.parentNode.style.position = 'relative';
const _img = document.createElement('img');
function imageHandler(imgUrl) {
if (imgSrc.startsWith(document.location.origin) && imgSrc.includes('/static/common/noartwork/')) return;
const span = (content, isOK = false) => (isOK ? '<span>' : '<span style="color: yellow;">') + content + '</span>';
let sticker = document.getElementById('cover-inspector');
if (sticker != null) sticker.remove();
sticker = document.createElement('div');
sticker.id = 'cover-inspector';
sticker.style = `
position: absolute; right: 4px; bottom: 4px;
color: white; background-color: #ae2300; border: 1px solid whitesmoke;
font: 700 8pt "Segoe UI"; padding: 1px 5px; cursor:default; z-index: 10;
`;
// if (parseInt(img.getAttribute('width')) < 200 || parseInt(img.getAttribute('height')) < 200) {
// sticker.style.fontSize = '4.2pt';
// sticker.style.textAlign = 'center';
// sticker.style.padding = '1px 2px';
// sticker.style.right = '1px';
// sticker.style.bottom = '1px';
// }
Promise.all([
new Promise(function(resolve, reject) {
_img.src = imgUrl;
_img.onload = evt => { resolve(evt.currentTarget) };
_img.onerror = evt => { reject(evt.message) };
}),
getRemoteFileSize(imgUrl).catch(function(reason) {
console.warn('Failed to get remote image size (' + imgUrl + '):', 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 = imgUrl.startsWith(document.location.origin + '/image.php?'),
isPreferredHost = preferredHosts.some(preferredHost => imgUrl.startsWith(preferredHost)),
isSizeOK = acceptableCoverSize == 0 || results[1] <= acceptableCoverSize * 2**10,
isResolutionOK = acceptableCoverResolution == 0
|| (results[0].naturalWidth >= acceptableCoverResolution
&& results[0].naturalHeight >= acceptableCoverResolution);
if (isPreferredHost && isSizeOK && isResolutionOK) return true;
sticker.style.opacity = 0.8;
sticker.innerHTML = span(formattedSize(results[1]), isSizeOK) + ' / ' +
span(results[0].naturalWidth + '×' + results[0].naturalHeight, isResolutionOK);
if (isProxied) sticker.innerHTML = span('PROXY') + ' / ' + sticker.innerHTML;
else if (!isPreferredHost) sticker.innerHTML = span('XTRN') + ' / ' + sticker.innerHTML;
if (!isPreferredHost && id) rehostImageLinks.then(function(rehostImageLinks) {
sticker.style.cursor = 'pointer';
sticker.title = 'Click to rehost to preferred image host';
sticker.onclick = function(evt) {
if (evt.currentTarget.disabled) return false;
img.style.opacity = 0.5;
sticker.disabled = true;
rehostImageLinks([imgUrl], true).then(rehostedImages => queryAjaxAPI('groupedit', { id: id }, new URLSearchParams({
image: rehostedImages[0],
summary: 'Cover rehost',
})).then(function(response) {
console.log(response);
sticker.remove();
Promise.resolve(img.src = rehostedImages[0]).then(imageHandler);
//cument.location.reload();
})).catch(function(reason) {
unsafeWindow.ihhLogFail(reason);
sticker.disabled = false;
img.style.opacity = 1;
});
};
});
img.insertAdjacentElement('afterend', sticker);
return false;
}).catch(function(reason) {
sticker.innerHTML = span('INVALID');
img.insertAdjacentElement('afterend', sticker);
img.remove();
});
}
img.onload = evt => { if (evt.currentTarget.style.opacity < 1) evt.currentTarget.style.opacity = 1 };
if (id) rehostImageLinks.then(function(rehostImageLinks) {
img.ondragover = evt => false;
img.ondragenter = evt => { evt.currentTarget.parentNode.parentNode.style.backgroundColor = 'lawngreen' };
img[isFirefox ? 'ondragexit' : 'ondragleave'] =
evt => { evt.currentTarget.parentNode.parentNode.style.backgroundColor = null };
img.ondrop = function(evt) {
function dataSendHandler(endPoint) {
const sticker = document.getElementById('cover-inspector');
img.style.opacity = 0.5;
if (sticker != null) sticker.disabled = true;
endPoint([items[0]], true).then(imageUrls => queryAjaxAPI('groupedit', { id: id }, new URLSearchParams({
image: imageUrls[0],
summary: 'Cover update',
})).then(function(response) {
console.log(response);
if (sticker != null) sticker.remove();
Promise.resolve(img.src = imageUrls[0]).then(imageHandler);
//cument.location.reload();
})).catch(function(reason) {
unsafeWindow.ihhLogFail(reason);
if (sticker != null) sticker.disabled = false;
img.style.opacity = 1;
});
}
evt.preventDefault();
console.debug(evt.dataTransfer);
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(/^(https?:\/\/.+)$/i));
}
if (Array.isArray(items) && items.length > 0) {
if (confirm('Update torrent cover from the dropped URL?\n\n' + items[0]))
dataSendHandler(rehostImageLinks);
} else if (evt.dataTransfer.files.length > 0 && typeof unsafeWindow.uploadImages == 'function') {
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(unsafeWindow.uploadImages);
}
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.toSource())) imgSrc = RegExp.$1
else if (imgSrc.startsWith('https://i.imgur.com/')) imgSrc = imgSrc.replace(/\/(\w{7,})m\.(\w+)$/, '/$1.$2');
console.debug('imgSrc:', imgSrc);
imageHandler(imgSrc);
}
document.body.querySelectorAll([
'div#covers p > img',
'td > div.group_image > img',
'div.box_image > div > img',
].join(', ')).forEach(inspectImage);
if (id) rehostImageLinks.then(function(rehostImageLinks) {
function setCoverFromLink(a) {
console.assert(a instanceof HTMLAnchorElement, 'a instanceof HTMLAnchorElement');
if (!(a instanceof HTMLAnchorElement)) throw 'Invalid invoker';
rehostImageLinks([a.href], true).then(rehostedImages => queryAjaxAPI('groupedit', { id: id }, new URLSearchParams({
image: rehostedImages[0],
summary: 'Cover update',
})).then(function(response) {
console.log(response);
document.location.reload();
})).catch(unsafeWindow.ihhLogFail);
}
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 link', evt => { setCoverFromLink(menuInvoker) });
document.body.append(menu);
function clickHandler(evt) {
if (!evt.altKey) return true;
evt.preventDefault();
if (confirm('Set torrent group cover from this link?')) 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;
a.onclick = clickHandler;
a.title = 'Alt + click to set torrent image from this URL (or use context menu command)';
return true;
}
document.body.querySelectorAll([
'div.torrent_description > div.body a',
'table#torrent_details > tbody > tr.torrentdetails > td > blockquote a',
].join(', ')).forEach(setAnchorHandlers);
});