// ==UserScript== // @name [RED] Cover Inspector // @namespace https://greasyfork.org/users/321857-anakunda // @version 1.13.15 // @run-at document-end // @description Easify & speed-up finding and updating of invalid, missing or non 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 // @match https://redacted.ch/torrents.php?* // @match https://redacted.ch/artist.php?id=* // @match https://redacted.ch/collages.php?id=* // @match https://redacted.ch/collages.php?page=*&id=* // @match https://redacted.ch/collage.php?id=* // @match https://redacted.ch/collage.php?page=*&id=* // @match https://redacted.ch/userhistory.php?action=subscribed_collages // @match https://redacted.ch/userhistory.php?page=*&action=subscribed_collages // @connect * // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_openInTab // @grant GM_registerMenuCommand // @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'/*, 'i.imgur.com'*/], }[document.domain]; const preferredTypes = GM_getValue('preferred_types', ['jpeg', 'png', 'gif'].map(type => 'image/' + type)); 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; } const uaVersions = { }; function setUserAgent(params, suffixLen = 8) { if (params && typeof params == 'object' && httpParser.test(params.url)) try { const url = new URL(params.url); if ([document.location.hostname, 'ptpimg.me'].includes(url.hostname)) return; //return ['dzcdn.', 'mzstatic.com'].some(pattern => hostname.includes(pattern)); params.anonymous = true; if (!navigator.userAgent) return; if (!uaVersions[url.hostname] || ++uaVersions[url.hostname].usageCount > 16) uaVersions[url.hostname] = { versionSuffix: Math.floor(Math.random() * Math.pow(2, suffixLen * 4)).toString(16).padStart(suffixLen, '0'), usageCount: 1, }; if (!params.headers) params.headers = { }; params.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 + '.' + uaVersions[url.hostname].versionSuffix); } catch(e) { console.warn('Invalid url:', params.url) } } 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'); 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 fineResolution = getPreference('fine_cover_resolution', 500); let acceptableResolution = getPreference('acceptable_cover_resolution', 300); if (fineResolution > 0 && acceptableResolution > fineResolution) acceptableResolution = fineResolution; 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'], 'AcousticSounds': ['acousticsounds.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'], 'Goodreads': ['i.gr-assets.com'], 'GooPics': ['goopics.net'], 'HDtracks': ['cdn.hdtracks.com'], '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'], 'RA': ['residentadvisor.net'], '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; } function noCoverHere(url) { if (!url || !url.protocol.startsWith('http')) return true; let str = url.hostname.toLowerCase(); if ([ document.location.hostname, 'redacted.ch', 'orpheus.network', 'apollo.rip', 'notwhat.cd', 'dicmusic.club', 'what.cd', 'jpopsuki.eu', 'rutracker.net', 'github.com', 'gitlab.com', 'db.etree.org', 'youri-egoro', 'dr.loudness-war.info', 'ptpimg.me', 'imgur.com', '2i.cz', 'abload.de', 'allthepics.net', 'bilder-upload.eu', 'casimages.com', 'catbox.moe', 'cubeupload.com', 'dibpic.com', 'discordapp.net', 'extraimage.org', 'fastpic.ru', 'fastpic.org', 'forumbilder.com', 'freeimage.host', 'funkyimg.com', 'ge.tt', 'geekpic.net', 'getapic.me', 'gifyu.com', 'goopics.net', 'image.cx', 'imageban.ru', 'imagekit.io', 'imagensbrasil.org', 'imageride.com', 'imagetot.com', 'imagevenue.com', 'imgbank.cz', 'ibb.co', 'imgbox.com', 'imgcdn.dev', 'imgoo.com', 'imgpile.com', 'imgsha.com', 'png8.com', 'ipev.ru', 'jerking.empornium.ph', 'lensdump.com', 'prntscr.com', 'lostpic.net', 'lut.im', 'noelshack.com', 'photobucket.com', 'picabox.ru', 'free-picload.com', 'pimpandhost.com', 'pinimg.com', 'pixhost.to', 'pomf.cat', 'postimg.cc', 'thesungod.xyz', 'radikal.ru', 'savephoto.ru', 'slow.pics', 'sm.ms', 'svgshare.com', 'twimg.com', 'upimager.com', 'uupload.ir', 'vgy.me', 'z4a.net', 'imgchr.com', ].concat(GM_getValue('no_covers_here', [ ])).some(hostName => hostName && (str == (hostName = hostName.toLowerCase()) || str.endsWith('.' + hostName)))) return true; str = url.pathname.toLowerCase(); const pathParts = { 'discogs.com': ['artist', 'label', 'user'].map(folder => '/' + folder + '/'), }; for (let domain in pathParts) if ((url.hostname == domain || url.hostname.endsWith('.' + domain)) && pathParts[domain].some(pathPart => str.includes(pathPart.toLowerCase()))) return true; return false; } const musicResourceDomains = [ '7static.com', 'archive.org', 'bcbits.com', 'beatport.com', 'boomkat.com', 'cloudfront.net', 'coverartarchive.org', 'discogs.com', 'dzcdn.net', 'ebayimg.com', 'genius.com', 'highresaudio.com', 'i.gr-assets.com', 'junodownload.com', 'last.fm', 'lastfm.freetls.fastly.net', 'media-amazon.com', 'metal-archives.com', 'mora.jp', 'mzstatic.com', 'progarchives.com', 'qobuz.com', 'rovicorp.com', 'sndcdn.com', 'ssl-images-amazon.com', 'tidal.com', 'traxsource.com', 'vgm.io', 'vgmdb.net', 'wikimedia.org', 'residentadvisor.net', 'hdtracks.com', 'acousticsounds.com', ]; const click2goHostLists = [ GM_getValue('click2go_blacklist', ['imgur.com', 'amazonaws.com']), GM_getValue('click2go_whitelist', musicResourceDomains.concat([ 'discordapp.net', 'forumbilder.com', 'jpopsuki.eu', 'pinimg.com', 'shopify.com', 'twimg.com', ])), GM_getValue('click2go_badlist', ['photobucket.com']), ]; const getDomainListIndex = (domain, listNdx) => domain && Array.isArray(listNdx = click2goHostLists[listNdx]) ? (domain = domain.toLowerCase(), listNdx.findIndex(domain2 => domain2.toLowerCase() == domain)) : -1; const isOnDomainList = (domain, listNdx) => getDomainListIndex(domain, listNdx) >= 0; 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/'); const singleResultGetter = result => Array.isArray(result) ? result[0] : result; 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) try { var imageUrl = new URL(src[1]) } catch(e) { console.warn(e) } } if (!imageUrl) try { imageUrl = new URL(img.src) } catch(e) { console.warn('Invalid IMG source: img.src'); return undefined; } if (imageUrl.hostname.endsWith('.imgur.com')) imageUrl.pathname = imageUrl.pathname.replace(/\/(\w{7,})m\.(\w+)$/, '/$1.$2'); return imageUrl.href; } function deProxifyImgSrc(imageUrl) { if (!imageUrl) throw 'Invalid argument'; if (httpParser.test(imageUrl)) try { imageUrl = new URL(imageUrl); if (imageUrl.hostname == document.location.hostname && imageUrl.pathname == '/image.php' && (imageUrl = imageUrl.searchParams.get('i')) && httpParser.test(imageUrl)) return imageUrl; } catch (e) { console.warn(e) } } function openGroup(groupId) { if (!(groupId > 0)) throw 'Invalid argument'; const url = new URL('/torrents.php', document.location.origin); url.searchParams.set('id', groupId); GM_openInTab(url.href, 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 ihh.verifyImageUrl(sub); }).catch(reason => ihh.verifyImageUrl(imageUrl))); } if ('imageDetailsCache' in sessionStorage) try { var imageDetailsCache = JSON.parse(sessionStorage.getItem('imageDetailsCache')); } catch(e) { console.warn(e) } if (!imageDetailsCache || typeof imageDetailsCache != 'object') imageDetailsCache = { }; function getImageDetails(imageUrl) { if (!imageUrl) throw 'Invalid argument'; if (!httpParser.test(imageUrl)) return Promise.reject('Invalid URL'); return imageUrl in imageDetailsCache ? Promise.resolve(imageDetailsCache[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 loading error (' + image.src + ')') }; image.loading = 'eager'; image.referrerPolicy = 'same-origin'; image.src = imageUrl; }), (function getRemoteFileSize() { const getByXHR = (method = 'GET') => new Promise(function(resolve, reject) { const params = { method: method, url: imageUrl, binary: true, timeout: 90e3, responseType: 'blob' }; setUserAgent(params); 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'))*/; })().catch(function(reason) { console.warn(`[Cover Inspector] Failed to get remote image size (${imageUrl}):`, reason); return null; }), (function getRemoteFileType() { const getByXHR = (method = 'GET') => new Promise(function(resolve, reject) { const params = { method: method, url: imageUrl, timeout: 90e3 }; setUserAgent(params); let contentType, hXHR = GM_xmlhttpRequest(Object.assign(params, { onreadystatechange: function(response) { if (contentType != undefined || response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return; const abort = () => { if (!hXHR) return; if (method != 'HEAD') hXHR.abort(); hXHR = undefined; } if (response.status < 200 || response.status >= 400) { reject(defaultErrorHandler(response)); return abort(); } const invalidUrls = [ 'imgur.com/removed.png', 'gtimg.cn/music/photo_new/T001M000003kfNgb0XXvgV_0.jpg', '//discogs.com/8ce89316e3941a67b4829ca9778d6fc10f307715/images/spacer.gif', 'amazon.com/images/I/31CTP6oiIBL.jpg', 'amazon.com/images/I/31zMd62JpyL.jpg', 'amazon.com/images/I/01RmK+J4pJL.gif', '/0dc61986-bccf-49d4-8fad-6b147ea8f327.jpg', '/ab2d1d04-233d-4b08-8234-9782b34dcab8.jpg', 'postimg.cc/wkn3jcyn9/image.jpg', 'tinyimg.io/notfound', 'hdtracks.com/img/logo.jpg', ]; if (invalidUrls.some(invalidUrl => response.finalUrl.endsWith(invalidUrl))) { reject('Dummy image (placeholder): ' + response.finalUrl); return abort(); } const invalidEtags = [ 'd835884373f4d6c8f24742ceabe74946', '25d628d3d3a546cc025b3685715e065f42f9cbb735688b773069e82aac16c597f03617314f78375d143876b6d8421542109f86ccd02eab6ba8b0e469b67dc953', '"55fade2068e7503eae8d7ddf5eb6bd09"', '"1580238364"', '"rbFK6Ned4SXbK7Fsn+EfdgKVO8HjvrmlciYi8ZvC9Mc"', '7ef77ea97052c1abcabeb44ad1d0c4fce4d269b8a4f439ef11050681a789a1814fc7085a96d23212af594b6b2855c99f475b8b61d790f22b9d71490425899efa', ]; const Etag = /^(?:Etag)\s*:\s*(.+?)\s*$/im.exec(response.responseHeaders); if (Etag != null && invalidEtags.some(etag => etag.toLowerCase() == Etag[1].toLowerCase())) { reject('Dummy image (placeholder): ' + response.finalUrl); return abort(); } contentType = /^(?:Content-Type)\s*:\s*(.+?)(?:\s*;(.+?))?\s*$/im.exec(response.responseHeaders); resolve(contentType != null ? contentType[1].toLowerCase() : null); abort(); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, })); }); return getByXHR('HEAD').catch(reason => /^HTTP error (?:400|403|405|406|416)\b/.test(reason) ? getByXHR('GET') : Promise.reject(reason)); })(), ]).then(results => ({ src: results[0].src, width: results[0].naturalWidth, height: results[0].naturalHeight, size: results[1], mimeType: results[2], localProxy: false, })).then(function(imageDetails) { if (imageDetails.width <= 0 || imageDetails.height <= 0) return Promise.reject('Zero area'); const deproxiedSrc = deProxifyImgSrc(imageDetails.src); if (deproxiedSrc) return getImageDetails(deproxiedSrc) .then(imageDetails => Object.assign({ }, imageDetails, { localProxy: true })); // if (imageDetails.size < 2 * 2**10 && imageDetails.width == 400 && imageDetails.height == 100) // return Promise.reject('Known placeholder image'); // if (imageDetails.size == 503) return Promise.reject('Known placeholder image'); if (!(imageUrl in imageDetailsCache)) { imageDetailsCache[imageUrl] = imageDetails; try { sessionStorage.setItem('imageDetailsCache', JSON.stringify(imageDetailsCache)) } catch(e) { console.warn(e) } } return imageDetails; }); } const bb2Html = bbBody => queryAjaxAPI('preview', undefined, { body: bbBody }); let userAuth = document.body.querySelector('input[name="auth"]'); if (userAuth != null) userAuth = userAuth.value; else if ((userAuth = document.body.querySelector('#nav_logout > a')) != null) { userAuth = new URLSearchParams(userAuth.search); userAuth = userAuth.get('auth') || null; } if (!userAuth) console.warn('[Cover Inspector] Failed to extract user auth key, removal from collages will be unavailable'); const badCoverCollages = { 'redacted.ch': [20036, 31445, 31735], }[document.domain] || [ ]; const inCollage = (torrentGroup, collageIndex) => Array.isArray(badCoverCollages) && badCoverCollages[collageIndex] > 0 && torrentGroup && Array.isArray(torrentGroup.group.collages) && torrentGroup.group.collages.some(collage => collage.id == badCoverCollages[collageIndex]); function addToCollage(collageIndex, groupId) { if (!Array.isArray(badCoverCollages)) return Promise.reject('Cover related collages not defined for current site'); if (!(badCoverCollages[collageIndex] > 0) || !(groupId > 0)) throw 'Invalid argument'; return ajaxApiKey ? queryAjaxAPI('addtocollage', { collageid: badCoverCollages[collageIndex] }, { groupids: groupId }).then(function(response) { if (response.groupsadded.includes(groupId)) return Promise.resolve('Added'); if (response.groupsrejected.includes(groupId)) return Promise.reject('Rejected'); if (response.groupsduplicated.includes(groupId)) return Promise.reject('Duplicated'); return Promise.reject('Unknown status'); }) : Promise.reject('API key not set'); } function removeFromCollage(collageId, groupId) { if (!(collageId > 0) || !(groupId > 0)) throw 'Invalid argument'; return userAuth ? new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest, payLoad = new URLSearchParams({ action: 'manage_handle', collageid: collageId, 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); }) : Promise.reject('Not supported on this page'); } const testImageQuality = imageUrl => acceptableResolution > 0 ? getImageDetails(imageUrl) .then(imageDetails => Math.min(imageDetails.width, imageDetails.height) < acceptableResolution ? Promise.reject('Poor image resolution') : imageDetails.width * imageDetails.height) : Promise.resolve(-1); function getLinks(descBody) { if (!descBody) return null; if (typeof descBody == 'string') descBody = domParser.parseFromString(descBody, 'text/html'); if (descBody instanceof Document) descBody = descBody.getElementsByTagName('A'); else throw 'Invalid argument'; if (descBody.length > 0) descBody = Array.from(descBody, function(a) { if (a.href && a.target == '_blank') try { return new URL(a) } catch(e) { console.warn(e) } return null; }).filter(url => url instanceof URL && !noCoverHere(url)); return descBody.length > 0 ? descBody : null; } function isMusicResource(imageUrl) { if (imageUrl) try { imageUrl = new URL(imageUrl); const domain = imageUrl.hostname.split('.').slice(-2).join('.').toLowerCase(); return musicResourceDomains.some(domain2 => domain2.toLowerCase() == domain); } catch (e) { console.warn(e) } return false; } function setGroupImage(groupId, imageUrl, summary = 'Automated attempt to lookup cover') { if (!(groupId > 0) || !imageUrl) throw 'Invalid argument'; return queryAjaxAPI('groupedit', { id: groupId }, { image: imageUrl, summary: summary }); } function autoLookupSummary(reason) { const summary = 'Automated attempt to lookup cover'; if (/^(?:not set|unset|missing)$/i.test(reason)) reason = 'missing'; else if (/\b(?:error|timeout)\b/i.test(reason)) reason = 'link broken'; return reason ? summary + ' (' + reason + ')' : summary; } function setNewSrc(img, src) { if (!(img instanceof HTMLImageElement) || !src) throw 'Invalid argument'; img.onload = function(evt) { if (evt.currentTarget.style.opacity < 1) evt.currentTarget.style.opacity = 1; evt.currentTarget.hidden = false; } img.onerror = evt => { evt.currentTarget.hidden = true }; if (img.hasAttribute('onclick')) img.removeAttribute('onclick'); img.onclick = evt => { lightbox.init(evt.currentTarget.src, 220) }; img.src = src; } function counterDecrement(id, tableIndex) { if (!id) throw 'Invalid argument'; let elem = 'div.cover-inspector'; if (tableIndex) elem += '-' + tableIndex; elem += ' span.' + id; if ((elem = document.body.querySelector(elem)) == null || !(elem.count > 0)) return; if (--elem.count > 0) elem.textContent = elem.count; else { (elem = elem.parentNode).textContent = 'Batch completed'; elem.style.color = 'green'; elem.style.fontWeight = 'bold'; setTimeout(function(elem) { elem.style.transition = 'opacity 2s ease-in-out'; elem.style.opacity = 0; setTimeout(elem => { elem.remove() }, 2000, elem); }, 4000, elem); } } function inspectImage(img, groupId) { if (!(img instanceof HTMLImageElement)) throw 'Invalid argument'; if (img.parentNode != null) img.parentNode.style.position = 'relative'; else return Promise.resolve(-1); for (var inListing = img; inListing != null; inListing = inListing.parentNode) if (inListing.nodeName == 'DIV') if (inListing.classList.contains('group_image')) { inListing = true; break; } else if (inListing.classList.contains('box_image')) { inListing = false; break; } if (typeof inListing != 'boolean') throw 'Unexpected cover context'; let isSecondaryCover = !inListing && /^cover_(\d+)$/.test(img.id), sticker; isSecondaryCover = Boolean(isSecondaryCover) && !(parseInt(isSecondaryCover[1]) > 0); if (groupId && isSecondaryCover) groupId = undefined; function editOnClick(elem, lookupFirst = 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'; if (elem.hasAttribute('onclick')) elem.removeAttribute('onclick'); elem.onclick = function(evt) { if (evt.currentTarget.disabled) return false; else evt.currentTarget.disabled = true; (lookupFirst ? findCover(groupId, img) : Promise.reject('Lookup disabled')).catch(function() { 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; }; } function setSticker(imageUrl) { if ((sticker = img.parentNode.querySelector('div.cover-inspector')) != 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; justify-content: flex-end; cursor: default; transition-duration: 0.25s; z-index: 1; ${inListing ? 'flex-flow: column; right: 0; bottom: 0; padding: 1pt 0 2pt; font-size: 6.5pt; text-align: right; line-height: 8pt;' : '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'); 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 (imageDetails.localProxy) setNewSrc(img, imageDetails.src); imageDetails.src = new URL(imageDetails.src || imageUrl); const isPreferredHost = Array.isArray(preferredHosts) && preferredHosts.includes(imageDetails.src.hostname); const isSizeOK = !(acceptableSize > 0) || imageDetails.size <= acceptableSize * 2**10; const isResolutionAcceptable = !(acceptableResolution > 0) || ((document.location.pathname == '/artist.php' || imageDetails.width >= acceptableResolution) && imageDetails.height >= acceptableResolution); const isResolutionFine = isResolutionAcceptable && (!(fineResolution > 0) || ((document.location.pathname == '/artist.php' || imageDetails.width >= fineResolution) && imageDetails.height >= fineResolution)); const isTypeOK = !imageDetails.mimeType || preferredTypes.some(type => imageDetails.mimeType.toLowerCase() == type); const friendlyHost = getHostFriendlyName(imageDetails.src.href); const resolution = span(imageDetails.width + '×' + imageDetails.height, 'resolution', isResolutionFine), size = span(formattedSize(imageDetails.size), 'size', isSizeOK), type = span(imageDetails.mimeType, 'mime-type', isTypeOK); let domain = imageDetails.src.hostname.split('.').slice(-2).join('.'); let host, downsize, lookup; addStickerItems(1, resolution, size); if (isPreferredHost && isSizeOK && isResolutionFine && 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 { function keyHandlers(evt) { if (evt.altKey) { if (!click2goHostLists.some((_, listNdx) => isOnDomainList(domain, listNdx)) || !confirm(`This will remove "${domain}" from all domain lists for batch processing`)) return false; for (let listNdx of click2goHostLists.keys()) { const domainNdx = getDomainListIndex(domain, listNdx); if (domainNdx < 0) continue; click2goHostLists[listNdx].splice(domainNdx, 1); GM_setValue('click2go_' + ['black', 'white', 'bad'][listNdx] + 'list', click2goHostLists[listNdx]); } alert('All host lists successfully updated. The change will apply on next batch scan.'); } else if (evt.ctrlKey || evt.shiftKey) { const listNdx = (evt.ctrlKey << 1 | evt.shiftKey << 0) - 1; if (isOnDomainList(domain, listNdx) || !confirm([ `This will exclude "${domain}" from batch rehosting`, `This will force include "${domain}" in batch rehosting`, `This will consider "${domain}" bad host (new cover will be looked up)`, ][listNdx])) return false; click2goHostLists[listNdx].push(domain); GM_setValue('click2go_' + ['black', 'white', 'bad'][listNdx] + 'list', click2goHostLists[listNdx]); alert([ 'Hosts blacklist successfully updated. The change will apply on next batch scan.', 'Hosts whitelist successfully updated.', 'Hosts badlist successfully updated. The change will apply on next batch scan.', ][listNdx]); } return false; } function getHostTooltip() { let tooltip = 'Hosted at ' + imageDetails.src.hostname; if (imageDetails.localProxy) tooltip += ' (locally proxied)'; if (isOnDomainList(domain, 2)) tooltip += ' (bad host)'; else if (isOnDomainList(domain, 0)) tooltip += ' (blacklisted from batch rehosting)'; else if (isOnDomainList(domain, 1)) tooltip += ' (whitelisted for batch rehosting)'; if (isOnDomainList(domain, 2)) tooltip += '\n(look up different version on simple click)'; else if (!inListing || !isOnDomainList(domain, 0)) tooltip += '\n(rehost to preferred host on simple click)'; return tooltip + ` For host classification: Shift + click to ban domain from batch rehosts Ctrl + click to whitelist domain in batch rehosts Ctrl + Shift + click to mark domain as bad (will be replaced regardless of link validity) Alt + click to remove domain from all lists`; } 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); if (!isResolutionFine) if (isResolutionAcceptable) { let color = acceptableResolution > 0 ? acceptableResolution : 0; color = (Math.min(imageDetails.width, imageDetails.height) - color) / (fineResolution - color); color = 0xFFFF90 + Math.round((0xC0 - 0x90) * color); resolution.style.color = '#' + color.toString(16); setTooltip(resolution, 'Mediocre image quality (resolution)'); } else if (groupId > 0) lookup = resolution; if (!isPreferredHost) { host = span(friendlyHost || 'XTRN', 'xtrn-host', false); if (imageDetails.localProxy) host.classList.add('local-proxy'); } if (host instanceof HTMLElement) { if (isOnDomainList(domain, 0)) { host.style.color = '#ffd'; if (inListing) host.classList.add('blacklisted-from-click2go'); } else if (isOnDomainList(domain, 1)) { if (inListing) host.classList.add('whitelisted'); } else if (!isOnDomainList(domain, 2)) host.style.color = '#ffa'; setTooltip(host, getHostTooltip()); host.onclick = keyHandlers; addStickerItems(-1, host); } if (!isTypeOK) { type.onclick = function(evt) { if (!evt.shiftKey || !confirm(`This will add "${imageDetails.mimeType}" to whitelisted image types`)) return false; preferredTypes.push(imageDetails.mimeType); GM_setValue('preferred_types', preferredTypes); alert('MIME types whitelist successfully updated. The change will apply on next page load.'); return false; }; setTooltip(type, 'Shift + click to whitelist mimietype'); addStickerItems(1, type); } if (!imageDetails.localProxy && !isSizeOK && imageDetails.mimieType != 'image/gif') downsize = size; if (groupId > 0) imageHostHelper.then(function(ihh) { function setClick2Go(elem, clickHandler, tooltip) { if (!(elem instanceof HTMLElement) || elem.classList.contains('blacklisted-from-click2go')) return null; if (typeof clickHandler != 'function') throw 'Invalid argument'; elem.classList.add('click2go'); elem.style.cursor = 'pointer'; 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(elem, tooltip); return elem; } let summary, tableIndex; if ('tableIndex' in img.dataset) tableIndex = parseInt(img.dataset.tableIndex); setClick2Go(lookup, function(evt) { evt.stopPropagation(); if (evt.currentTarget.disabled) return false; else evt.currentTarget.disabled = true; lookup = evt.currentTarget; img.style.opacity = 0.3; if (lookup == resolution) summary = 'Automated attempt to lookup better quality cover'; queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup => coverLookup(torrentGroup, ihh) .then(imageUrls => ihh.rehostImageLinks(imageUrls[0], true, false, false).then(ihh.singleImageGetter) .then(imageUrl => setGroupImage(torrentGroup.group.id, imageUrl, summary).then(function(response) { console.log('[Cover Inspector]', response); setNewSrc(img, imageUrl); setSticker(imageUrl).then(function(status) { if ((status & 0b100) != 0) { if (inCollage(torrentGroup, 2)) removeFromCollage(badCoverCollages[2], torrentGroup.group.id); } else if (torrentGroup.group.categoryId == 1 && !inCollage(torrentGroup, 2)) addToCollage(2, torrentGroup.group.id); }); if (inListing && autoOpenSucceed) openGroup(torrentGroup.group.id); })))).catch(function(reason) { ihh.logFail(`groupId ${groupId} cover lookup failed: ${reason}`); img.style.opacity = 1; lookup.disabled = false; }).then(() => { counterDecrement('process-covers-countdown', tableIndex) }); }, lookup == resolution ? 'Poor image quality (resolution)' : undefined ) || setClick2Go(downsize, function(evt) { evt.stopPropagation(); if (evt.currentTarget.disabled) return false; else evt.currentTarget.disabled = true; downsize = evt.currentTarget; img.style.opacity = 0.3; ihh.reduceImageSize(imageDetails.src.href, 2160, 90).then(output => output.size < imageDetails.size ? ihh.rehostImages([output.uri]).then(ihh.singleImageGetter).then(function(rehostedImgUrl) { summary = 'Automated cover downsize'; if (!isSizeOK) summary += ` (${formattedSize(imageDetails.size)} → ${formattedSize(output.size)})`; return setGroupImage(groupId, rehostedImgUrl, summary).then(function(response) { console.log('[Cover Inspector]', response); setNewSrc(img, rehostedImgUrl); setSticker(rehostedImgUrl); }); }) : Promise.reject('Converted image not smaller')).catch(function(reason) { ihh.logFail(`groupId ${groupId} cover downsize failed: ${reason}`); img.style.opacity = 1; downsize.disabled = false; }).then(() => { counterDecrement('process-covers-countdown', tableIndex) }); }, 'Downsize on click') || setClick2Go(host, function(evt) { evt.stopPropagation(); if (evt.shiftKey || evt.ctrlKey || evt.altKey) return keyHandlers(evt); if (evt.currentTarget.disabled) return false; else evt.currentTarget.disabled = true; host = evt.currentTarget; img.style.opacity = 0.3; summary = 'Automated cover rehost'; //summary += ' (' + imageDetails.src.hostname + ')'; getImageMax(imageDetails.src.href).then(maxImgUrl => ihh.rehostImageLinks(maxImgUrl, true).then(ihh.singleImageGetter)) .then(rehostedImgUrl => setGroupImage(groupId, rehostedImgUrl, summary).then(function(response) { console.log('[Cover Inspector]', response); setNewSrc(img, rehostedImgUrl); setSticker(rehostedImgUrl); })).catch(function(reason) { ihh.logFail(`groupId ${groupId} cover rehost failed: ${reason}`); img.style.opacity = 1; host.disabled = false; }).then(() => { counterDecrement('process-covers-countdown', tableIndex) }); }); }); } sticker.title = imageDetails.src.href; //setTooltip(sticker, imageDetails.src.href); sticker.onmouseenter = img.onmouseenter = evt => { sticker.style.opacity = 1 }; img.insertAdjacentElement('afterend', sticker); const status = 1 << 8 | 1 << 7 | (![host, downsize, lookup].some(elem => elem instanceof HTMLElement)) << 6 | !imageDetails.localProxy << 5 | isPreferredHost << 4 | isSizeOK << 3 | isResolutionAcceptable << 2 | isResolutionFine << 1 | isTypeOK << 0; img.dataset.statusFlags = status.toString(2).padStart(9, '0'); return status; }).catch(function(reason) { img.hidden = true; sticker.style = ` position: static; padding: 10pt; box-sizing: border-box; width: ${inListing ? '90px' : '100%'}; z-index: 1; text-align: center; background-color: red; font: 700 auto "Segoe UI", sans-serif; `; sticker.append(span('INVALID')); if (groupId > 0 && !isSecondaryCover) editOnClick(sticker, true); setTooltip(sticker, reason); img.insertAdjacentElement('afterend', sticker); img.dataset.statusFlags = (1 << 8).toString(2).padStart(9, '0'); return 1 << 8; }); } 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 => setGroupImage(groupId, imageUrl, 'Cover update from external link').then(function(response) { console.log('[Cover Inspector]', response); setNewSrc(img, imageUrl); setSticker(imageUrl); })).catch(function(reason) { ihh.logFail(`groupId ${groupId} cover update failed: ${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)); img.dataset.statusFlags = (0).toString(2).padStart(8, '0'); if (groupId > 0) editOnClick(img, true); return Promise.resolve(0); } const dcApiRateControl = { }, dcApiRequestsCache = new Map; function coverLookup(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 bareReleaseTitle = title => title && [ /\s+(?:EP|E\.\s?P\.|-\s+(?:EP|E\.\s?P\.))$/i, /\s+\((?:EP|E\.\s?P\.|Live)\)$/i, /\s+\[(?:EP|E\.\s?P\.|Live)\]$/i, ///\s+\((?:feat\.|ft\.|featuring\s).+\)$/i, /\s+\[(?:feat\.|ft\.|featuring\s).+\]$/i, ].reduce((title, rx) => title.replace(rx, ''), title.trim()); const audioFileCount = torrent => torrent && torrent.fileList ? torrent.fileList.split('|||').filter(file => /^(.+\.(?:flac|mp3|m4[ab]|aac|dts(?:hd)?|truehd|ac3|ogg|opus|wv|ape))\{{3}(\d+)\}{3}$/i.test(file)).length : 0; const lookupWorkers = [ ]; // Ext. lookup at iTunes if (torrentGroup.group.categoryId == 1) { const apiQuery = (endpoint, queryParams, noAmbiguity = false) => endpoint && queryParams ? new Promise(function(resolve, reject) { endpoint = new URL(endpoint.toLowerCase(), 'https://itunes.apple.com'); for (let field in queryParams) endpoint.searchParams.set(field, queryParams[field]); endpoint.searchParams.set('media', 'music'); endpoint.searchParams.set('entity', 'album'); GM_xmlhttpRequest({ method: 'GET', url: endpoint, headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, responseType: 'json', onload: function(response) { if (response.status >= 200 && response.status < 400) if (response.response.resultCount > 0) { let results = response.response.results; if (endpoint.pathname != '/lookup' && (results = results.filter(function(result) { let releaseYear = new Date(result.releaseDate); if (!isNaN(releaseYear)) releaseYear = releaseYear.getFullYear(); else return false; return torrentGroup.torrents.some(function(torrent) { if (torrent.fileCount < result.trackCount || torrent.remasterYear != releaseYear) return false; return audioFileCount(torrent) == result.trackCount; }); })).length <= 0) return reject('No matches'); else if (results.length > 1) { if (noAmbiguity) return reject('Ambiguous results'); console.info('[Cover Inspector] Ambiguous iTunes results for lookup query (endpoint=%s, queryParams=%o)', endpoint.pathname, queryParams); } let artworkUrls = results.map(function(result) { const imageUrl = result.artworkUrl100 || result.artworkUrl60; return imageUrl && imageUrl.replace(/\/(\d+)x(\d+)/, '/10000x10000'); }); 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)) }, }); }) : Promise.reject('Invalid argument'); lookupWorkers.push(function lookupCoversByUPC() { // 1 if (!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, '')); catNos = catNos.filter(RegExp.prototype.test.bind(/^(\d{9,13})$/)).map(catNo => parseInt(catNo)); return (catNos = catNos.filter(catNo => catNo >= 1e8)).length > 0 ? catNos : null; }); if ((upcs = upcs.filter(Boolean)).length <= 0) return Promise.reject('No torrents with UPC'); upcs = Array.prototype.concat.apply([ ], upcs); return Promise.all(upcs.map(upc => apiQuery('lookup', { upc: upc }).catch(reason => null))).then(artworkUrls => (artworkUrls = artworkUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], artworkUrls) : Promise.reject('No covers found by UPC')); }, function lookupCoversByTitleYear() { // 2 function addImportance(importance, maxArtists = 3) { if (importance && Array.isArray(torrentGroup.group.musicInfo[importance]) && torrentGroup.group.musicInfo[importance].length > 0) Array.prototype.push.apply(artistNames, torrentGroup.group.musicInfo[importance].slice(0, maxArtists).map(artist => artist.name)); } let artistNames = [ ], albumTitle = bareReleaseTitle(torrentGroup.group.name); addImportance('dj'); if (artistNames.length <= 0 && torrentGroup.group.releaseType != 7) { addImportance('artists'); if (torrentGroup.group.tags && torrentGroup.group.tags.includes('classical')) { addImportance('conductor'); //addImportance('composers'); } } if (artistNames.length <= 0) return Promise.reject('Cover lookup by artist/title/year not available'); return apiQuery('search', { term: artistNames.map(artistName => '"' + artistName + '"').join(' ') + ' "' + albumTitle + '"', attribute: 'mixTerm', }, artistNames.join(' & ').toLowerCase() == albumTitle.toLowerCase() || artistNames.join('').length + albumTitle.length < 15); }); } // Extract from desc. links lookupWorkers.push(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).then(singleResultGetter, reason => null))) .then(imageUrls => (imageUrls = imageUrls.filter(isMusicResource)).length > 0 ? imageUrls : Promise.reject('No cover images could be extracted from links in wiki body')); }); // Ext. lookup at MusicBrainz if (torrentGroup.group.categoryId == 1) { const search = (type, queryParams, strictReleaseMatch = false) => type && queryParams ? 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, headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, 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 = type.toLowerCase()) + '/'); queryParams = Object.keys(queryParams).map(field => `${field}:"${queryParams[field]}"`).join(' AND '); url.searchParams.set('query', queryParams); url.searchParams.set('fmt', 'json'); GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, 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).then(resolve) : Promise.reject('No release group'); } if (response.status >= 200 && response.status < 400) if (response.response.count > 0) switch (type) { case 'release': { let releases = response.response.releases, releaseGroupIds; if (!releases) return reject('No matches (renounced)'); const getReleaseGroupIds = releases => (releaseGroupIds = new Set(releases.map(release => release['release-group'] && release['release-group'].id))); if ((strictReleaseMatch || getReleaseGroupIds(releases).size > 1) && getReleaseGroupIds(releases = releases.filter(function(release) { let releaseYear = new Date(release.date); if (!isNaN(releaseYear)) releaseYear = releaseYear.getFullYear(); else return false; return torrentGroup.torrents.some(function(torrent) { if (torrent.fileCount < release['track-count'] || torrent.remasterYear != releaseYear) return false; return audioFileCount(torrent) == release['track-count']; }); })).size > 1) reject('Ambiguous results'); else getFromRG(releaseGroupIds).catch(function(reason) { if (releases.length > 0) Promise.all(releases.map(release => getFrontCovers('release', release.id).then(singleResultGetter, reason => null))).then(function(frontCovers) { if ((frontCovers = frontCovers.filter(Boolean)).length > 0) resolve(frontCovers); else reject('None of results has front cover'); }, reject); else reject('No matches'); }); break; } case 'release-group': { let releaseGroups = response.response['release-groups']; if (!releaseGroups) return reject('No matches (renounced)'); getFromRG(new Set(releaseGroups.map(releaseGroup => releaseGroup.id))).catch(reject); break; } default: reject('Unsupported search 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 lookupCoversByBarcode() { // 3 if (!Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject('Cover lookup by barcode not available'); let barcodes = torrentGroup.torrents.map(function(torrent) { let catNos = torrent.remasterCatalogueNumber.split('/').map(catNo => catNo.replace(/\s+/g, '')); catNos = catNos.filter(RegExp.prototype.test.bind(/^(\d{9,13})$/)).map(catNo => parseInt(catNo)); return (catNos = catNos.filter(catNo => catNo >= 1e8)).length > 0 ? catNos : null; }); if ((barcodes = barcodes.filter(Boolean)).length <= 0) return Promise.reject('No torrents with barcode'); barcodes = Array.prototype.concat.apply([ ], barcodes); return Promise.all(barcodes.map(barcode => search('release', { barcode: barcode }).catch(reason => null))) .then(imageUrls => (imageUrls = imageUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], imageUrls) : Promise.reject('No covers found by barcode')); }, function lookupCoversByCatNo() { // 4 if (!Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject('Cover lookup by label/cat.bo. not available'); let queryParams = 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 ((queryParams = queryParams.filter(Boolean)).length <= 0) return Promise.reject('No torrents with label/cat.no.'); return Promise.all(queryParams.map(queryParams => search('release', queryParams).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 lookupCoversByTitleYear() { // 5 let artistName = torrentGroup.group.musicInfo.dj && torrentGroup.group.musicInfo.dj[0]; if (!artistName) artistName = torrentGroup.group.musicInfo.artists && torrentGroup.group.musicInfo.artists[0]; if (!artistName) return Promise.reject('Cover lookup by artist/album/year not available'); return search('release-group', { artistname: artistName.name, releasegroup: bareReleaseTitle(torrentGroup.group.name), firstreleasedate: torrentGroup.group.year, }); }); } // Ext. lookup at Discogs, requ. credentials if (torrentGroup.group.categoryId == 1 && dcAuth) { function search(type, queryParams, strictReleaseMatch = false) { if (!type || !queryParams) throw 'Invalid argument'; const url = new URL('https://api.discogs.com/database/search'); for (let field in queryParams) url.searchParams.set(field, queryParams[field]); if (type) url.searchParams.set('type', type = type.toLowerCase()); url.searchParams.sort = 'score'; url.searchParams.sort_order = 'desc'; const cacheKey = url.pathname.slice(1) + url.search; if (dcApiRequestsCache.has(cacheKey)) return dcApiRequestsCache.get(cacheKey); let retryCounter = 0; const request = new Promise((resolve, reject) => (function request() { const now = Date.now(); const postpone = () => { setTimeout(request, dcApiRateControl.timeFrameExpiry - now) }; if (!dcApiRateControl.timeFrameExpiry || now > dcApiRateControl.timeFrameExpiry) { dcApiRateControl.timeFrameExpiry = now + 60 * 1000 + 500; if (dcApiRateControl.requestDebt > 0) { dcApiRateControl.requestCounter = Math.min(60, dcApiRateControl.requestDebt); dcApiRateControl.requestDebt -= dcApiRateControl.requestCounter; console.assert(dcApiRateControl.requestDebt >= 0, 'dcApiRateControl.requestDebt >= 0'); } else dcApiRateControl.requestCounter = 0; } if (++dcApiRateControl.requestCounter <= 60) GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Authorization': 'Discogs ' + dcAuth, }, responseType: 'json', onload: function(response) { function getFromResults(results) { if (!results || results.length <= 0) return reject('No matches'); const coverImages = results.map(result => result.cover_image || singleResultGetter(result.images)) .filter(coverImage => coverImage && !coverImage.endsWith('/spacer.gif')); if (coverImages.length > 0) resolve(coverImages); else reject('None of results has cover'); } function getFromMR(masterIds) { if (!masterIds || masterIds.size <= 0) return Promise.reject('No matches'); if (masterIds.size > 1) return Promise.reject('Ambiguous results'); if (!((masterIds = masterIds.values().next().value) > 0)) return Promise.reject('No master release'); return ihh.imageUrlResolver('https://www.discogs.com/master/' + masterIds) .then(singleResultGetter).then(resolve); } let requestsUsed = /^(?:x-discogs-ratelimit):\s*(\d+)\b/im.exec(response.responseHeaders); requestsUsed = /^(?:x-discogs-ratelimit-used):\s*(\d+)\b/im.exec(response.responseHeaders); if (requestsUsed != null && (requestsUsed = parseInt(requestsUsed[1]) + 1) > dcApiRateControl.requestCounter) { dcApiRateControl.requestCounter = requestsUsed; dcApiRateControl.requestDebt = Math.max(requestsUsed - 60, 0); } if (response.status >= 200 && response.status < 400) { let results = response.response.results, masterIds; if (results && results.length > 0) switch (type) { case 'release': { function getTrackCount(type, id) { } function verifiedResult(result) { if (!result) return false; let releaseYear = new Date(result.year); if (!isNaN(releaseYear)) releaseYear = releaseYear.getFullYear(); else return false; return torrentGroup.torrents.some(function(torrent) { if (!torrent || torrent.remasterYear != releaseYear) return false; if (!result.tracklist) return true; if (torrent.fileCount < result.tracklist.length) return false; return audioFileCount(torrent) == result.tracklist.length; }); } const getMasterIds = () => new Set(results.map(result => result.master_id)); if (strictReleaseMatch) results = results.filter(result => result.master_id > 0 ? false : verifiedResult(result)); else if (getMasterIds().size > 1) results = results.filter(verifiedResult); if (results.length > 1) { if (strictReleaseMatch) return reject('Ambiguous results'); console.info('[Cover Inspector] Ambiguous Discogs results for lookup query (type=%s, queryParams=%o)', type, queryParams); } if ((masterIds = getMasterIds()).size > 1) reject('Ambiguous results'); else getFromMR(masterIds).catch(reason => { getFromResults(results) }); break; } case 'master': if (results.length > 1) reject('Ambiguous results'); else getFromResults(results); break; default: reject('Unsupported search type'); } else reject('No matches'); } else if (response.status == 429) { console.warn(defaultErrorHandler(response), response.response.message, '(' + retryCounter + ')', `Rate limit used: ${requestsUsed}/60`); postpone(); } else reject(defaultErrorHandler(response)); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); else postpone(); })()); dcApiRequestsCache.set(cacheKey, request); return request; } lookupWorkers.push(function lookupCoversByBarcode() { // 6 if (!Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject('Cover lookup by barcode not available'); let barcodes = torrentGroup.torrents.map(function(torrent) { let catNos = torrent.remasterCatalogueNumber.split('/').map(catNo => catNo.replace(/\s+/g, '')); catNos = catNos.filter(RegExp.prototype.test.bind(/^(\d{9,13})$/)).map(catNo => parseInt(catNo)); return (catNos = catNos.filter(catNo => catNo >= 1e8)).length > 0 ? catNos : null; }); if ((barcodes = barcodes.filter(Boolean)).length <= 0) return Promise.reject('No torrents with barcode'); barcodes = Array.prototype.concat.apply([ ], barcodes); return Promise.all(barcodes.map(barcode => search('release', { barcode: barcode }).catch(reason => null))) .then(imageUrls => (imageUrls = imageUrls.filter(Boolean)).length > 0 ? Array.prototype.concat.apply([ ], imageUrls) : Promise.reject('No covers found by barcode')); }, function lookupCoversByCatNo() { // 7 if (!Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0) return Promise.reject('Cover lookup by label/cat.bo. not available'); let queryParams = torrentGroup.torrents.map(function(torrent) { if (!torrent.remasterRecordLabel || torrent.remasterRecordLabel.includes('/')) return null; if (!torrent.remasterCatalogueNumber || torrent.remasterCatalogueNumber.includes('/')) return null; const queryParams = { label: bareRecordLabel(torrent.remasterRecordLabel), catno: torrent.remasterCatalogueNumber, }; if ([6, 7].includes(torrentGroup.group.releaseType)) queryParams.format = 'Compilation'; return queryParams; }); if ((queryParams = queryParams.filter(Boolean)).length <= 0) return Promise.reject('No torrents with label/cat.no.'); return Promise.all(queryParams.map(queryParams => search('release', queryParams).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 lookupCoversByTitleYear() { // 8 let artistName = torrentGroup.group.musicInfo.dj && torrentGroup.group.musicInfo.dj[0]; if (!artistName && torrentGroup.group.releaseType != 7 && torrentGroup.group.musicInfo.artists) artistName = torrentGroup.group.musicInfo.artists[0]; if (!artistName && torrentGroup.group.releaseType != 7) return Promise.reject('Cover lookup by artist/album/year not available'); const queryParams = { }; if (artistName) queryParams.artist = artistName.name; queryParams.release_title = bareReleaseTitle(torrentGroup.group.name); queryParams.year = torrentGroup.group.year; if ([6, 7].includes(torrentGroup.group.releaseType)) queryParams.format = 'Compilation'; queryParams.strict = true; //!artistName return search('master', queryParams); }, function lookupCoversByTitleRlsYear() { // 9 let artistName = torrentGroup.group.musicInfo.dj && torrentGroup.group.musicInfo.dj[0]; if (!artistName && torrentGroup.group.releaseType != 7 && torrentGroup.group.musicInfo.artists) artistName = torrentGroup.group.musicInfo.artists[0]; if (!artistName/* && torrentGroup.group.releaseType != 7*/) return Promise.reject('Cover lookup by artist/album/year not available'); const queryParams = { }; if (artistName) queryParams.artist = artistName.name; queryParams.release_title = bareReleaseTitle(torrentGroup.group.name); if ([6, 7].includes(torrentGroup.group.releaseType)) queryParams.format = 'Compilation'; queryParams.strict = true; //!artistName return search('release', queryParams, true); }); } // Ext. lookup at Goodreads - for ebooks only if (torrentGroup.group.categoryId == 3) { function search(queryParams, noAmbiguity = true) { if (!queryParams) throw 'Invalid argument'; return new Promise(function(resolve, reject) { const requestUrl = new URL('https://www.goodreads.com/search'); for (let param in queryParams) requestUrl.searchParams.set(param, queryParams[param]); requestUrl.searchParams.set('search_type', 'books'); GM_xmlhttpRequest({ method: 'GET', url: requestUrl, headers: { 'Accept': 'text/html', 'X-Requested-With': 'XMLHttpRequest' }, responseType: 'document', onload: function(response) { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); const grImageMax = src => src && src.replace(/\._(?:\w+\d+_)+\./ig, '.'); let results = response.response.querySelector('div#imagecol img#coverImage') if (results != null && httpParser.test(results = results.src)) { if (!results.includes('/nophoto/book/')) return resolve([grImageMax(results)]); } else { results = response.response.querySelectorAll('table.tableList > tbody > tr'); if (results.length <= 0) return reject('No matches'); if (results.length > 1) { if (noAmbiguity) return reject('Ambiguous results'); console.warn('[Cover Inspector] Goodreads ambiguous results'); } if ((results = Array.prototype.map.call(results, function(result) { let coverUrl = result.querySelector('img[itemprop="image"]'); if (coverUrl != null && httpParser.test(coverUrl = coverUrl.src) && ![ '/nophoto/book/', '/books/1570622405l/50809027', ].some(pattern => coverUrl.includes(pattern))) return grImageMax(coverUrl); }).filter(Boolean)).length > 0) return resolve(results); } reject('No valid cover image for matched ebook'); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }); } function findByIdentifier(rx, minLength) { if (!(rx instanceof RegExp) || !(minLength >= 0)) throw 'Invalid argument'; let id = rx.exec(descBody.textContent); if (id != null && (id = id[2].replace(/\W/g, '')).length >= minLength) lookupWorkers.push(() => search({ q: id })); } const descBody = domParser.parseFromString(torrentGroup.group.wikiBody, 'text/html').body; findByIdentifier(/\b(ISBN-?13)\b.+?\b(\d+(?:\-\d+)*)\b/m, 12); findByIdentifier(/\b(ISBN(?:-?10)?)\b.+?\b(\d+(?:\-\d+)*)\b/m, 9); findByIdentifier(/\b(EAN(?:-?13)?)\b.+?\b(\d+(?:\-\d+)*)\b/m, 12); findByIdentifier(/\b(UPC(?:-A)?)\b.+?\b(\d+(?:\-\d+)*)\b/m, 11); findByIdentifier(/\b(ASIN)\b.+?\b([A-Z\d]{10})\b/m, 11); lookupWorkers.push(() => search({ q: torrentGroup.group.name .replace(/(?:\s+(?:\((?:19|2\d)\d{2}\)|\[(?:19|2\d)\d{2}\]|\((?:epub|mobi|pdf)\)|\[(?:epub|mobi|pdf)\]))+$/ig, '') })); } return (function lookupMethod(index = 0) { if (index < lookupWorkers.length) return lookupWorkers[index]().then(function(results) { console.log('[Cover Inspector] Covers lookup successfull for', torrentGroup, ', method index:', index); return results; }, reason => lookupMethod(index + 1)); return Promise.reject('None of release identifiers was sufficient to find the cover'); })(); } function findCover(groupId, img) { if (!(groupId > 0)) throw 'Invalid argument'; return imageHostHelper.then(ihh => queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup => coverLookup(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); if (badCoverCollages) for (let collageIndex of [0, 1]) if (inCollage(torrentGroup, collageIndex)) removeFromCollage(badCoverCollages[collageIndex], torrentGroup.group.id); if (!(img instanceof HTMLImageElement)) img = document.body.querySelector('div#covers img'); if (img instanceof HTMLImageElement) { setNewSrc(img, imageUrl); inspectImage(img, torrentGroup.group.id).then(function(status) { if ((status & 0b100) != 0) { if (inCollage(torrentGroup, 2)) removeFromCollage(badCoverCollages[2], torrentGroup.group.id); } else if (torrentGroup.group.categoryId == 1 && !inCollage(torrentGroup, 2)) addToCollage(2, torrentGroup.group.id); }, reason => { console.warn('[Cover Inspector] inspectImage(', img, ') failed with reason', reason) }); } else testImageQuality(imageUrl).then(mpix => { if (inCollage(torrentGroup, 2)) removeFromCollage(badCoverCollages[2], torrentGroup.group.id) }, reason => { if (torrentGroup.group.categoryId == 1 && !inCollage(torrentGroup, 2)) addToCollage(2, torrentGroup.group.id) }); }))).catch(function(reason) { if (torrentGroup.group.wikiImage && !inCollage(torrentGroup, 1)) ihh.verifyImageUrl(torrentGroup.group.wikiImage).catch(reason => { addToCollage(1, torrentGroup.group.id) }); return Promise.reject(reason); }))); } function getGroupId(root) { if (root instanceof HTMLElement) for (let a of root.getElementsByTagName('A')) { if (a.origin != document.location.origin || a.pathname != '/torrents.php') continue; a = new URLSearchParams(a.search); if (a.has('id') && !a.has('action') && (a = parseInt(a.get('id'))) > 0) return a; } console.warn('[Cover Inspector] Failed to find group id:', root); } function addTableHandlers(table, parent, style, index) { function addHeaderButton(caption, clickHandler, id, tooltip) { if (!caption || typeof clickHandler != 'function') return; const elem = document.createElement('SPAN'); if (id) elem.classList.add(id); elem.classList.add('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); return elem; } function iterateReleaseGroups(callback) { for (const tr of table.querySelectorAll('tbody > tr.group, tbody > tr.torrent')) { const groupId = getGroupId(tr.querySelector('div.group_info')); console.assert(groupId > 0, 'Failed to extract group id:', tr) if (groupId > 0) callback(groupId, tr.querySelector('div.group_image > img')); } } function getGroupCreationTime(elem) { if (!(elem instanceof HTMLElement) || !((elem = getGroupId(elem.querySelector('div.group_info'))) > 0)) return; if ((elem = document.body.querySelectorAll(`tr.group_torrent.groupid_${elem} *.time[title]`)).length <= 0) return; if ((elem = Array.from(elem, elem => new Date(elem.title)).filter(date => !isNaN(date))).length <= 0) return; return Math.min(...elem.map(date => date.getTime())); } function changeToCounter(elem, id) { if (!(elem instanceof HTMLElement) || !id) throw 'Invalid argument'; if (!elem.count) { elem.remove(); return null; } elem.onclick = elem.onmouseenter = elem.onmouseleave = null; elem.style.color = 'orange'; elem.style.cursor = null; elem.textContent = ' releases remaining'; elem.removeAttribute('title'); const counter = document.createElement('SPAN'); counter.className = id; counter.textContent = counter.count = elem.count; counter.style.fontWeight = 'bold'; elem.prepend(counter); delete elem.count; return elem; } if (!(table instanceof HTMLElement) || !(parent instanceof HTMLElement)) return; const images = table.querySelectorAll('tbody > tr div.group_image > img'); if (index) for (let img of images) img.dataset.tableIndex = index; const container = document.createElement('DIV'); container.className = index ? 'cover-inspector-' + index : 'cover-inspector'; if (style) container.style = style; if (images.length > 0) addHeaderButton('Inspect all covers', function inspectAll(evt) { if (!evt.currentTarget.disabled) evt.currentTarget.disabled = true; else return false; evt.currentTarget.style.color = evt.currentTarget.dataset.color = 'orange'; evt.currentTarget.textContent = '…wait…'; evt.currentTarget.style.cursor = null; const currentTarget = evt.currentTarget, inspectWorkers = [ ]; let autoFix = parent.querySelector('span.auto-fix-covers'); iterateReleaseGroups((groupId, img) => { if (img != null) inspectWorkers.push(inspectImage(img, groupId)) }); if (autoFix != null && inspectWorkers.length > 0) autoFix.hidden = true; (inspectWorkers.length > 0 ? imageHostHelper.then(ihh => Promise.all(inspectWorkers).then(function(statuses) { const failedToLoad = statuses.filter(status => (status >> 7 & 0b11) == 0b10).length; if (autoFix != null || (autoFix = parent.querySelector('span.auto-fix-covers')) != null) if (failedToLoad > 0) { autoFix.hidden = false; autoFix.count = statuses.filter(status => (status >> 7 & 0b01) == 0).length; autoFix.title = autoFix.count.toString() + ' covers to lookup (missing covers included)'; } else autoFix.remove(); const minimumRehostAge = GM_getValue('minimum_age_for_rehost'); const getClick2Gos = () => Array.prototype.filter.call(table.querySelectorAll('div.cover-inspector > span.click2go:not([disabled])'), function(elem) { if (elem.classList.contains('whitelisted')) return true; if (elem.classList.contains('xtrn-host')) { if (!(minimumRehostAge > 0)) return false; while (elem != null && elem.nodeName != 'TR') elem = elem.parentNode; if (!((elem = getGroupCreationTime(elem)) > 0)) return false; return elem < Date.now() - minimumRehostAge * 24 * 60 * 60 * 1000; } return true; }); if ((currentTarget.count = getClick2Gos().length) > 0) { currentTarget.id = 'process-all-covers'; currentTarget.onclick = function processAll(evt) { if (evt.currentTarget.disabled) return false; if (failedToLoad > 0 && evt.ctrlKey) return inspectAll(evt); const click2Gos = getClick2Gos(); evt.currentTarget.count = click2Gos.length; changeToCounter(evt.currentTarget, 'process-covers-countdown'); for (let elem of click2Gos) elem.click(); }; currentTarget.style.color = currentTarget.dataset.color = 'mediumseagreen'; currentTarget.textContent = 'Process existing covers'; currentTarget.style.cursor = 'pointer'; currentTarget.disabled = false; currentTarget.title = currentTarget.count.toString() + ' releases to process'; console.log('[Cover Inspector] Page scan completed, %d images cached', Object.keys(imageDetailsCache).length); if (failedToLoad > 0) currentTarget.title += `\n(${failedToLoad} covers failed to load, scan again on Ctrl + click)`; } else return Promise.reject('Nothing to process'); })) : Promise.reject('Nothing to process')).catch(reason => { currentTarget.remove() }); }, 'inspect-all-covers'); imageHostHelper.then(function(ihh) { function setCoverFromTorrentGroup(torrentGroup, img, reason) { if (!torrentGroup) throw 'Invalid argument'; return coverLookup(torrentGroup, ihh).then(imageUrls => ihh.rehostImageLinks(imageUrls[0], true, false, false).then(ihh.singleImageGetter).then(imageUrl => setGroupImage(torrentGroup.group.id, imageUrl, autoLookupSummary(reason)).then(function(response) { console.log('[Cover Inspector]', response); if (badCoverCollages) for (let collageIndex of [0, 1]) if (inCollage(torrentGroup, collageIndex)) removeFromCollage(badCoverCollages[collageIndex], torrentGroup.group.id); if (img instanceof HTMLImageElement) { setNewSrc(img, imageUrl); inspectImage(img, torrentGroup.group.id).then(function(status) { if ((status & 0b100) != 0) { if (inCollage(torrentGroup, 2)) removeFromCollage(badCoverCollages[2], torrentGroup.group.id); } else if (torrentGroup.group.categoryId == 1 && !inCollage(torrentGroup, 2)) addToCollage(2, torrentGroup.group.id); }, reason => { console.warn('[Cover Inspector] inspectImage(', img, ') failed with reason', reason) }); } else testImageQuality(imageUrl).then(mpix => { if (inCollage(torrentGroup, 2)) removeFromCollage(badCoverCollages[2], torrentGroup.group.id) }, reason => { if (torrentGroup.group.categoryId == 1 && !inCollage(torrentGroup, 2)) addToCollage(2, torrentGroup.group.id) }); if (autoOpenSucceed) openGroup(torrentGroup.group.id); return imageUrl; }))).catch(function(reason) { if (torrentGroup.group.wikiImage && !inCollage(torrentGroup, 1)) ihh.verifyImageUrl(torrentGroup.group.wikiImage) .catch(reason => { addToCollage(1, torrentGroup.group.id) }); if (Array.isArray(torrentGroup.torrents) && torrentGroup.torrents.length > 0) Promise.all(torrentGroup.torrents.filter(torrent => /\b(?:https?):\/\//i.test(torrent.description)) .map(torrent => bb2Html(torrent.description).then(getLinks, reason => null))).then(function(urls) { if ((urls = urls.filter(Boolean).map(urls => urls.filter(isMusicResource)).filter(urls => urls.length > 0)).length <= 0) return; if (autoOpenWithLink) openGroup(torrentGroup.group.id); console.log('[Cover Inspector] Links found in torrent descriptions for', torrentGroup, ':', urls); }); ihh.logFail(`groupId ${torrentGroup.group.id} cover lookup failed: ${reason}`); }); } const missingImages = Array.prototype.filter.call(images, img => !hasArtworkSet(img)); if (images.length <= 0 || missingImages.length > 0) addHeaderButton('Add missing covers', function autoAdd(evt) { if (images.length <= 0 || (evt.currentTarget.count = Array.prototype.filter.call(images, img => !hasArtworkSet(img)).length) <= 0) { evt.currentTarget.remove(); if (images.length > 0) return; } else changeToCounter(evt.currentTarget, 'missing-covers-countdown'); iterateReleaseGroups(function(groupId, img) { if (img instanceof HTMLImageElement) { if (!hasArtworkSet(img)) queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup => { setCoverFromTorrentGroup(torrentGroup, img, 'missing').then(() => { counterDecrement('missing-covers-countdown', index) }) }); } else queryAjaxAPI('torrentgroup', { id: groupId }).then(function(torrentGroup) { if (!torrentGroup.group.wikiImage) setCoverFromTorrentGroup(torrentGroup, null, 'missing') .then(() => { counterDecrement('missing-covers-countdown', index) }); }); }); }, 'auto-add-covers', missingImages.length > 0 ? (missingImages.length + ' covers missing') : undefined); addHeaderButton('Fix invalid covers', function autoFix(evt) { if (evt.currentTarget.count > 0) changeToCounter(evt.currentTarget, 'invalid-covers-countdown'); else evt.currentTarget.remove(); const autoAdd = parent.querySelector('span.auto-add-covers'); if (autoAdd != null) autoAdd.remove(); iterateReleaseGroups(function(groupId, img) { if (img instanceof HTMLImageElement) (function() { if (!hasArtworkSet(img)) return Promise.reject('not set'); const realImageUrl = realImgSrc(img), deproxiedSrc = deProxifyImgSrc(realImageUrl); return deproxiedSrc ? setGroupImage(groupId, deproxiedSrc, 'Deproxied release image (not working anymore)') .then(result => ihh.verifyImageUrl(deproxiedSrc)) : ihh.verifyImageUrl(realImageUrl); })().catch(function(reason) { console.log('[Cover Inspector] Invalid or missing cover for groupId %d, reason:', groupId, reason); queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup => { setCoverFromTorrentGroup(torrentGroup, img, reason).then(() => { counterDecrement('invalid-covers-countdown', index) }) }, ihh.logFail); }); else queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup => (function() { if (!torrentGroup.group.wikiImage) return Promise.reject('not set'); const deproxiedSrc = deProxifyImgSrc(torrentGroup.group.wikiImage); return deproxiedSrc ? setGroupImage(groupId, deproxiedSrc, 'Deproxied release image (not working anymore)') .then(result => ihh.verifyImageUrl(deproxiedSrc)) : ihh.verifyImageUrl(torrentGroup.group.wikiImage); })().catch(function(reason) { console.log('[Cover Inspector] Invalid or missing cover for groupId %d, reason:', groupId, reason); setCoverFromTorrentGroup(torrentGroup, null, reason).then(() => { counterDecrement('invalid-covers-countdown', index) }); }), ihh.logFail); }); }, 'auto-fix-covers', 'Missing covers lookup included'); if (missingImages.length > 0) for (const img of missingImages) { img.removeAttribute('onclick'); const groupId = getGroupId(img.parentNode.parentNode.querySelector('div.group_info')); if (groupId > 0) img.onclick = function(evt) { findCover(groupId, evt.currentTarget).catch(reason => { ihh.logFail(`groupId ${groupId} cover lookup failed: ${reason}`) }); return false; } } }); parent.append(container); } const params = new URLSearchParams(document.location.search), id = parseInt(params.get('id')) || undefined; const findParent = table => table instanceof HTMLElement && Array.prototype.find.call(table.querySelectorAll(':scope > tbody > tr.colhead > td'), td => /^(?:Torrents?|Name)\b/.test(td.textContent.trim())) || null; switch (document.location.pathname) { case '/artist.php': { if (!(id > 0)) break; document.body.querySelectorAll('div.box_image 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;' // document.body.querySelectorAll('table.torrent_table').forEach(function(table, index) { // const parent = findParent(table); // if (parent) addTableHandlers(table, parent, 'display: inline-block; margin-left: 3em;', index + 1); // }); break; } case '/torrents.php': { if (id > 0) { for (let img of document.body.querySelectorAll('div#covers 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'; const img = document.body.querySelector('div#covers img'); ihh.imageUrlResolver(a.href).then(singleResultGetter).then(function(imageUrl) { if (img != null) img.style.opacity = 0.3; return ihh.rehostImageLinks(imageUrl, true, false, false).then(ihh.singleImageGetter) .then(rehostedImage => setGroupImage(id, rehostedImage, 'Cover update from description link').then(function(response) { console.log(response); if (img != null) { setNewSrc(img, rehostedImage); inspectImage(img, id); } else document.location.reload(); })); }).catch(function(reason) { ihh.logFail('Setting cover from link source failed: ' + reason); if (img != null && img.style.opacity < 1) img.style.opacity = 1; }); } 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 (const root of [ 'div.torrent_description > div.body', 'table#torrent_details > tbody > tr.torrentdetails > td > blockquote', ]) for (let a of document.body.querySelectorAll(root + ' a')) if (!noCoverHere(a)) setAnchorHandlers(a); if (GM_getValue('auto_expand_extra_covers', true)) { const xtraCovers = document.body.querySelector('div.box_image span#cover_controls_0 > a.show_all_covers'); if (xtraCovers != null) xtraCovers.click(); } GM_registerMenuCommand('Auto add cover', () => { findCover(id).catch(alert) }, 'A'); }); } else { const useIndexes = params.get('action') == 'notify'; document.body.querySelectorAll('table.torrent_table').forEach(function(table, index) { const parent = findParent(table); if (parent) addTableHandlers(table, parent, 'display: inline-block; margin-left: 17pt;', useIndexes ? index + 1 : undefined); }); } break; } case '/collages.php': case '/collage.php': { function getAllCovers(groupId) { if (!(groupId > 0)) throw 'Invalid argument'; return 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(); }); } if (!badCoverCollages.includes(id)) break; 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(newStatus, ...addedText) { if ((td = tr.querySelector('td.status')) == null) return; // assertion failed td.textContent = (status = Number(newStatus) || 0) > 1 ? 'success' : 'failed'; td.className = 'status ' + td.textContent + ' status-code-' + status; if (addedText.length > 0) Array.prototype.push.apply(tooltips, addedText); if (tooltips.length > 0) td.title = tooltips.join('\n'); else td.removeAttribute('title'); //setTooltip(td, tooltips.join('\n')); 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; } let status, tooltips = [ ]; const inspectGroupId = groupId => queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup => (function() { if (!torrentGroup.group.wikiImage) return Promise.reject('not set'); const deproxiedSrc = deProxifyImgSrc(torrentGroup.group.wikiImage); return deproxiedSrc ? setGroupImage(groupId, deproxiedSrc, 'Deproxied release image (not working anymore)') .then(result => ihh.verifyImageUrl(deproxiedSrc)) : ihh.verifyImageUrl(torrentGroup.group.wikiImage); })().then(imageUrl => (torrentGroup.group.categoryId == 1 ? testImageQuality(imageUrl) : Promise.resolve(-1)).then(function(mpix) { const hostname = new URL(imageUrl).hostname.toLowerCase(), domain = hostname.split('.').slice(-2).join('.'); if (isOnDomainList(domain, 2)) return Promise.reject('Unacknowledged host'); setStatus(3, 'This release seems to have a valid image'); const rfc = () => removeFromCollage(id, torrentGroup.group.id) .then(statusCode => { setStatus(status, '(removed from collage)') }); if (torrentGroup.group.categoryId == 1) getAllCovers(torrentGroup.group.id).then(imageUrls => Promise.all(imageUrls.slice(1).map(ihh.verifyImageUrl)).then(rfc, function(reason) { setStatus(1, '(invalid additional cover(s) require attention)', reason); }), reason => { setStatus(2, 'Could not count additiona covers (' + reason + ')') }); else rfc(); if ((!Array.isArray(preferredHosts) || !preferredHosts.includes(hostname)) && !isOnDomainList(domain, 0) && isOnDomainList(domain, 1)) { ihh.rehostImageLinks(imageUrl, true, false, true).then(ihh.singleImageGetter) .then(imageUrl => setGroupImage(torrentGroup.group.id, imageUrl, 'Automated cover rehost').then(function(response) { setStatus(status, '(' + response + ')'); console.log('[Cover Inspector]', response); })); } if (autoOpenSucceed) openGroup(torrentGroup.group.id); })).catch(reason => coverLookup(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], autoLookupSummary(reason)).then(function(response) { setStatus(3, response, '(reminder - release may contain additional covers to review)'); if (imageUrls.length > 1) setStatus(2, '(more external links in description require attention)'); console.log('[Cover Inspector]', response); if (autoOpenSucceed) openGroup(torrentGroup.group.id); const rfc = () => removeFromCollage(id, torrentGroup.group.id) .then(statusCode => { setStatus(status, '(removed from collage)') }); if (id != badCoverCollages[2]) rfc(); return testImageQuality(imageUrls[0]).then(mpix => { if (id == badCoverCollages[2]) rfc() }, function(reason) { if (id == badCoverCollages[2]) return Promise.reject(reason); else { setStatus(2, 'However the image resolution is low'); if (torrentGroup.group.categoryId == 1 && !inCollage(torrentGroup, 2)) addToCollage(2, torrentGroup.group.id).then(result => { setStatus(status, '(added to poor quality covers collage)') }); } }); // 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(); }))).catch(reason => Array.isArray(torrentGroup.torrents) && torrentGroup.torrents.length > 0 ? Promise.all(torrentGroup.torrents.filter(torrent => /\b(?:https?):\/\//i.test(torrent.description)) .map(torrent => bb2Html(torrent.description).then(getLinks, reason => null))).then(function(urls) { if ((urls = urls.filter(Boolean).map(urls => urls.filter(isMusicResource)).filter(urls => urls.length > 0)).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')); if (autoOpenWithLink) openGroup(torrentGroup.group.id); console.log('[Cover Inspector] Links found in torrent descriptions for', torrentGroup, ':', urls); }) : 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'; const groupId = getGroupId(tr); if (groupId > 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; } case '/userhistory.php': { document.body.querySelectorAll('table.torrent_table').forEach(function(table, index) { const parent = findParent(table); if (parent) addTableHandlers(table, parent, 'display: inline-block; margin-left: 3em;', index + 1); }); break; } }