// ==UserScript== // @name [RED] Cover Inspector // @namespace https://greasyfork.org/users/321857-anakunda // @version 1.12.3 // @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 // @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 // @downloadURL none // ==/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 e = 'HTTP error ' + response.status; if (response.statusText) e += ' (' + response.statusText + ')'; if (response.error) e += ' (' + response.error + ')'; return e; } function defaultTimeoutHandler(response) { console.error('HTTP timeout:', response); const e = 'HTTP timeout'; return e; } 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 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 getHostFriendlyName(imageUrl) { if (httpParser.test(imageUrl)) try { imageUrl = new URL(imageUrl).hostname.toLowerCase() } catch(e) { console.error(e); return; } 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'], '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'], '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'], '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.endsWith('.' + domain) || imageUrl == 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; try { $(span).tooltipster({ content: tooltip.replace(/\r?\n/g, '
') }) } catch(e) { console.warn(e) } } 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 || (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'; try { $(convert).tooltipster() } catch(e) { console.warn(e) } }); } } 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)'; try { $(rehost).tooltipster() } catch(e) { console.warn(e) } }); sticker.title = imageUrl; try { $(sticker).tooltipster() } catch(e) { console.warn(e) } 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; try { $(sticker).tooltipster({ content: reason }) } catch(e) { console.warn(e) } 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 = 'lawngreen' }; img[isFirefox ? 'ondragexit' : 'ondragleave'] = 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(); 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(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 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; } } 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 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; 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)'; try { $(a).tooltipster() } catch(e) { console.warn(e) } } return true; } document.body.querySelectorAll([ 'div.torrent_description > div.body a', 'table#torrent_details > tbody > tr.torrentdetails > td > blockquote a', ].join(', ')).forEach(setAnchorHandlers); });