op// ==UserScript==
// @name [RED] Cover Inspector
// @namespace https://greasyfork.org/users/321857-anakunda
// @version 1.13.16
// @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
// ==/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, '<br>')
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');
});
}
const maxOpenTabs = GM_getValue('max_open_tabs', 25), autoCloseTimeout = GM_getValue('tab_auto_close_timeout', 0);
let openedTabs = [ ], tabsQueueRecovery = [ ], lastOnQueue;
function openTabLimited(endpoint, params, hash) {
function updateQueueInfo() {
const id = 'waiting-tabs-counter';
let counter = document.getElementById(id);
if (counter == null) {
if (tabsQueueRecovery.length <= 0) return;
const queueInfo = document.createElement('DIV');
queueInfo.style = `
position: fixed; left: 10pt; bottom: 10pt; padding: 5pt; z-index: 999;
font-size: 8pt; color: white; background-color: sienna;
border: thin solid black; box-shadow: 2pt 2pt 5pt black; cursor: default;
`;
const tooltip = 'By closing this tab the queue will be discarded';
if (typeof jQuery.fn.tooltipster == 'function') $(queueInfo).tooltipster({ content: tooltip });
else queueInfo.title = tooltip;
counter = document.createElement('SPAN');
counter.id = id;
counter.style.fontWeight = 'bold';
queueInfo.append(counter, ' release group(s) queued to view');
document.body.append(queueInfo);
} else if (tabsQueueRecovery.length <= 0) {
document.body.removeChild(counter.parentNode);
return;
}
counter.textContent = tabsQueueRecovery.length;
}
if (typeof GM_openInTab != 'function') return Promise.reject('Not supported');
if (!endpoint) return Promise.reject('Invalid argument');
const saveQueue = () => localStorage.setItem('coverInspectorTabsQueue', JSON.stringify(tabsQueueRecovery));
let recoveryEntry;
if (maxOpenTabs > 0) {
tabsQueueRecovery.push(recoveryEntry = { endpoint: endpoint, params: params || null, hash: hash || '' });
if (openedTabs.length >= maxOpenTabs) updateQueueInfo();
saveQueue();
}
const waitFreeSlot = () => (maxOpenTabs > 0 && openedTabs.length >= maxOpenTabs ?
Promise.race(openedTabs.map(tabHandler => new Promise(function(resolve) {
console.assert(!tabHandler.closed);
if (!tabHandler.closed) tabHandler.resolver = resolve; //else resolve(tabHandler);
}))) : Promise.resolve(null)).then(function(tabHandler) {
console.assert(openedTabs.length <= maxOpenTabs);
const url = new URL(endpoint + '.php', document.location.origin);
if (params) for (let param in params) url.searchParams.set(param, params[param]);
if (hash) url.hash = hash;
(tabHandler = GM_openInTab(url.href, true)).onclose = function() {
console.assert(this.closed);
if (this.autoCloseTimer >= 0) clearTimeout(this.autoCloseTimer);
const index = openedTabs.indexOf(this);
console.assert(index >= 0);
if (index >= 0) openedTabs.splice(index, 1);
else openedTabs = openedTabs.filter(opernGroup => !opernGroup.closed);
if (typeof this.resolver == 'function') this.resolver(this);
}.bind(tabHandler);
if (autoCloseTimeout > 0) tabHandler.autoCloseTimer = setTimeout(tabHandler =>
{ if (!tabHandler.closed) tabHandler.close() }, autoCloseTimeout * 1000, tabHandler);
openedTabs.push(tabHandler);
if (maxOpenTabs > 0) {
const index = tabsQueueRecovery.indexOf(recoveryEntry);
console.assert(index >= 0);
if (index >= 0) tabsQueueRecovery.splice(index, 1);
updateQueueInfo();
saveQueue();
}
return tabHandler;
});
return lastOnQueue = lastOnQueue instanceof Promise ? lastOnQueue.then(waitFreeSlot) : waitFreeSlot();
}
const openGroup = groupId => groupId > 0 ? openTabLimited('torrents', { id: groupId }) : null;
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'],
'MixCloud': ['mixcloud.com'],
'Mobilism': ['mobilism.org'],
'Mora': ['mora.jp'],
'MusicBrainz': ['coverartarchive.org'],
'NoelShack': ['noelshack.com'],
'OTOTOY': ['ototoy.jp'],
'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 hostSubstitutions = {
'pro.beatport.com': 'www.beatport.com',
};
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',
'naxos.com', 'deejay.de', 'mixcloud.com', 'cdjapan.co.jp', 'ototoy.jp',
];
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 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',
'vgy.me/Dr3kmf.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 {
const url = new URL(a), hostNorm = url.hostname.toLowerCase();
if (hostNorm in hostSubstitutions) url.hostname = hostSubstitutions[hostNorm];
return url;
} 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 spfClientId = GM_getValue('spotify_client_id'), spfClientSecret = GM_getValue('spotify_client_secret');
const spfAuth = spfClientId && spfClientSecret && btoa(spfClientId + ':' + spfClientSecret);
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 = [ ];
function getAllLabelsCatNos() {
const queryParams = torrentGroup.torrents.map(function(torrent) {
if (!torrent.remasterRecordLabel || !torrent.remasterCatalogueNumber) return null;
const [labels, catNos] = [torrent.remasterRecordLabel, torrent.remasterCatalogueNumber].map(value =>
(value = value.split('/').map(value => value.trim()).filter(Boolean)).length > 0 ? value : null).filter(Boolean);
return labels.length > 0 && catNos.length == labels.length ? labels.map((label, index) => ({
label: label.replace(/(?:\s+Record(?:s|ings)|,?\s+(?:Inc|Ltd|GmBH|a\.?s|s\.?r\.?o)\.?)+$/i, ''),
catno: catNos[index],
})) : null;
}).filter(Boolean);
return queryParams.length > 0 ? Array.prototype.concat.apply([ ], queryParams).filter((qp1, ndx, arr) =>
arr.findIndex(qp2 => Object.keys(qp2).every(key => qp2[key] == qp1[key])) == ndx) : null;
}
// 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');
const request = (retryCounter = 0) => 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 if (response.status == 403 && retryCounter < 100) {
alert('Retried HTTP error 403 on iTunes');
setTimeout(request, 1000, retryCounter + 1);
} else reject(defaultErrorHandler(response));
},
onerror: response => {
if (response.status == 403) alert('Unhandled HTTP error 403 on iTunes');
reject(defaultErrorHandler(response));
},
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
});
request();
}) : Promise.reject('Invalid argument');
lookupWorkers.push(function lookupCoversByUPC() { // 0
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() { // 1
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() { // 2
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');
const queryParams = getAllLabelsCatNos();
if (queryParams == null) 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 Spotify
if (torrentGroup.group.categoryId == 1) {
function requestEndpoint(server, endpoint, auth, params) {
if (!server || !endpoint || !auth) throw 'Invalid argument';
return new Promise(function(resolve, reject) {
const url = new URL(endpoint, 'https://' + server + '.spotify.com'), isPost = server != 'api';
if (params) if (isPost) var payload = new URLSearchParams(params);
else for (let param in params) url.searchParams.set(param, params[param]);
GM_xmlhttpRequest({ method: isPost ? 'POST' : 'GET', url: url.href,
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Authorization': auth },
responseType: 'json',
onload: function(response) {
if (response.status >= 200 && response.status < 400) resolve(response.response);
else reject(defaultErrorHandler(response));
},
onerror: response => { reject(defaultErrorHandler(response)) },
ontimeout: response => { reject(defaultTimeoutHandler(response)) },
data: payload || undefined,
});
});
}
const search = queryParams => queryParams && typeof queryParams == 'object' ? (function setOAuth2Token() {
const isTokenValid = accessToken => typeof accessToken == 'object' && accessToken.token_type
&& accessToken.access_token && accessToken.expires_at >= Date.now() + 30 * 1000;
if ('spotifyAccessToken' in localStorage) try {
const accessToken = JSON.parse(localStorage.getItem('spotifyAccessToken'));
if (isTokenValid(accessToken)) return Promise.resolve(accessToken);
} catch(e) { console.warn(e) }
const timeStamp = Date.now();
return spfAuth ? requestEndpoint('accounts', '/api/token', 'Basic ' + spfAuth, { 'grant_type': 'client_credentials' }).then(function(accessToken) {
if (!accessToken.timestamp) accessToken.timestamp = timeStamp;
if (!accessToken.expires_at) accessToken.expires_at = accessToken.timestamp +
(accessToken.expires_in_ms || accessToken.expires_in * 1000);
if (!isTokenValid(accessToken)) {
console.warn('Received invalid Spotify token:', accessToken);
return Promise.reject('invalid token received');
}
localStorage.setItem('spotifyAccessToken', JSON.stringify(accessToken));
return accessToken;
}) : Promise.reject('Basic authorization not fully configured');
})().then(function(accessToken) {
return requestEndpoint('api', '/v1/search', accessToken.token_type + ' ' + accessToken.access_token, {
q: Object.keys(queryParams).map(param => `${param}:"${queryParams[param]}"`).join(' '),
type: 'album',
limit: 50,
}).then(function(results) {
if (results.albums.total > 0) results = results.albums.items; else return Promise.reject('No matches');
console.debug('[Cover Inspector] Spotify search results for %o:', queryParams, results);
if (!Object.keys(queryParams).includes('upc')) results = results.filter(function(result) {
if (result.album_type == 'single' ? ![9, 5].includes(torrentGroup.group.releaseType)
: torrentGroup.group.releaseType == 9) return false;
if ((result.album_type == 'compilation') != [6, 7].includes(torrentGroup.group.releaseType)) return false;
let releaseYear = new Date(result.release_date);
if (isNaN(releaseYear)) return false; else releaseYear = releaseYear.getFullYear();
return torrentGroup.torrents.some(torrent => torrent.fileCount >= result.total_tracks
&& torrent.remasterYear == releaseYear && audioFileCount(torrent) == result.total_tracks);
});
if (results.length <= 0) return Promise.reject('No matches'); else if (results.length > 1) {
//return reject('Ambiguous results');
console.info('[Cover Inspector] Ambiguous Spotify results for lookup query (queryParams=%o)', queryParams);
}
results = results.map(function(result) {
if (!result.images) return null;
let highest = Math.max(...result.images.map(image => image.width * image.height));
highest = result.images.find(image => image.width * image.height == highest);
return highest && highest.url;
}).filter(Boolean);
return results.length > 0 ? results : Promise.reject('No covers');
});
}) : Promise.reject('No query provided');
lookupWorkers.push(function lookupCoversByUPC() { // 6
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 => search({ 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() { // 7
const queryParams = { };
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) queryParams.artist = artistName;
else if (torrentGroup.group.releaseType != 7)
return Promise.reject('Cover lookup by artist/album/year not available');
queryParams.album = bareReleaseTitle(torrentGroup.group.name);
return search(queryParams);
});
}
// 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() { // 8
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() { // 9
if (!Array.isArray(torrentGroup.torrents) || torrentGroup.torrents.length <= 0)
return Promise.reject('Cover lookup by label/cat.bo. not available');
const queryParams = getAllLabelsCatNos();
if (queryParams == null) 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() { // 10
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() { // 11
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(results =>
Promise.all(results.map(result => ihh.verifyImageUrl(result).catch(reason => null)))).then(function(results) {
if ((results = results.filter(Boolean)).length <= 0) return Promise.reject('No valid image');
console.log('[Cover Inspector] Covers lookup successfull for', torrentGroup, ', method index:', index);
return results;
}).catch(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;
}
}
});
// addHeaderButton('Open all in tabs', function inspectAll(evt) {
// iterateReleaseGroups(groupIdc => { openGroup(groupIdc) });
// }, 'test-tabs-control');
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)) {
const hostNorm = a.hostname.toLowerCase();
if (hostNorm in hostSubstitutions) a.hostname = hostSubstitutions[hostNorm];
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('Cover auto lookup', () => { 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;
}
// Crash recovery
if ('coverInspectorTabsQueue' in localStorage) try {
const savedQueue = JSON.parse(localStorage.getItem('coverInspectorTabsQueue'));
if (Array.isArray(savedQueue) && savedQueue.length > 0) GM_registerMenuCommand('Restore open tabs queue', function() {
if (!confirm('Delete and process saved queue?')) return;
localStorage.removeItem('coverInspectorTabsQueue');
for (let queuedEntry of savedQueue) openTabLimited(queuedEntry.endpoint, queuedEntry.params, queuedEntry.hash);
});
} catch(e) { console.warn(e) }