// ==UserScript== // @name [RED] Cover Inspector // @namespace https://greasyfork.org/users/321857-anakunda // @version 1.13.10 // @run-at document-end // @description Easify & speed-up of finding and updating of invalid, missing or not optimal album covers on site // @author Anakunda // @copyright 2020-22, Anakunda (https://greasyfork.org/users/321857-anakunda) // @license GPL-3.0-or-later // @iconURL https://i.ibb.co/4gpP2J4/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 = { 'redacted.ch': ['ptpimg.me'], }[document.domain]; const preferredTypes = ['jpeg', 'webp', 'gif']; 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 needsUniqueUA(url) { if (httpParser.test(url)) try { const hostname = new URL(url).hostname; return ['dzcdn.', 'mzstatic.com'].some(pattern => hostname.includes(pattern)); } catch(e) { } return false } function setUniqueUA(headers, divisor = 2, period = 60 * 60) { if (!headers || typeof headers != 'object' || !navigator.userAgent) return; period = Math.floor(period * Math.pow(10, 3 - (divisor = Math.floor(divisor)))); divisor = Math.pow(10, divisor); headers['User-Agent'] = navigator.userAgent.replace(/\b(Gecko|\w*WebKit|Blink|Goanna|Flow|\w*HTML|Servo|NetSurf)\/(\d+(\.\d+)*)\b/, (match, engine, engineVersion) => engine + '/' + engineVersion + '.' + (Math.floor(Date.now() / divisor) % period).toString().padStart(period.toString().length, '0')); } function getRemoteFileSize(url) { if (!httpParser.test(url)) return Promise.reject('Not a valid URL'); const getByXHR = (method = 'GET') => new Promise(function(resolve, reject) { const params = { method: method, url: url, binary: true, responseType: 'blob', headers: { } }; if (needsUniqueUA(url)) { params.anonymous = true; setUniqueUA(params.headers, 1); } let size, hXHR = GM_xmlhttpRequest(Object.assign(params, { onreadystatechange: function(response) { if (size > 0 || response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return; size = /^(?:Content-Length)\s*:\s*(\d+)\b/im.exec(response.responseHeaders); if (size != null && (size = parseInt(size[1])) > 0) { resolve(size); if (method != 'HEAD') hXHR.abort(); } else if (method == 'HEAD') reject('Content size missing or invalid in header'); }, onload: function(response) { // fail-safe if (size > 0) return; else if (response.status >= 200 && response.status < 400) { /*if (response.response) { size = response.response.size; resolve(size); } else */if (response.responseText && (size = response.responseText.length) > 0) resolve(size); 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'); const getByXHR = (method = 'GET') => new Promise(function(resolve, reject) { const params = { method: method, url: url, headers: { } }; if (needsUniqueUA(url)) { params.anonymous = true; setUniqueUA(params.headers, 1); } let contentType, hXHR = GM_xmlhttpRequest(Object.assign(params, { onreadystatechange: function(response) { if (contentType !== undefined || response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return; contentType = /^(?:Content-Type)\s*:\s*(.+?)(?:\s*;(.+?))?\s*$/im.exec(response.responseHeaders); if (contentType != null) resolve(contentType[1].toLowerCase()); 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 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'); }, 15000, 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); }); }); function setTooltip(elem, tooltip, params) { if (!(elem instanceof HTMLElement)) throw 'Invalid argument'; document.tooltipster.then(function() { if (tooltip) tooltip = tooltip.replace(/\r?\n/g, '
') if ($(elem).data('plugin_tooltipster')) if (tooltip) $(elem).tooltipster('update', tooltip).tooltipster('enable'); else $(elem).tooltipster('disable'); else if (tooltip) $(elem).tooltipster({ content: tooltip }); }).catch(function(reason) { if (tooltip) elem.title = tooltip; else elem.removeAttribute('title'); }); } function getPreference(key, defVal) { let value = GM_getValue(key); if (value == undefined) GM_setValue(key, value = defVal); return value; } const acceptableSize = getPreference('acceptable_cover_size', 4 * 2**10); const acceptableResolution = getPreference('acceptable_cover_resolution', 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'], '7digital': ['7static.com'], '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'], 'Dibpic': ['dibpic.com'], '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'], 'Genius': ['genius.com'], 'GetaPic': ['getapic.me'], 'Gifyu': ['gifyu.com'], 'GooPics': ['goopics.net'], 'HRA': ['highresaudio.com'], 'imageCx': ['image.cx'], 'ImageBan': ['imageban.ru'], 'ImageKit': ['imagekit.io'], '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'], 'ProgArchives': ['progarchives.com'], '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'], 'VGMdb': ['vgm.io', 'vgmdb.net'], '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 noCoverHeres = [ document.location.hostname, 'redacted.ch', 'orpheus.network', 'apollo.rip', 'notwhat.cd', 'dicmusic.club', 'what.cd', 'jpopsuki.eu', 'rutracker.net', 'github.com', 'gitlab.com', 'ptpimg.me', 'imgur.com', ].concat(GM_getValue('no_covers_here', [ ])); const hostsBlacklist = GM_getValue('banned_from_click2go', [ ]); const bannedFromClick2Go = friendlyHost => friendlyHost && Array.isArray(hostsBlacklist) && hostsBlacklist.some(fh => fh.toLowerCase() == friendlyHost.toLowerCase()); const domParser = new DOMParser; const autoOpenSucceed = GM_getValue('auto_open_succeed', true); const autoOpenWithLink = GM_getValue('auto_open_with_link', true); const hasArtworkSet = img => img instanceof HTMLImageElement && img.src && !img.src.includes('/static/common/noartwork/'); function realImgSrc(img) { if (!(img instanceof HTMLImageElement)) throw 'Invalid argument'; if (img.hasAttribute('onclick')) { const src = /\blightbox\.init\('(https?:\/\/.+?)',\s*\d+\)/.exec(img.getAttribute('onclick')); if (src != null) return src[1]; } return !img.src.startsWith('https://i.imgur.com/') ? img.src : img.src.replace(/\/(\w{7,})m\.(\w+)$/, '/$1.$2'); } const openGroup = groupId => { if (groupId > 0) GM_openInTab(`${document.location.origin}/torrents.php?id=${groupId}`, true) } 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)); } const getImageDetails = imageUrl => httpParser.test(imageUrl) ? Promise.all([ new Promise(function(resolve, reject) { const image = new Image; 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(`[Cover Inspector] Failed to get remote image size (${imageUrl}):`, reason); return null; }), getRemoteFileType(imageUrl).catch(function(reason) { console.warn(`[Cover Inspector] Failed to get remote image type (${imageUrl}):`, reason); return null; }), ]).then(results => ({ src: results[0].src, width: results[0].naturalWidth, height: results[0].naturalHeight, size: results[1], mimeType: results[2], })) : Promise.reject('Void or invalid URL'); const bb2Html = bbBody => queryAjaxAPI('preview', undefined, { body: bbBody }); function getLinks(html) { if ((html = domParser.parseFromString(html, 'text/html').getElementsByTagName('A')).length > 0) html = Array.from(html, function(a) { if (a.href && a.target == '_blank') try { return new URL(a) } catch(e) { console.warn(e) } return null; }).filter(url => url && !noCoverHeres.includes(url.hostname)); return html.length > 0 ? html : null; } if ('imageDetailsCache' in sessionStorage) try { var imageDetailsCache = JSON.parse(sessionStorage.getItem('imageDetailsCache')); } catch(e) { console.warn(e) } if (!imageDetailsCache) imageDetailsCache = { }; const imgClickHandler = evt => { lightbox.init(evt.currentTarget.src, 220) }; function inspectImage(img, groupId) { if (!(img instanceof HTMLImageElement)) throw 'Invalid argument'; if (img.parentNode == null) return Promise.resolve(false); if (img.hidden) img.onload = evt => { evt.currentTarget.hidden = false }; img.parentNode.style.position = 'relative'; const inListing = img.width == 90; let isSecondaryCover = !inListing && /^cover_(\d+)$/.test(img.id); isSecondaryCover = Boolean(isSecondaryCover) && !(parseInt(isSecondaryCover[1]) > 0); if (groupId && isSecondaryCover) groupId = undefined; const imgLoadHandler = evt => { if (evt.currentTarget.style.opacity < 1) evt.currentTarget.style.opacity = 1 }; let sticker; function editOnClick(elem) { function editOnClick(evt) { evt.stopPropagation(); evt.preventDefault(); //document.getSelection().removeAllRanges(); const url = new URL('torrents.php', document.location.origin); url.searchParams.set('action', 'editgroup'); url.searchParams.set('groupid', groupId); if ((evt.shiftKey || evt.ctrlKey) && typeof GM_openInTab == 'function') GM_openInTab(url.href, evt.shiftKey); else document.location.assign(url); return false; } 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 setSticker(imageUrl) { sticker = img.parentNode && img.parentNode.querySelector('div.cover-inspector'); if (sticker != null) sticker.remove(); sticker = document.createElement('DIV'); sticker.className = 'cover-inspector'; sticker.style = `position: absolute; display: flex; 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 ? 'flex-flow: column; right: 0; bottom: 0; padding: 0; font-size: 6pt; text-align: right;' : 'flex-flow: row wrap; right: -3pt; bottom: -7pt; padding: 1px; font-size: 8.5pt; max-width: 98%;'}` if (isSecondaryCover) sticker.style.bottom = '7pt'; function span(content, className, isOK = false, tooltip) { const span = document.createElement('SPAN'); if (className) span.className = className; span.style = `padding: 0 ${inListing ? '2px' : '4px'};`; if (!isOK) span.style.color = 'yellow'; span.textContent = content; if (tooltip) setTooltip(span, tooltip); return span; } return (function() { if (!imageUrl) return Promise.reject('Void image URL'); if (!httpParser.test(imageUrl)) return Promise.reject('Invalid image URL'); if (imageUrl in imageDetailsCache) return Promise.resolve(imageDetailsCache[imageUrl]); return getImageDetails(imageUrl); })().then(function(imageDetails) { function isOutside(target, related) { if (target instanceof HTMLElement) { target = target.parentNode; while (related instanceof HTMLElement) if ((related = related.parentNode) == target) return false; } return true; } function addStickerItems(direction = 1, ...elements) { if (direction && elements.length > 0) direction = direction > 0 ? 'append' : 'prepend'; else return; if (!inListing) for (let element of direction == 'append' ? elements : elements.reverse()) { if (sticker.firstChild != null) sticker[direction]('/'); sticker[direction](element); } else sticker[direction](...elements); } if (!(imageUrl in imageDetailsCache)) { imageDetailsCache[imageUrl] = imageDetails; try { sessionStorage.setItem('imageDetailsCache', JSON.stringify(imageDetailsCache)) } catch(e) { console.warn(e) } } imageDetails.src = new URL(imageDetails.src || imageUrl); if (imageDetails.width <= 0 || imageDetails.height <= 0 || imageDetails.size < 2 * 2**10 && imageDetails.width == 400 && imageDetails.height == 100 || imageDetails.size == 503) return Promise.reject('Image is invalid'); const isProxied = imageDetails.src.hostname == document.location.hostname && imageDetails.src.pathname == '/image.php'; const isPreferredHost = Array.isArray(preferredHosts) && preferredHosts.includes(imageDetails.src.hostname); const isSizeOK = acceptableSize == 0 || imageDetails.size <= acceptableSize * 2**10; const isResolutionOK = acceptableResolution == 0 || ((document.location.pathname == '/artist.php' || imageDetails.width >= acceptableResolution) && imageDetails.height >= acceptableResolution); const isTypeOK = !imageDetails.mimeType || preferredTypes.some(format => imageDetails.mimeType == 'image/' + format); sticker.onmouseenter = img.onmouseenter = evt => { sticker.style.opacity = 1 }; const friendlyHost = getHostFriendlyName(imageDetails.src.href); const resolution = span(imageDetails.width + '×' + imageDetails.height, 'resolution', isResolutionOK), size = span(formattedSize(imageDetails.size), 'size', isSizeOK), type = span(imageDetails.mimeType, 'mime-type', isTypeOK); addStickerItems(1, resolution, size); 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 (imageDetails.mimeType) addStickerItems(1, type); } else { sticker.style.backgroundColor = '#ae2300'; sticker.style.opacity = 2/3; sticker.onmouseleave = img.onmouseleave = evt => { if (isOutside(evt.currentTarget, evt.relatedTarget)) sticker.style.opacity = 2/3 }; if (inListing && groupId > 0) editOnClick(sticker); let host, convert; if (isProxied) { host = span('PROXY', 'proxy'); try { imageDetails.src = new URL(imageDetails.src.searchParams.get('i') || imageDetails.src.searchParams.values().next().value || decodeURIComponent(imageDetails.src.search.slice(1))); } catch(e) { console.warn(e) } } else if (!isPreferredHost) { host = span(friendlyHost || 'XTRN', 'external-host', false, 'Image at unpreferred image host') if (bannedFromClick2Go(friendlyHost)) host.style.color = 'orange'; } if (host instanceof HTMLElement) addStickerItems(-1, host); if (!isTypeOK) addStickerItems(1, type); if (!isSizeOK) convert = size; else if (!isTypeOK) convert = type; if (groupId > 0 && (!inListing || !bannedFromClick2Go(friendlyHost))) imageHostHelper.then(function(ihh) { function setClick2Go(elem, clickHandler, tooltip) { if (!(elem instanceof HTMLElement) || typeof clickHandler != 'function') throw 'Invalid argument'; elem.style.cursor = 'pointer'; elem.classList.add('click2go'); elem.style.transitionDuration = '0.25s'; elem.onmouseenter = evt => { evt.currentTarget.style.textShadow = '0 0 5px lime' }; elem.onmouseleave = evt => { evt.currentTarget.style.textShadow = null }; elem.onclick = clickHandler; if (tooltip) setTooltip(host, tooltip); } if (host instanceof HTMLElement) setClick2Go(host, function(evt) { evt.stopPropagation(); if (evt.currentTarget.disabled) return false; else evt.currentTarget.disabled = true; host = evt.currentTarget; img.style.opacity = 0.3; getImageMax(imageDetails.src.href).then(maxImgUrl => ihh.rehostImageLinks([maxImgUrl], true).then(ihh.singleImageGetter)) .then(rehostedImgUrl => queryAjaxAPI('groupedit', { id: groupId }, { image: rehostedImgUrl, summary: 'Cover rehost', }).then(function(response) { console.log(response); sticker.remove(); img.onload = imgLoadHandler; img.onclick = imgClickHandler; Promise.resolve(img.src = rehostedImgUrl).then(setSticker); })).catch(function(reason) { if ('logFail' in ihh) ihh.logFail(reason); img.style.opacity = 1; host.disabled = false; }); }, 'Hosted at ' + imageDetails.src.hostname + '\n(rehost to preferred image host on click)'); else if (convert instanceof HTMLElement) setClick2Go(convert, function(evt) { evt.stopPropagation(); if (evt.currentTarget.disabled) return false; else evt.currentTarget.disabled = true; convert = evt.currentTarget; img.style.opacity = 0.3; ihh.reduceImageSize(imageDetails.src.href, 2160, 90) .then(output => ihh.rehostImages([output.uri]).then(ihh.singleImageGetter) .then(rehostedImgUrl => queryAjaxAPI('groupedit', { id: groupId }, { image: rehostedImgUrl, summary: 'Cover downsize', //+ ' (' + formattedSize(imageDetails.size) + ' => ' + formattedSize(output.size) + ')', }).then(function(response) { console.log(response); sticker.remove(); img.onload = imgLoadHandler; img.onclick = imgClickHandler; Promise.resolve(img.src = rehostedImgUrl).then(setSticker); }))).catch(function(reason) { if ('logFail' in ihh) ihh.logFail(reason); img.style.opacity = 1; convert.disabled = false; }); }, 'Downsize on click'); }); } sticker.title = imageDetails.src.href; //setTooltip(sticker, imageDetails.src.href); img.insertAdjacentElement('afterend', sticker); return true; }).catch(function(reason) { img.hidden = true; sticker.style = `position: static; text-align: center; background-color: red; width: ${inListing ? '90px' : '100%'}; font-family: "Segoe UI", sans-serif; font-weight: 700; z-index: 1;`; sticker.append(span('INVALID')); if (groupId > 0 && !isSecondaryCover) editOnClick(sticker); setTooltip(sticker, reason); img.insertAdjacentElement('afterend', sticker); return false; }); } if (groupId > 0) 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) { sticker = evt.currentTarget.parentNode.querySelector('div.cover-inspector'); if (sticker != null) sticker.disabled = true; img.style.opacity = 0.3; endPoint([items[0]], true, false, true, { ctrlKey: evt.ctrlKey, shiftKey: evt.shiftKey, altKey: evt.altKey, }).then(ihh.singleImageGetter).then(imageUrl => queryAjaxAPI('groupedit', { id: groupId }, { image: imageUrl, summary: 'Cover update', }).then(function(response) { console.log(response); img.onload = imgLoadHandler; img.onclick = imgClickHandler; setSticker(img.src = imageUrl); })).catch(function(reason) { if ('logFail' in ihh) ihh.logFail(reason); if (sticker != null) sticker.disabled = false; img.style.opacity = 1; }); } evt.stopPropagation(); let 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) { if (confirm('Update torrent cover from the dropped URL?\n\n' + items[0])) dataSendHandler(ihh.rehostImageLinks); } else if (evt.dataTransfer.files.length > 0) { 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; }; }); if (hasArtworkSet(img)) return setSticker(realImgSrc(img)); if (groupId > 0) editOnClick(img); return Promise.resolve(false); } function getImagesFromTorrentGroup(torrentGroup, ihh) { if (!torrentGroup || !ihh) throw 'Invalid argument'; const dcApiToken = GM_getValue('discogs_api_token'), dcApiConsumerKey = GM_getValue('discogs_api_consumerkey'), dcApiConsumerSecret = GM_getValue('discogs_api_consumersecret'); const dcAuth = dcApiToken ? 'token=' + dcApiToken : dcApiConsumerKey && dcApiConsumerSecret ? `key=${dcApiConsumerKey}, secret=${dcApiConsumerSecret}` : null; const bareRecordLabel = label => label && label.replace(/\s+(?:Records|Recordings)$/i, ''); const lookupWorkers = [function getImagesFromWikiBody() { const links = getLinks(torrentGroup.group.wikiBody); if (!links) return Promise.reject('No active external links found in dscriptions'); return Promise.all(links.map(url => ihh.imageUrlResolver(url.href).catch(reason => null))) .then(imageUrls => imageUrls.filter(getHostFriendlyName)).then(imageUrls => imageUrls.length > 0 ? imageUrls : Promise.reject('No covers could be looked up by links in wiki body')); }]; // Ext. lookup at iTunes lookupWorkers.push(function getImagesFromUPCs() { if (torrentGroup.group.categoryId != 1 || !Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject('No torrents with UPC'); let upcs = torrentGroup.torrents.map(function(torrent) { let catNos = torrent.remasterCatalogueNumber.split('/').map(catNo => catNo.replace(/\s+/g, '')); return (catNos = catNos.filter(RegExp.prototype.test.bind(/^(\d{9,13})$/))).length > 0 ? catNos : null; }); if ((upcs = upcs.filter(Boolean)).length > 0) upcs = Array.prototype.concat.apply([ ], upcs); else return Promise.reject('No torrents with UPC'); return Promise.all(upcs.map(upc => new Promise(function(resolve, reject) { if (isNaN(upc = parseInt(upc)) || upc < 1e8) return reject('Cover lookup by UPC code not available'); const url = new URL('https://itunes.apple.com/lookup'); url.searchParams.set('upc', upc); url.searchParams.set('media', 'music'); url.searchParams.set('entity', 'album'); GM_xmlhttpRequest({ method: 'GET', url: url.href, responseType: 'json', onload: function(response) { if (response.status >= 200 && response.status < 400) if (response.response.resultCount > 0) { let artworkUrls = response.response.results.map(function(result) { const imageUrl = result.artworkUrl100 || result.artworkUrl60; return imageUrl && imageUrl.replace(/\/(\d+)x(\d+)/, '/100000x100000'); }); if ((artworkUrls = artworkUrls.filter(Boolean)).length > 0) resolve(artworkUrls); else reject('No matches'); } else reject('No matches'); else reject(defaultErrorHandler(response)); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }).catch(reason => null))).then(artworkUrls => (artworkUrls = artworkUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], artworkUrls) : Promise.reject('No covers found by UPC code(s)')); }); // Ext. lookup at MusicBrainz const mbSearch = (type, searchParams) => type && searchParams ? new Promise(function(resolve, reject) { const getFrontCovers = (type, id) => type && id ? new Promise(function(resolve, reject) { GM_xmlhttpRequest({ method: 'GET', url: 'http://coverartarchive.org/' + type + '/' + id, responseType: 'json', onload: function(response) { if (response.status >= 200 && response.status < 400) { if (!response.response.images || response.response.images.length <= 0) return reject('No artwork for this id'); let coverImages = response.response.images.filter(image => image.front || image.types && image.types.includes('Front')); //if (coverImages.length <= 0) coverImages = response.response.images; coverImages = coverImages.map(image => image.image).filter(Boolean); if (coverImages.length > 0) resolve(coverImages); else reject('No front cover for this id'); } else reject(defaultErrorHandler(response)); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }) : Promise.reject('Invalid argument'); const url = new URL('http://musicbrainz.org/ws/2/' + type + '/'); url.searchParams.set('query', Object.keys(searchParams).map(param => `${param}:"${searchParams[param]}"`).join(' AND ')); url.searchParams.set('fmt', 'json'); GM_xmlhttpRequest({ method: 'GET', url: url.href, responseType: 'json', onload: function(response) { function getFromRG(releaseGroupIds) { if (!releaseGroupIds || releaseGroupIds.size <= 0) return Promise.reject('No matches'); if (releaseGroupIds.size > 1) return Promise.reject('Ambiguous results'); releaseGroupIds = releaseGroupIds.values().next().value; return releaseGroupIds ? getFrontCovers('release-group', releaseGroupIds) : Promise.reject('No release group'); } if (response.status >= 200 && response.status < 400) if (response.response.count > 0) switch (type.toLowerCase()) { case 'release': var releaseGroupIds = new Set(response.response.releases.map(release => release['release-group'] && release['release-group'].id)); if (releaseGroupIds.size > 1) return reject('Ambiguous results'); getFromRG(releaseGroupIds).then(resolve, reason => Promise.all(response.response.releases.map(release => getFrontCovers('release', release.id).then(coverImages => coverImages && coverImages[0], reason => null))) .then(function(coverImages) { if ((coverImages = coverImages.filter(Boolean)).length > 0) resolve(coverImages); else reject('None of results has cover'); }, reject)); break; case 'release-group': releaseGroupIds = new Set(response.response['release-groups'].map(releaseGroup => releaseGroup.id)); getFromRG(releaseGroupIds).then(resolve, reject); break; default: reject('Unknown entity type'); } else reject('No matches'); else reject(defaultErrorHandler(response)); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }) : Promise.reject('Invalid argument'); lookupWorkers.push(function getImagesFromUPCs() { if (torrentGroup.group.categoryId != 1 || !Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject('Cover lookup by UPC code not available'); let upcs = torrentGroup.torrents.map(function(torrent) { let catNos = torrent.remasterCatalogueNumber.split('/').map(catNo => catNo.replace(/\s+/g, '')); return (catNos = catNos.filter(RegExp.prototype.test.bind(/^(\d{9,13})$/))).length > 0 ? catNos : null; }); if ((upcs = upcs.filter(Boolean)).length > 0) upcs = Array.prototype.concat.apply([ ], upcs); else return Promise.reject('No torrents with UPC code'); return Promise.all(upcs.map(upc => mbSearch('release', { barcode: upc }).catch(reason => null))) .then(imageUrls => (imageUrls = imageUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], imageUrls) : Promise.reject('No covers found by UPC code')); }, function getImagesFromCatNos() { if (torrentGroup.group.categoryId != 1 || !Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject('Cover lookup by label/cat.bo. not available'); let searchParams = torrentGroup.torrents.map(function(torrent) { if (!torrent.remasterRecordLabel || torrent.remasterRecordLabel.includes('/')) return null; if (!torrent.remasterCatalogueNumber || torrent.remasterCatalogueNumber.includes('/')) return null; return { label: bareRecordLabel(torrent.remasterRecordLabel), catno: torrent.remasterCatalogueNumber, }; }); if ((searchParams = searchParams.filter(Boolean)).length <= 0) return Promise.reject('No torrents with label/cat.no.'); return Promise.all(searchParams.map(searchParams => mbSearch('release', searchParams).catch(reason => null))) .then(imageUrls => (imageUrls = imageUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], imageUrls) : Promise.reject('No covers found by label/cat.bo.')); }, function getImagesFromTitleYear() { if (torrentGroup.group.categoryId != 1 || !torrentGroup.group.musicInfo.artists || torrentGroup.group.musicInfo.artists.length <= 0) return Promise.reject('Cover lookup by artist/album/year not available'); return mbSearch('release-group', { artistname: torrentGroup.group.musicInfo.artists[0].name, releasegroup: torrentGroup.group.name, firstreleasedate: torrentGroup.group.year, }); }); // Ext. lookup at Discogs, requ. credentials if (dcAuth) { const dcSearch = searchParams => new Promise(function(resolve, reject) { const url = new URL('https://api.discogs.com/database/search'); for (let key in searchParams) url.searchParams.set(key, searchParams[key]); url.searchParams.set('type', 'release'); GM_xmlhttpRequest({ method: 'GET', url: url.href, responseType: 'json', headers: { 'Authorization': 'Discogs ' + dcAuth }, onload: function(response) { if (response.status >= 200 && response.status < 400) { if (!response.response.results || response.response.results.length <= 0) return reject('No matches'); const masterIds = new Set(response.response.results.map(result => result.master_id || 0)); if (masterIds.size > 1) return reject('Ambiguous results'); const masterId = masterIds.values().next().value; (masterId != 0 ? ihh.imageUrlResolver('https://www.discogs.com/master/' + masterId) : Promise.reject('No master release')) .then(result => [Array.isArray(result) ? result[0] : result], reason => response.response.results.map(result => result.cover_image)) .then(function(coverImages) { if ((coverImages = coverImages.filter(Boolean)).length > 0) resolve(coverImages); else reject('None of results has cover'); }); } else reject(defaultErrorHandler(response)); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }); lookupWorkers.push(function getImagesFromUPCs() { if (torrentGroup.group.categoryId != 1 || !Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject('Cover lookup by UPC not available'); let upcs = torrentGroup.torrents.map(function(torrent) { let catNos = torrent.remasterCatalogueNumber.split('/').map(catNo => catNo.replace(/\s+/g, '')); return (catNos = catNos.filter(RegExp.prototype.test.bind(/^(\d{9,13})$/))).length > 0 ? catNos : null; }); if ((upcs = upcs.filter(Boolean)).length > 0) upcs = Array.prototype.concat.apply([ ], upcs); else return Promise.reject('No torrents with UPC code'); return Promise.all(upcs.map(upc => dcSearch({ barcode: upc }).catch(reason => null))).then(imageUrls => (imageUrls = imageUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], imageUrls) : Promise.reject('No covers found by UPC code')); }, function getImagesFromCatNos() { if (torrentGroup.group.categoryId != 1 || !Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject('Cover lookup by label/cat.bo. not available'); let searchParams = torrentGroup.torrents.map(function(torrent) { if (!torrent.remasterRecordLabel || torrent.remasterRecordLabel.includes('/')) return null; if (!torrent.remasterCatalogueNumber || torrent.remasterCatalogueNumber.includes('/')) return null; return { //release_title: torrentGroup.group.name, label: bareRecordLabel(torrent.remasterRecordLabel), catno: torrent.remasterCatalogueNumber, }; }); if ((searchParams = searchParams.filter(Boolean)).length <= 0) return Promise.reject('No torrents with label/cat.no.'); return Promise.all(searchParams.map(searchParams => dcSearch(searchParams).catch(reason => null))) .then(imageUrls => (imageUrls = imageUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], imageUrls) : Promise.reject('No covers found by label/cat.bo.')); }); } return (function lookup(index = 0) { if (index < lookupWorkers.length) return lookupWorkers[index]().catch(reason => lookup(index + 1)); return Promise.reject('Cover lookup by release details found nothing'); })(); } const setGroupImage = (groupId, imageUrl) => queryAjaxAPI('groupedit', { id: groupId }, { image: imageUrl, summary: 'Automated attempt to lookup cover from release details', }); function addTableHandlers(table, parent, style) { function addHeaderButton(caption, clickHandler, id, tooltip) { if (!caption || typeof clickHandler != 'function') return; const elem = document.createElement('SPAN'); if (id) elem.id = id; elem.className = 'brackets'; elem.style = 'margin-right: 5pt; cursor: pointer; font-weight: normal; transition: color 0.25s;'; elem.textContent = caption; elem.onmouseenter = evt => { evt.currentTarget.style.color = 'orange' }; elem.onmouseleave = evt => { evt.currentTarget.style.color = evt.currentTarget.dataset.color || null }; elem.onclick = clickHandler; if (tooltip) elem.title = tooltip; //setTooltip(tooltip); container.append(elem); } 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; } if (!(table instanceof HTMLElement) || !(parent instanceof HTMLElement)) return; const images = table.querySelectorAll('tbody > tr div.group_image > img'); if (!(images.length > 0)) return; const container = document.createElement('DIV'); container.className = 'cover-inspector'; if (style) container.style = style; addHeaderButton('Inspect all covers', function(evt) { evt.currentTarget.hidden = true; const currentTarget = evt.currentTarget, inspectWorkers = [ ]; for (let tr of table.querySelectorAll('tbody > tr.group, tbody > tr.torrent')) { const img = tr.querySelector('div.group_image > img'); if (img != null) inspectWorkers.push(inspectImage(img, getGroupId(tr.querySelector('div.group_info')))); } (inspectWorkers.length > 0 ? imageHostHelper.then(ihh => Promise.all(inspectWorkers).then(function(results) { currentTarget.id = 'rehost-all-covers'; currentTarget.textContent = 'Rehost all'; currentTarget.onclick = function(evt) { evt.currentTarget.remove(); for (let elem of table.querySelectorAll('div.cover-inspector > span.click2go:not([disabled])')) elem.click(); }; currentTarget.style.color = currentTarget.dataset.color = 'dodgerblue'; currentTarget.hidden = false; })) : Promise.reject('Nothing to rehost')).catch(reason => { currentTarget.remove() }); }, 'inspect-all-covers'); imageHostHelper.then(function(ihh) { function setCoverFromTorrentGroup(torrentGroup, img) { if (!torrentGroup) throw 'Invalid argument'; return getImagesFromTorrentGroup(torrentGroup, ihh).then(imageUrls => ihh.rehostImageLinks(imageUrls[0], true, false, false).then(ihh.singleImageGetter).then(imageUrl => setGroupImage(torrentGroup.group.id, imageUrl).then(function(response) { if (img instanceof HTMLImageElement) { img.onload = function(evt) { evt.currentTarget.hidden = false; inspectImage(evt.currentTarget, torrentGroup.group.id); }; img.src = imageUrl; img.removeAttribute('onclick'); img.onclick = evt => { lightbox.init(evt.currentTarget.src, 90) }; } console.log('[Cover Inspector]', response); if (autoOpenSucceed) openGroup(torrentGroup.group.id); }))).catch(reason => autoOpenWithLink && Array.isArray(torrentGroup.torrents) && torrentGroup.torrents.length > 0 ? Promise.all(torrentGroup.torrents.filter(torrent => torrent.description && /\b(?:https?):\/\//i.test(torrent.description)).map(torrent => bb2Html(torrent.description).then(getLinks, reason => null))).then(function(urls) { if ((urls = urls.filter(Boolean)).length <= 0) return Promise.reject(reason); console.log('[Cover Inspector] Links found in torrent descriptions for %d:', torrentGroup.group.id, urls); openGroup(torrentGroup.group.id); }) : Promise.reject(reason)); } const missingImages = Array.prototype.filter.call(images, img => !hasArtworkSet(img)).length; if (images.length <= 0 || missingImages > 0) addHeaderButton('Add missing covers', function(evt) { evt.currentTarget.remove(); table.querySelectorAll('tbody > tr.group, tbody > tr.torrent').forEach(function(tr) { const groupId = getGroupId(tr.querySelector('div.group_info')), img = tr.querySelector('div.group_image > img'); if (groupId > 0) if (img instanceof HTMLImageElement) { if (!hasArtworkSet(img)) queryAjaxAPI('torrentgroup', { id: groupId }) .then(torrentGroup => setCoverFromTorrentGroup(torrentGroup, img)); } else queryAjaxAPI('torrentgroup', { id: groupId }).then(function(torrentGroup) { if (!torrentGroup.group.wikiImage) return setCoverFromTorrentGroup(torrentGroup); }); }); }, 'auto-add-covers', missingImages + ' missing covers'); addHeaderButton('Fix invalid covers', function(evt) { evt.currentTarget.remove(); let elem = document.getElementById('auto-add-covers'); if (elem != null) elem.remove(); table.querySelectorAll('tbody > tr.group, tbody > tr.torrent').forEach(function(tr) { const groupId = getGroupId(tr.querySelector('div.group_info')), img = tr.querySelector('div.group_image > img'); if (groupId > 0) if (img instanceof HTMLImageElement) (hasArtworkSet(img) ? ihh.verifyImageUrl(img.src) : Promise.reject('Not set')).catch(reason => queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup => setCoverFromTorrentGroup(torrentGroup, img))); else queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup => ihh.verifyImageUrl(torrentGroup.group.wikiImage).catch(reason => setCoverFromTorrentGroup(torrentGroup))); }); }, 'auto-fix-covers'); }); parent.append(container); } const params = new URLSearchParams(document.location.search), id = parseInt(params.get('id')) || undefined; switch (document.location.pathname) { case '/artist.php': { if (!(id > 0)) break; document.body.querySelectorAll('div.box_image > div > img').forEach(inspectImage); const table = document.getElementById('discog_table'); if (table != null) addTableHandlers(table, table.querySelector(':scope > div.box'), 'display: block; text-align: right;'); //color: cornsilk; background-color: slategrey;' break; } case '/torrents.php': { if (id > 0) { for (let img of document.body.querySelectorAll('div#covers p > img')) inspectImage(img, id); 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(ihh.singleImageGetter).then(rehostedImage => queryAjaxAPI('groupedit', { id: id }, { image: rehostedImage, 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; setTooltip(a, 'Alt + click to set release cover from this URL (or use context menu command)'); } return true; } for (let a of document.body.querySelectorAll([ 'div.torrent_description > div.body a', 'table#torrent_details > tbody > tr.torrentdetails > td > blockquote a', ].join(', '))) if (!noCoverHeres.includes(a.hostname)) setAnchorHandlers(a); GM_registerMenuCommand('Auto add cover', function() { queryAjaxAPI('torrentgroup', { id: id }).then(torrentGroup => getImagesFromTorrentGroup(torrentGroup, ihh).then(imageUrls => ihh.rehostImageLinks([imageUrls[0]], true, false, false).then(ihh.singleImageGetter).then(imageUrl => setGroupImage(torrentGroup.group.id, imageUrl).then(function(response) { console.log('[Cover Inspector]', response); const img = document.body.querySelector('div#covers p > img'); if (img != null) img.src = imageUrl; else throw 'Cover preview could not be located'; img.onclick = imgClickHandler; inspectImage(img, id); })))).catch(alert); }, 'A'); }); } else { const table = document.body.querySelector('table.torrent_table'); if (table != null) { const parent = Array.prototype.find.call(table.querySelectorAll(':scope > tbody > tr.colhead > td'), td => /^\s*(?:Torrents?|Name)\b/.test(td.textContent)); if (parent) addTableHandlers(table, parent, 'display: inline-block; margin-left: 3em;'); } } break; } case '/collages.php': { if (![20036, 31445].includes(id)) break; let userAuth = document.body.querySelector('input[name="auth"]'); if (userAuth != null) userAuth = userAuth.value; else throw 'User auth could not be located'; const removeFromCollage = groupId => new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest, payLoad = new URLSearchParams({ action: 'manage_handle', collageid: id, groupid: groupId, 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); }); const getAllCovers = groupId => groupId > 0 ? new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest; xhr.open('GET', 'torrents.php?' + new URLSearchParams({ id: groupId }).toString(), true); xhr.responseType = 'document'; xhr.onload = function() { if (this.status >= 200 && this.status < 400) resolve(Array.from(this.response.querySelectorAll('div#covers div > p > img'), realImgSrc)); else reject(defaultErrorHandler(this)); }; xhr.onerror = function() { reject(defaultErrorHandler(xhr)) }; xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) }; xhr.send(); }) : Promise.reject('Invalid argument'); imageHostHelper.then(function(ihh) { function fixCollagePage(evt) { evt.currentTarget.remove(); const autoHideFailed = GM_getValue('auto_hide_failed', false); document.body.querySelectorAll('table#discog_table > tbody > tr').forEach(function(tr) { function setStatus(status, tooltip) { if ((td = tr.querySelector('td.status')) == null) return; // assertion failed td.textContent = (status = Number(status) || 0) >= 2 ? 'success' : 'failed'; td.className = 'status ' + td.textContent + ' status-code-' + status; if (Array.isArray(tooltip)) tooltip = tooltip.join('\n'); 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) if (autoHideFailed) tr.hidden = true; else if ((td = document.getElementById('hide-status-failed')) != null) td.hidden = false; } const inspectGroupId = groupId => queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup => ihh.verifyImageUrl(torrentGroup.group.wikiImage).then(function(imageUrl) { let status = 3, tooltip = ['This release seems to have a valid image']; setStatus(status, tooltip); const rfc = () => removeFromCollage(torrentGroup.group.id).then(function(statusCode) { tooltip.push('(removed from collage)'); setStatus(status, tooltip); }); if (torrentGroup.group.categoryId == 1) getAllCovers(torrentGroup.group.id).then(imageUrls => Promise.all(imageUrls.slice(1).map(ihh.verifyImageUrl)).then(rfc, function(reason) { tooltip.push('(invalid additional cover(s) require attention)', reason); setStatus(status = 1, tooltip); }), function(reason) { tooltip.push('Could not count additiona covers (' + reason + ')'); setStatus(status = 2, tooltip); }); else rfc(); if (new URL(imageUrl).hostname != 'ptpimg.me/') ihh.rehostImageLinks([imageUrl], true, false, true) .then(ihh.singleImageGetter).then(imageUrl => queryAjaxAPI('groupedit', { id: torrentGroup.group.id }, { image: imageUrl, summary: 'Automated cover rehost', }).then(function(response) { tooltip.push('(' + response + ')'); setStatus(status, tooltip); console.log('[Cover Inspector]', response); })); if (autoOpenSucceed) openGroup(torrentGroup.group.id); }).catch(reason => getImagesFromTorrentGroup(torrentGroup, ihh).then(imageUrls => ihh.rehostImageLinks([imageUrls[0]], true, false, false).then(results => results.map(ihh.directLinkGetter)).then(imageUrls => setGroupImage(torrentGroup.group.id, imageUrls[0]).then(function(response) { let status = 3; const tooltip = [response, '(reminder - release may contain additional covers to review)']; if (imageUrls.length > 1) { status = 2; tooltip.push('(more external links in description require attention)'); } setStatus(status, tooltip); console.log('[Cover Inspector]', response); const rfc = () => removeFromCollage(torrentGroup.group.id).then(function(status) { tooltip.push('(removed from collage)'); setStatus(status, tooltip); }); /*if (torrentGroup.group.categoryId == 1) getAllCovers(torrentGroup.group.id).then(imageUrls => Promise.all(imageUrls.slice(1).map(ihh.verifyImageUrl)).then(rfc, function(reason) { tooltip.push('(invalid additional cover(s) require attention)'); setStatus(status = 1, tooltip); }), function(reason) { tooltip.push('Could not count additiona covers (' + reason + ')'); setStatus(status = 2, tooltip); }); else */rfc(); if (autoOpenSucceed) openGroup(torrentGroup.group.id); })))).catch(reason => Array.isArray(torrentGroup.torrents) && torrentGroup.torrents.length > 0 ? Promise.all(torrentGroup.torrents.filter(torrent => torrent.description && /\b(?:https?):\/\//i.test(torrent.description)).map(torrent => bb2Html(torrent.description).then(getLinks, reason => null))).then(function(urls) { if ((urls = urls.filter(Boolean)).length <= 0) return Promise.reject(reason); setStatus(1, 'No active external links in album description,\nbut release descriptions contain some:\n\n' + (urls = Array.prototype.concat.apply([ ], urls)).join('\n')); console.log('[Cover Inspector] Links found in torrent descriptions for', torrentGroup, ':', urls); if (autoOpenWithLink) openGroup(torrentGroup.group.id); }) : Promise.reject(reason))).catch(reason => { setStatus(0, reason) }); let td = document.createElement('TD'); tr.append(td); if (tr.classList.contains('colhead_dark')) { 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); } else if (/^group_(\d+)$/.test(tr.id)) { td.className = 'status'; td.style.opacity = 0.3; td.textContent = 'unknown'; let groupId = tr.querySelector(':scope > td > strong > a:last-of-type'); if (groupId != null && (groupId = parseInt(new URLSearchParams(groupId.search).get('id'))) > 0 || (groupId = /^group_(\d+)$/.exec(tr.id)) != null && (groupId = parseInt(groupId[1])) > 0) { inspectGroupId(groupId); } else setStatus(0, 'Could not extract torrent id'); } }); } const td = document.body.querySelector('table#discog_table > tbody > tr.colhead_dark > td:nth-of-type(3)'); if (td != null) { function addButton(caption, clickHandler, id, color = 'currentcolor', visible = true, tooltip) { if (!caption || typeof clickHandler != 'function') throw 'Invalid argument'; const elem = document.createElement('SPAN'); if (id) elem.id = id; elem.className = 'brackets'; elem.textContent = caption; elem.style = `float: right; margin-right: 1em; cursor: pointer; color: ${color};`; elem.onclick = clickHandler; if (!visible) elem.hidden = true; if (tooltip) elem.title = tooltip; td.append(elem); return elem; } addButton('Try to add covers', fixCollagePage, 'auto-add-covers', 'gold'); addButton('Hide failed', function(evt) { evt.currentTarget.hidden = true; document.body.querySelectorAll('table#discog_table > tbody > tr[id] td.status.status-code-0') .forEach(td => { td.parentNode.hidden = true }) }, 'hide-status-failed', undefined, false); } }); break; } }