// ==UserScript== // @name [RED] Cover Inspector // @namespace https://greasyfork.org/users/321857-anakunda // @version 1.10.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 // @downloadURL none // ==/UserScript== 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; let imgSrc = img.dataset.gazelleTempSrc || img.src; if (imgSrc.startsWith(document.location.origin) && imgSrc.includes('/static/common/noartwork/')) return; 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); img.parentNode.style.position = 'relative'; const _img = document.createElement('img'); (function imageHandler(imgUrl) { const span = (content, isOK = false) => (isOK ? '' : '') + content + '', sticker = document.createElement('div'); sticker.className = '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 (img.width < 200 || img.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; sticker.disabled = true; img.style.opacity = 0.5; rehostImageLinks([imgUrl], true).then(rehostedImages => queryAjaxAPI('groupedit', { id: id }, new URLSearchParams({ image: rehostedImages[0], summary: 'Image update/rehost', })).then(function(response) { console.log(rehostedImages[0], response); sticker.remove(); Promise.resolve(img.src = rehostedImages[0]).then(imageHandler); })).catch(unsafeWindow.ihhLogFail).then(function() { img.style.opacity = 1; sticker.disabled = false; }); }; }); img.insertAdjacentElement('afterend', sticker); return false; }).catch(function(reason) { sticker.innerHTML = span('INVALID'); img.insertAdjacentElement('afterend', sticker); img.remove(); }); })(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: 'Image update/rehost', })).then(function(response) { console.log(rehostedImages[0], 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); });